import {
  ApiOutlined,
  BellOutlined,
  BgColorsOutlined,
  BookOutlined,
  ClockCircleOutlined,
  CopyOutlined,
  ClusterOutlined,
  DownOutlined,
  LoadingOutlined,
  MenuFoldOutlined,
  MenuUnfoldOutlined,
  ProfileOutlined,
  ReloadOutlined,
  RocketOutlined,
  RightOutlined,
  SearchOutlined,
  SettingOutlined,
} from '@ant-design/icons';
import {
  Alert,
  Button,
  Checkbox,
  Dropdown,
  Drawer,
  Grid,
  Input,
  InputNumber,
  Layout,
  Modal,
  Select,
  Segmented,
  Space,
  Typography,
} from 'antd';
import type { ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { fetchPlanItems } from '../../features/planBoard/api';
import { copyTextToClipboard } from '../../utils/clipboard';
import { isAutomationFailedItem, isReleasePendingMainItem, isWorkingPlanItem } from '../../features/planBoard/quickFilters';
import {
  cancelServerRestartReservation,
  fetchServerCommands,
  fetchServerRestartReservation,
  restartServerCommand,
  scheduleServerRestartReservation,
} from '../../features/serverCommand/api';
import type { ServerCommandItem, ServerRestartReservation } from '../../features/serverCommand/types';
import {
  DEFAULT_APP_CONFIG,
  saveAutomationNotificationPreferenceToServer,
  saveAppConfigToServer,
  setStoredAppConfig,
  syncAppConfigFromServer,
  useAppConfig,
  type AppConfig,
  type PlanCostTimeUnit,
} from './appConfig';
import { renderModalWithEnterConfirm } from './modalKeyboard';
import { fetchWebPushConfig } from './notificationApi';
import {
  clearNotificationIdentity,
  getSavedNotificationDeviceId,
} from './notificationIdentity';
import {
  clearWebPushSubscriptionRegistration,
  ensureWebPushSubscriptionRegistered,
  syncExistingWebPushSubscriptionRegistration,
} from './webPushRegistration';
import {
  ALLOWED_REGISTRATION_TOKEN,
  setRegisteredAccessToken,
  useTokenAccess,
} from './tokenAccess';
import { isPreviewRuntime } from './previewRuntime';
import { chatConnectionGateway, chatGateway } from './chatV2';
import { emitChatConversationCleared, emitChatConversationsUpdated } from './chatV2/data/chatClientEvents';
import { HeaderMessageCenter } from './HeaderMessageCenter';
import { fetchChatRuntimeJobDetail } from './mainChatPanel';
import {
  getSharedActiveConversationSnapshot,
  subscribeSharedActiveConversation,
} from './mainChatPanel/sharedActiveConversation';
import { buildChatPath } from './routes';
import type { MainHeaderProps } from './types';
import type { ChatRuntimeJobDetail, ChatRuntimeSnapshot } from './mainChatPanel/types';

const { Header } = Layout;
const { Paragraph, Text } = Typography;
const { useBreakpoint } = Grid;
const APP_SETTINGS_CATEGORIES = [
  { value: 'automation', label: '작업' },
  { value: 'workspace', label: '작업 환경' },
] as const;

const APP_SETTINGS_SECTIONS: Array<{
  value:
    | 'headerAppearance'
    | 'chatSettings'
    | 'automationRuntime'
    | 'planDefaults'
    | 'planCost'
    | 'worklogAutomation'
    | 'automationNotifications'
    | 'gestureShortcuts';
  label: string;
  category: (typeof APP_SETTINGS_CATEGORIES)[number]['value'];
}> = [
  { value: 'headerAppearance', label: '헤더 표시', category: 'workspace' },
  { value: 'chatSettings', label: '채팅 문맥 설정', category: 'workspace' },
  { value: 'automationRuntime', label: '자동접수 / 주기', category: 'automation' },
  { value: 'planDefaults', label: '자동화 기본값', category: 'automation' },
  { value: 'planCost', label: '비용 표시', category: 'automation' },
  { value: 'worklogAutomation', label: '업무일지 자동화 설정', category: 'automation' },
  { value: 'automationNotifications', label: '자동화 알림', category: 'automation' },
  { value: 'gestureShortcuts', label: '제스처 / 단축키', category: 'workspace' },
];

const PLAN_COST_TIME_UNIT_LABELS: Record<PlanCostTimeUnit, string> = {
  hour: '시간',
  minute: '분',
  second: '초',
};

type ClientNotificationPermissionState = 'unsupported' | 'default' | 'granted' | 'denied';
type SettingsModalKey = 'appSettings' | 'notification' | 'token' | 'update';
type AppSettingsCategoryKey = (typeof APP_SETTINGS_CATEGORIES)[number]['value'];
type AppSettingsSectionKey = (typeof APP_SETTINGS_SECTIONS)[number]['value'];

let lastPushSwRegisterAttempts: Array<{ url: string; mode: string; error: string }> = [];
type FeedbackTone = 'success' | 'info' | 'warning' | 'error';
type InlineFeedback = {
  tone: FeedbackTone;
  message: string;
};
type RestartProgressState = {
  targetLabel: string;
  stepLabel: string;
  detail: string;
  cancelPending: boolean;
  cancellable: boolean;
};
type RestartReservationOverlayStep = {
  label: string;
  status: 'done' | 'active' | 'pending';
};
type RestartReservationOverlayState = {
  title: string;
  statusText: string;
  detail: string;
  steps: RestartReservationOverlayStep[];
};

type RestartCompletionAction = 'reload' | 'reset-client-state' | 'custom';
type RestartCompletionConfirmOptions = {
  title: string;
  targetLabel: string;
  action: RestartCompletionAction;
  okText?: string;
  cancelText?: string;
  promptText?: string;
  customAction?: () => Promise<void>;
  onCancel?: () => void;
  onActionError?: (message: string) => void;
};
const SERVER_RESTART_CACHE_BUST_PARAM = '__serverRestartedAt';
const RESERVED_RESTART_WORK_SERVER_DELAY_MS = 5_000;
const RESERVED_RESTART_VERIFICATION_CLOCK_SKEW_MS = 5_000;
const HEADER_THEME_STORAGE_KEY = 'work-server.header-theme';
const DESKTOP_HEADER_HEIGHT_STORAGE_KEY = 'work-server.desktop-header-height';
const MOBILE_HEADER_HEIGHT_STORAGE_KEY = 'work-server.mobile-header-height';
const DESKTOP_HEADER_HEIGHT_MIN = 48;
const DESKTOP_HEADER_HEIGHT_MAX = 88;
const DESKTOP_HEADER_HEIGHT_DEFAULT = 60;
const MOBILE_HEADER_HEIGHT_MIN = 32;
const MOBILE_HEADER_HEIGHT_MAX = 56;
const MOBILE_HEADER_HEIGHT_DEFAULT = 36;

type HeaderThemeKey = 'ocean' | 'sunset' | 'forest';

const HEADER_THEME_OPTIONS: Array<{
  key: HeaderThemeKey;
  label: string;
  description: string;
  icon: ReactNode;
}> = [
  { key: 'ocean', label: 'Ocean', description: '시원한 블루 중심 테마', icon: <BgColorsOutlined /> },
  { key: 'sunset', label: 'Sunset', description: '주황과 로즈 포인트 테마', icon: <RocketOutlined /> },
  { key: 'forest', label: 'Forest', description: '그린과 민트 중심 테마', icon: <ClusterOutlined /> },
];

function normalizeHeaderTheme(value: string | null | undefined): HeaderThemeKey {
  return HEADER_THEME_OPTIONS.some((option) => option.key === value) ? (value as HeaderThemeKey) : 'ocean';
}

function getStoredHeaderTheme() {
  if (typeof window === 'undefined') {
    return 'ocean' satisfies HeaderThemeKey;
  }

  try {
    return normalizeHeaderTheme(window.localStorage.getItem(HEADER_THEME_STORAGE_KEY));
  } catch {
    return 'ocean' satisfies HeaderThemeKey;
  }
}

function normalizeMobileHeaderHeight(value: number | string | null | undefined) {
  const parsedValue = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);

  if (!Number.isFinite(parsedValue)) {
    return MOBILE_HEADER_HEIGHT_DEFAULT;
  }

  return Math.min(MOBILE_HEADER_HEIGHT_MAX, Math.max(MOBILE_HEADER_HEIGHT_MIN, Math.round(parsedValue)));
}

function normalizeDesktopHeaderHeight(value: number | string | null | undefined) {
  const parsedValue = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);

  if (!Number.isFinite(parsedValue)) {
    return DESKTOP_HEADER_HEIGHT_DEFAULT;
  }

  return Math.min(DESKTOP_HEADER_HEIGHT_MAX, Math.max(DESKTOP_HEADER_HEIGHT_MIN, Math.round(parsedValue)));
}

function getStoredDesktopHeaderHeight() {
  if (typeof window === 'undefined') {
    return DESKTOP_HEADER_HEIGHT_DEFAULT;
  }

  try {
    return normalizeDesktopHeaderHeight(window.localStorage.getItem(DESKTOP_HEADER_HEIGHT_STORAGE_KEY));
  } catch {
    return DESKTOP_HEADER_HEIGHT_DEFAULT;
  }
}

function getStoredMobileHeaderHeight() {
  if (typeof window === 'undefined') {
    return MOBILE_HEADER_HEIGHT_DEFAULT;
  }

  try {
    return normalizeMobileHeaderHeight(window.localStorage.getItem(MOBILE_HEADER_HEIGHT_STORAGE_KEY));
  } catch {
    return MOBILE_HEADER_HEIGHT_DEFAULT;
  }
}

function areChatSettingsEqual(left: AppConfig['chat'], right: AppConfig['chat']) {
  return (
    left.maxContextMessages === right.maxContextMessages &&
    left.maxContextChars === right.maxContextChars &&
    left.codexLiveMaxExecutionSeconds === right.codexLiveMaxExecutionSeconds &&
    left.codexLiveIdleTimeoutSeconds === right.codexLiveIdleTimeoutSeconds &&
    left.receiveRoomNotifications === right.receiveRoomNotifications &&
    left.guidePwaInstallForNotifications === right.guidePwaInstallForNotifications &&
    left.guideWebPushPermission === right.guideWebPushPermission &&
    left.guideWebPushRegistration === right.guideWebPushRegistration &&
    left.restartReservationCompletionDelaySeconds === right.restartReservationCompletionDelaySeconds
  );
}

function getChatSettingsDiffLabels(saved: AppConfig['chat'], draft: AppConfig['chat']) {
  const changedLabels: string[] = [];

  if (saved.maxContextMessages !== draft.maxContextMessages) {
    changedLabels.push('최근 문맥 메시지 수');
  }

  if (saved.maxContextChars !== draft.maxContextChars) {
    changedLabels.push('최근 문맥 글자 수');
  }

  if (saved.codexLiveMaxExecutionSeconds !== draft.codexLiveMaxExecutionSeconds) {
    changedLabels.push('Codex Live 최대 실행 시간');
  }

  if (saved.codexLiveIdleTimeoutSeconds !== draft.codexLiveIdleTimeoutSeconds) {
    changedLabels.push('Codex Live 무출력 실패 시간');
  }

  if (saved.receiveRoomNotifications !== draft.receiveRoomNotifications) {
    changedLabels.push('채팅방 알림 수신');
  }

  if (saved.guidePwaInstallForNotifications !== draft.guidePwaInstallForNotifications) {
    changedLabels.push('PWA 설치 유도');
  }

  if (saved.guideWebPushPermission !== draft.guideWebPushPermission) {
    changedLabels.push('웹푸시 권한 유도');
  }

  if (saved.guideWebPushRegistration !== draft.guideWebPushRegistration) {
    changedLabels.push('웹푸시 등록 유도');
  }

  if (saved.restartReservationCompletionDelaySeconds !== draft.restartReservationCompletionDelaySeconds) {
    changedLabels.push('재기동 예약 자동 실행 대기 시간');
  }

  return changedLabels;
}

function areWorklogAutomationSettingsEqual(left: AppConfig['worklogAutomation'], right: AppConfig['worklogAutomation']) {
  return (
    left.autoCreateDailyWorklog === right.autoCreateDailyWorklog &&
    left.dailyCreateTime === right.dailyCreateTime &&
    left.includeScreenshots === right.includeScreenshots &&
    left.includeChangedFiles === right.includeChangedFiles &&
    left.includeCommandLogs === right.includeCommandLogs &&
    left.template === right.template
  );
}

function getWorklogAutomationDiffLabels(saved: AppConfig['worklogAutomation'], draft: AppConfig['worklogAutomation']) {
  const changedLabels: string[] = [];

  if (saved.autoCreateDailyWorklog !== draft.autoCreateDailyWorklog) {
    changedLabels.push('일일 작업일지 자동 생성');
  }

  if (saved.dailyCreateTime !== draft.dailyCreateTime) {
    changedLabels.push('자동 생성 시각');
  }

  if (saved.includeScreenshots !== draft.includeScreenshots) {
    changedLabels.push('스크린샷 포함');
  }

  if (saved.includeChangedFiles !== draft.includeChangedFiles) {
    changedLabels.push('변경 파일 목록 포함');
  }

  if (saved.includeCommandLogs !== draft.includeCommandLogs) {
    changedLabels.push('실행 커맨드 포함');
  }

  if (saved.template !== draft.template) {
    changedLabels.push('기본 템플릿');
  }

  return changedLabels;
}

function arePlanDefaultSettingsEqual(left: AppConfig['planDefaults'], right: AppConfig['planDefaults']) {
  return (
    left.jangsingProcessingRequired === right.jangsingProcessingRequired &&
    left.autoDeployToMain === right.autoDeployToMain &&
    left.openEditorAfterCreate === right.openEditorAfterCreate
  );
}

function getPlanDefaultDiffLabels(saved: AppConfig['planDefaults'], draft: AppConfig['planDefaults']) {
  const changedLabels: string[] = [];

  if (saved.jangsingProcessingRequired !== draft.jangsingProcessingRequired) {
    changedLabels.push('기능동작확인 기본값');
  }

  if (saved.autoDeployToMain !== draft.autoDeployToMain) {
    changedLabels.push('메인까지 자동등록');
  }

  if (saved.openEditorAfterCreate !== draft.openEditorAfterCreate) {
    changedLabels.push('등록 후 편집기 열기');
  }

  return changedLabels;
}

function arePlanCostSettingsEqual(left: AppConfig['planCost'], right: AppConfig['planCost']) {
  return (
    left.baseCostPerMillionTokens === right.baseCostPerMillionTokens &&
    left.retryCostMultiplierPercent === right.retryCostMultiplierPercent &&
    left.hourlyCostMultiplierPercent === right.hourlyCostMultiplierPercent &&
    left.timeCostUnit === right.timeCostUnit &&
    left.attentionCostThresholdMultiplier === right.attentionCostThresholdMultiplier &&
    left.warningCostThresholdMultiplier === right.warningCostThresholdMultiplier &&
    left.highCostThresholdMultiplier === right.highCostThresholdMultiplier
  );
}

function getPlanCostDiffLabels(saved: AppConfig['planCost'], draft: AppConfig['planCost']) {
  const changedLabels: string[] = [];

  if (saved.baseCostPerMillionTokens !== draft.baseCostPerMillionTokens) {
    changedLabels.push('백만 토큰당 기준 비용');
  }

  if (saved.retryCostMultiplierPercent !== draft.retryCostMultiplierPercent) {
    changedLabels.push('재처리 가산 비율');
  }

  if (saved.hourlyCostMultiplierPercent !== draft.hourlyCostMultiplierPercent) {
    changedLabels.push('시간 가산 비율');
  }

  if (saved.timeCostUnit !== draft.timeCostUnit) {
    changedLabels.push('시간 가산 기준 단위');
  }

  if (saved.attentionCostThresholdMultiplier !== draft.attentionCostThresholdMultiplier) {
    changedLabels.push('관심 구간 기준');
  }

  if (saved.warningCostThresholdMultiplier !== draft.warningCostThresholdMultiplier) {
    changedLabels.push('주의 구간 기준');
  }

  if (saved.highCostThresholdMultiplier !== draft.highCostThresholdMultiplier) {
    changedLabels.push('높음 구간 기준');
  }

  return changedLabels;
}

function formatCostAmount(value: number) {
  return `${new Intl.NumberFormat('ko-KR').format(value)}원`;
}

function formatPlanCostTimeMultiplierLabel(planCost: AppConfig['planCost']) {
  return `${PLAN_COST_TIME_UNIT_LABELS[planCost.timeCostUnit]}당 ${planCost.hourlyCostMultiplierPercent}%`;
}

function areAutomationRuntimeSettingsEqual(left: AppConfig['automation'], right: AppConfig['automation']) {
  return (
    left.autoRefreshEnabled === right.autoRefreshEnabled &&
    left.autoRefreshIntervalSeconds === right.autoRefreshIntervalSeconds &&
    left.autoReceiveScheduleType === right.autoReceiveScheduleType &&
    left.autoReceiveIntervalSeconds === right.autoReceiveIntervalSeconds &&
    left.autoReceiveDailyTime === right.autoReceiveDailyTime &&
    left.autoReceiveWeeklyDay === right.autoReceiveWeeklyDay &&
    left.autoReceiveWeeklyTime === right.autoReceiveWeeklyTime
  );
}

function getAutomationRuntimeDiffLabels(left: AppConfig['automation'], right: AppConfig['automation']) {
  const changedLabels: string[] = [];

  if (left.autoRefreshEnabled !== right.autoRefreshEnabled) {
    changedLabels.push('자동화 워커 실행');
  }

  if (left.autoRefreshIntervalSeconds !== right.autoRefreshIntervalSeconds) {
    changedLabels.push('워커 새로고침 주기');
  }

  if (left.autoReceiveScheduleType !== right.autoReceiveScheduleType) {
    changedLabels.push('자동접수 스케줄 방식');
  }

  if (left.autoReceiveIntervalSeconds !== right.autoReceiveIntervalSeconds) {
    changedLabels.push('자동접수 간격');
  }

  if (left.autoReceiveDailyTime !== right.autoReceiveDailyTime) {
    changedLabels.push('매일 자동접수 시각');
  }

  if (left.autoReceiveWeeklyDay !== right.autoReceiveWeeklyDay) {
    changedLabels.push('주간 자동접수 요일');
  }

  if (left.autoReceiveWeeklyTime !== right.autoReceiveWeeklyTime) {
    changedLabels.push('주간 자동접수 시각');
  }

  return changedLabels;
}

function formatAutomationRuntimeSchedule(automation: AppConfig['automation']) {
  if (automation.autoReceiveScheduleType === 'daily') {
    return `매일 ${automation.autoReceiveDailyTime}`;
  }

  if (automation.autoReceiveScheduleType === 'weekly') {
    const weeklyDayLabels: Record<AppConfig['automation']['autoReceiveWeeklyDay'], string> = {
      mon: '월요일',
      tue: '화요일',
      wed: '수요일',
      thu: '목요일',
      fri: '금요일',
      sat: '토요일',
      sun: '일요일',
    };

    return `${weeklyDayLabels[automation.autoReceiveWeeklyDay]} ${automation.autoReceiveWeeklyTime}`;
  }

  return `${automation.autoReceiveIntervalSeconds}초 간격`;
}

function getPlanCostThresholdPreview(planCost: AppConfig['planCost']) {
  const attentionThreshold = planCost.baseCostPerMillionTokens * Math.max(0.1, planCost.attentionCostThresholdMultiplier);
  const warningThreshold =
    planCost.baseCostPerMillionTokens *
    Math.max(planCost.attentionCostThresholdMultiplier, planCost.warningCostThresholdMultiplier);
  const highThreshold =
    planCost.baseCostPerMillionTokens * Math.max(planCost.warningCostThresholdMultiplier, planCost.highCostThresholdMultiplier);

  return `안정 ${formatCostAmount(attentionThreshold)} 이하 · 관심 ${formatCostAmount(
    warningThreshold,
  )} 이하 · 주의 ${formatCostAmount(highThreshold)} 이하 · 높음 ${formatCostAmount(highThreshold)} 초과`;
}

function getAppSettingsSectionCategory(section: AppSettingsSectionKey): AppSettingsCategoryKey {
  return APP_SETTINGS_SECTIONS.find((entry) => entry.value === section)?.category ?? 'automation';
}

function getAppSettingsSectionOptions(category: AppSettingsCategoryKey) {
  return APP_SETTINGS_SECTIONS.filter((entry) => entry.category === category).map((entry) => ({
    label: entry.label,
    value: entry.value,
  }));
}

function formatRuntimeTimestamp(value: string | null | undefined) {
  if (!value) {
    return '-';
  }

  const date = new Date(value);

  if (Number.isNaN(date.getTime())) {
    return value;
  }

  return date.toLocaleTimeString('ko-KR', {
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false,
  });
}

function formatRuntimeRelativeLabel(value: string | null | undefined) {
  if (!value) {
    return '-';
  }

  const date = new Date(value);

  if (Number.isNaN(date.getTime())) {
    return formatRuntimeTimestamp(value);
  }

  const diffMs = Date.now() - date.getTime();

  if (diffMs <= 15_000) {
    return '방금';
  }

  const diffSeconds = Math.floor(diffMs / 1000);

  if (diffSeconds < 60) {
    return `${diffSeconds}초 전`;
  }

  const diffMinutes = Math.floor(diffSeconds / 60);

  if (diffMinutes < 60) {
    return `${diffMinutes}분 전`;
  }

  const diffHours = Math.floor(diffMinutes / 60);

  if (diffHours < 24) {
    return `${diffHours}시간 전`;
  }

  const diffDays = Math.floor(diffHours / 24);
  return `${diffDays}일 전`;
}

function buildRuntimeTerminalLabel(terminalStatus: ChatRuntimeJobDetail['terminalStatus']) {
  if (!terminalStatus) {
    return '-';
  }

  if (terminalStatus === 'cancelled') {
    return '취소됨';
  }

  if (terminalStatus === 'removed') {
    return '대기열 제거됨';
  }

  if (terminalStatus === 'failed') {
    return '실패';
  }

  return '완료';
}

const AUTOMATION_NOTIFICATION_OPTIONS: Array<{
  key: keyof Pick<
    AppConfig['automation'],
    | 'notifyOnAutomationStart'
    | 'notifyOnAutomationProgress'
    | 'notifyOnAutomationCompletion'
    | 'notifyOnAutomationRelease'
    | 'notifyOnAutomationMain'
    | 'notifyOnAutomationFailure'
    | 'notifyOnAutomationRestart'
    | 'notifyOnAutomationIssueResolved'
  >;
  label: string;
  description: string;
}> = [
  {
    key: 'notifyOnAutomationStart',
    label: '자동화 시작',
    description: '작업 자동화가 시작될 때 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationProgress',
    label: '자동화 진행',
    description: '자동 작업이 오래 걸릴 때 진행 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationCompletion',
    label: '자동 작업 완료',
    description: '작업완료, 변경 없음 완료, 수동 완료 처리 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationRelease',
    label: 'release 반영 완료',
    description: 'release 반영 완료 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationMain',
    label: 'main 반영 완료',
    description: 'main 반영과 메인 프로젝트 동기화 완료 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationFailure',
    label: '자동화 실패',
    description: '브랜치, 자동 작업, release, main 반영 실패 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationRestart',
    label: '작업 재시작',
    description: '재시도나 이슈 조치로 작업이 다시 큐에 들어갈 때 알림을 받습니다.',
  },
  {
    key: 'notifyOnAutomationIssueResolved',
    label: '이슈 해결 처리',
    description: '최신 이슈가 해결 처리될 때 알림을 받습니다.',
  },
];

function areAutomationNotificationSettingsEqual(left: AppConfig['automation'], right: AppConfig['automation']) {
  return AUTOMATION_NOTIFICATION_OPTIONS.every((option) => left[option.key] === right[option.key]);
}

function getAutomationNotificationDiffLabels(saved: AppConfig['automation'], draft: AppConfig['automation']) {
  return AUTOMATION_NOTIFICATION_OPTIONS.filter((option) => saved[option.key] !== draft[option.key]).map(
    (option) => option.label,
  );
}

function areGestureShortcutSettingsEqual(
  left: AppConfig['gestureShortcuts'],
  right: AppConfig['gestureShortcuts'],
) {
  return left.openSearch === right.openSearch;
}

function getGestureShortcutDiffLabels(saved: AppConfig['gestureShortcuts'], draft: AppConfig['gestureShortcuts']) {
  const changedLabels: string[] = [];

  if (saved.openSearch !== draft.openSearch) {
    changedLabels.push('통합 검색 열기');
  }

  return changedLabels;
}

function isSettingsModalAllowed(modal: SettingsModalKey, hasAccess: boolean) {
  if (hasAccess) {
    return true;
  }

  return modal === 'token';
}

function getSettingsModalTitle(modal: SettingsModalKey) {
  switch (modal) {
    case 'notification':
      return '알림';
    case 'token':
      return '권한';
    case 'update':
      return '업데이트';
    default:
      return '앱 설정';
  }
}

function formatDateTimeLabel(value: string | null) {
  if (!value) {
    return '-';
  }

  const parsed = new Date(value);

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

  return new Intl.DateTimeFormat('ko-KR', {
    timeZone: 'Asia/Seoul',
    dateStyle: 'medium',
    timeStyle: 'short',
  }).format(parsed);
}

function getKstDateKey(date: Date) {
  return new Intl.DateTimeFormat('en-CA', {
    timeZone: 'Asia/Seoul',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(date);
}

function formatTimeLabel(value: string | null) {
  if (!value) {
    return '-';
  }

  const parsed = new Date(value);

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

  return new Intl.DateTimeFormat('ko-KR', {
    timeZone: 'Asia/Seoul',
    hour: '2-digit',
    minute: '2-digit',
    hour12: false,
  }).format(parsed);
}

function formatMonthDayLabel(value: string | null) {
  if (!value) {
    return '-';
  }

  const parsed = new Date(value);

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

  return new Intl.DateTimeFormat('ko-KR', {
    timeZone: 'Asia/Seoul',
    month: 'numeric',
    day: 'numeric',
  }).format(parsed);
}

function formatCompactHistoryLabel(value: string | null, nowTimestamp: number) {
  if (!value) {
    return null;
  }

  const parsed = new Date(value);

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

  return getKstDateKey(parsed) === getKstDateKey(new Date(nowTimestamp))
    ? formatTimeLabel(value)
    : formatMonthDayLabel(value);
}

function formatRelativeSecondsLabel(targetAt: string | null, nowTimestamp: number) {
  if (!targetAt) {
    return null;
  }

  const targetTimestamp = Date.parse(targetAt);

  if (!Number.isFinite(targetTimestamp)) {
    return null;
  }

  const diffMs = targetTimestamp - nowTimestamp;

  if (diffMs <= 0) {
    return '곧 확인';
  }

  const diffSeconds = Math.max(1, Math.ceil(diffMs / 1000));
  return `${diffSeconds}초 후`;
}

function formatRemainingSecondsLabel(targetTimestamp: number | null, nowTimestamp: number) {
  if (!targetTimestamp || !Number.isFinite(targetTimestamp)) {
    return null;
  }

  const diffMs = targetTimestamp - nowTimestamp;

  if (diffMs <= 0) {
    return '곧';
  }

  return `${Math.max(1, Math.ceil(diffMs / 1000))}초`;
}

function buildCacheBustedReloadUrl() {
  const nextUrl = new URL(window.location.href);
  nextUrl.searchParams.set(SERVER_RESTART_CACHE_BUST_PARAM, `${Date.now()}`);
  return nextUrl.toString();
}

function getServerRestartReservationTimingLabel(
  reservation: ServerRestartReservation | null,
  nowTimestamp: number,
) {
  if (!reservation) {
    return null;
  }

  if (reservation.status === 'waiting' && reservation.nextCheckAt) {
    const relativeLabel = formatRelativeSecondsLabel(reservation.nextCheckAt, nowTimestamp);
    return relativeLabel ? `확인 ${relativeLabel}` : '확인 대기';
  }

  if (reservation.status === 'ready') {
    const relativeLabel = formatRelativeSecondsLabel(reservation.autoExecuteAt, nowTimestamp);
    return relativeLabel ? `자동 실행 ${relativeLabel}` : '자동 실행 준비';
  }

  if (reservation.status === 'executing' && reservation.startedAt) {
    const workServerRestartAt = Date.parse(reservation.startedAt) + RESERVED_RESTART_WORK_SERVER_DELAY_MS;
    const remainingLabel = formatRemainingSecondsLabel(workServerRestartAt, nowTimestamp);
    return remainingLabel ? `${remainingLabel} 남음` : '곧 완료';
  }

  if (reservation.status === 'completed' && reservation.completedAt) {
    const completedLabel = formatCompactHistoryLabel(reservation.completedAt, nowTimestamp);
    return completedLabel ? `완료 ${completedLabel}` : '완료';
  }

  if (reservation.status === 'cancelled' && reservation.cancelledAt) {
    const cancelledLabel = formatCompactHistoryLabel(reservation.cancelledAt, nowTimestamp);
    return cancelledLabel ? `취소 ${cancelledLabel}` : '취소';
  }

  if (reservation.requestedAt) {
    const requestedLabel = formatCompactHistoryLabel(reservation.requestedAt, nowTimestamp);
    return requestedLabel ? `예약 ${requestedLabel}` : '예약';
  }

  return null;
}

function getServerRestartReservationPendingSummary(
  reservation: ServerRestartReservation | null,
  nowTimestamp: number,
) {
  if (!reservation) {
    return null;
  }

  const codexPending =
    (reservation.workloadSummary.codexRunningCount ?? 0) + (reservation.workloadSummary.codexQueuedCount ?? 0);
  const automationPending =
    (reservation.workloadSummary.automationRunningCount ?? 0) + (reservation.workloadSummary.automationQueuedCount ?? 0);
  const totalPending = codexPending + automationPending;

  if (reservation.status === 'waiting') {
    const relativeLabel = formatRelativeSecondsLabel(reservation.nextCheckAt, nowTimestamp);

    if (totalPending > 0) {
      return `작업 ${totalPending}건 · 완료 뒤 10초`;
    }

    return relativeLabel ? `재확인 ${relativeLabel}` : '재기동 대기';
  }

  if (reservation.status === 'ready') {
    const relativeLabel = formatRelativeSecondsLabel(reservation.autoExecuteAt, nowTimestamp);
    return relativeLabel
      ? `${relativeLabel} 뒤 자동 실행`
      : `${reservation.autoExecuteDelaySeconds}초 뒤 자동 실행`;
  }

  if (reservation.status === 'executing') {
    return reservation.activeClientCount > 0 ? `알림 ${reservation.activeClientCount}건 전송` : null;
  }

  return null;
}

function getServerRestartReservationOverlayState(
  reservation: ServerRestartReservation | null,
  nowTimestamp: number,
  reloadPending: boolean,
): RestartReservationOverlayState | null {
  if (!reservation) {
    return null;
  }

  if (reservation.status === 'executing') {
    if (reservation.executionPhase === 'commit-main-worktree') {
      return {
        title: '재기동 실행 준비 중',
        statusText: 'main 작업트리 커밋 단계',
        detail: reservation.waitingReason?.trim() || 'main 작업트리 커밋 단계를 확인한 뒤 서버 재기동을 이어갑니다.',
        steps: [
          { label: 'main 작업트리 커밋', status: 'active' },
          { label: 'TEST 재기동', status: 'pending' },
          { label: 'WORK 재기동', status: 'pending' },
          { label: '새로고침', status: 'pending' },
        ],
      };
    }

    if (reservation.target === 'work-server') {
      return {
        title: 'WORK 서버 재기동 중',
        statusText: '새 런타임 반영 중',
        detail: reservation.waitingReason?.trim() || 'WORK 서버를 재기동하고 새 런타임을 확인하는 중입니다.',
        steps: [
          { label: 'main 작업트리 커밋', status: 'done' },
          { label: 'WORK 재기동', status: 'active' },
          { label: '정상 기동 확인', status: 'pending' },
        ],
      };
    }

    if (reservation.target === 'test') {
      return {
        title: 'TEST 서버 재기동 중',
        statusText: '새 런타임 반영 중',
        detail: reservation.waitingReason?.trim() || 'TEST 서버를 재기동하고 새 런타임을 확인하는 중입니다.',
        steps: [
          { label: 'main 작업트리 커밋', status: 'done' },
          { label: 'TEST 재기동', status: 'active' },
          { label: '정상 기동 확인', status: 'pending' },
        ],
      };
    }

    const startedTimestamp = reservation.startedAt ? Date.parse(reservation.startedAt) : Number.NaN;
    const workServerRestartAt = Number.isFinite(startedTimestamp)
      ? startedTimestamp + RESERVED_RESTART_WORK_SERVER_DELAY_MS
      : null;
    const beforeWorkServerRestart =
      workServerRestartAt !== null && Number.isFinite(workServerRestartAt) ? nowTimestamp < workServerRestartAt : false;
    const remainingLabel = formatRemainingSecondsLabel(workServerRestartAt, nowTimestamp);
    const statusText = beforeWorkServerRestart && remainingLabel ? `${remainingLabel} 뒤 다음 단계` : '곧 마무리';
    const detail = beforeWorkServerRestart
      ? 'TEST 서버를 먼저 재기동하고, 이어서 WORK 서버 재기동을 준비합니다.'
      : 'WORK 서버 재기동 뒤 정상 기동을 확인하는 중입니다.';

    return {
      title: beforeWorkServerRestart ? 'TEST 서버 재기동 중' : 'WORK 서버 재기동 준비 중',
      statusText,
      detail,
      steps: [
        { label: 'main 작업트리 커밋', status: 'done' },
        { label: 'TEST 재기동', status: beforeWorkServerRestart ? 'active' : 'done' },
        { label: 'WORK 재기동', status: beforeWorkServerRestart ? 'pending' : 'active' },
        { label: '새로고침', status: 'pending' },
      ],
    };
  }

  if (reservation.status === 'ready') {
    return {
      title: '재기동 예약 자동 실행 대기',
      statusText: '자동 재기동 대기 중',
      detail: '진행 중인 작업이 모두 끝났습니다. 설정된 대기 시간이 지나면 TEST 서버부터 순서대로 재기동합니다.',
      steps: [
        { label: '자동 실행 대기', status: 'active' },
        { label: 'main 작업트리 커밋', status: 'pending' },
        { label: 'TEST 재기동', status: 'pending' },
        { label: 'WORK 재기동', status: 'pending' },
        { label: '새로고침', status: 'pending' },
      ],
    };
  }

  if (reservation.status === 'recovering') {
    const targetLabel = reservation.autoFix.targetKey ? `${reservation.autoFix.targetKey.toUpperCase()} 자동 개선` : 'Codex 자동 개선';
    const statusText =
      reservation.autoFix.status === 'failed'
        ? '자동 개선 실패'
        : reservation.autoFix.status === 'completed'
          ? '자동 개선 완료'
          : '빌드 오류 분석 및 수정 중';
    const detail =
      reservation.autoFix.detail?.trim()
      || reservation.autoFix.summary?.trim()
      || reservation.waitingReason?.trim()
      || '재기동을 막는 빌드 오류를 Codex가 자동으로 수정한 뒤 재기동을 다시 시도합니다.';

    return {
      title: targetLabel,
      statusText,
      detail,
      steps: [
        { label: reservation.target === 'all' ? 'main 작업트리 커밋' : '재기동 요청', status: 'done' },
        { label: '빌드 실패 감지', status: 'done' },
        { label: 'Codex 자동 개선', status: reservation.autoFix.status === 'completed' ? 'done' : 'active' },
        { label: '재기동 재시도', status: reservation.autoFix.status === 'completed' ? 'active' : 'pending' },
      ],
    };
  }

  if (reservation.status === 'completed' && reloadPending) {
    return {
      title: '재기동 완료 처리 중',
      statusText: '정상 기동 최종 확인',
      detail: 'TEST/WORK 서버 새 런타임을 다시 확인한 뒤 브라우저 상태를 정리하고 최신 화면으로 연결합니다.',
      steps: [
        { label: 'main 작업트리 커밋', status: 'done' },
        { label: 'TEST 재기동', status: 'done' },
        { label: 'WORK 재기동', status: 'done' },
        { label: '새로고침', status: 'active' },
      ],
    };
  }

  return null;
}

function getServerVersionStatusClassName(item: ServerCommandItem | null) {
  if (!item) {
    return 'app-header__server-version-indicator--unknown';
  }

  if (item.buildRequired) {
    return 'app-header__server-version-indicator--build-required';
  }

  if (item.updateAvailable) {
    return 'app-header__server-version-indicator--update-available';
  }

  return 'app-header__server-version-indicator--latest';
}

function getServerLastSourceChangedDateLabel(item: ServerCommandItem | null) {
  return formatDateTimeLabel(item?.latestSourceChangeAt ?? null);
}

function getServerLastSourceChangedHint(item: ServerCommandItem | null) {
  if (!item) {
    return null;
  }

  if (item.latestSourceChangePath?.trim()) {
    return `최근 경로: ${item.latestSourceChangePath.trim()}`;
  }

  if (!item.latestSourceChangeAt && item.updateSummary?.trim()) {
    return item.updateSummary.trim();
  }

  return null;
}

function getServerVersionStatusTitle(item: ServerCommandItem | null, label: string) {
  if (!item) {
    return `${label} 최신 버전 확인 전`;
  }

  if (item.buildRequired) {
    return `${label} 커밋 미반영 상태`;
  }

  if (item.updateAvailable) {
    return `${label} 운영 반영 대기 상태`;
  }

  return `${label} 최신 버전`;
}

function getServerRestartFollowupMessage(item: ServerCommandItem, label: string) {
  if (item.buildRequired) {
    return `${label}는 다시 떠 있어도 빨간 점이 유지될 수 있습니다. 현재 소스가 최신 빌드보다 새로워 재기동만으로는 해소되지 않습니다.`;
  }

  if (item.updateAvailable) {
    return `${label}는 다시 떠 있어도 반영 대기 상태라 점 상태가 바로 바뀌지 않을 수 있습니다.`;
  }

  return null;
}

function hasServerRuntimeChanged(previous: ServerCommandItem | null, next: ServerCommandItem | null) {
  if (!previous || !next) {
    return Boolean(next);
  }

  return (
    previous.startedAt !== next.startedAt ||
    previous.runningBuiltAt !== next.runningBuiltAt ||
    previous.runningVersion !== next.runningVersion ||
    previous.composeDetails !== next.composeDetails
  );
}

function getWorkServerRuntimeMarker(item: ServerCommandItem | null) {
  if (!item) {
    return '';
  }

  const runningVersion = item.runningVersion?.trim();
  if (runningVersion) {
    return runningVersion;
  }

  const runningBuiltAt = item.runningBuiltAt?.trim();
  if (runningBuiltAt) {
    return runningBuiltAt;
  }

  return item.startedAt?.trim() || '';
}

function hasServerUpdateStateImproved(previous: ServerCommandItem | null, next: ServerCommandItem | null) {
  if (!previous || !next) {
    return next ? !next.buildRequired && !next.updateAvailable : false;
  }

  const previousNeedsUpdate = previous.buildRequired || previous.updateAvailable;
  const nextNeedsUpdate = next.buildRequired || next.updateAvailable;

  return previousNeedsUpdate && !nextNeedsUpdate;
}

function hasServerRestartBeenVerified(
  key: 'test' | 'prod' | 'work-server',
  previous: ServerCommandItem | null,
  next: ServerCommandItem | null,
) {
  if (!next || next.availability !== 'online') {
    return false;
  }

  if (hasServerRuntimeChanged(previous, next) || hasServerUpdateStateImproved(previous, next)) {
    return true;
  }

  if (key === 'test') {
    return Boolean(next.runningBuiltAt) && !next.buildRequired;
  }

  if (key === 'prod') {
    return Boolean(next.runningBuiltAt) && !next.updateAvailable;
  }

  if (key === 'work-server') {
    if (next.buildRequired || next.updateAvailable) {
      return false;
    }

    const previousRuntimeMarker = getWorkServerRuntimeMarker(previous);
    const nextRuntimeMarker = getWorkServerRuntimeMarker(next);

    if (!nextRuntimeMarker) {
      return false;
    }

    return previousRuntimeMarker !== nextRuntimeMarker || hasServerUpdateStateImproved(previous, next);
  }

  return false;
}

function hasReservedRestartStartedAfterReservation(
  serverStartedAt: string | null | undefined,
  reservationStartedAt: string | null | undefined,
) {
  const serverStartedTime = Date.parse(serverStartedAt ?? '');
  const reservationStartedTime = Date.parse(reservationStartedAt ?? '');

  if (!Number.isFinite(serverStartedTime) || !Number.isFinite(reservationStartedTime)) {
    return false;
  }

  return serverStartedTime + RESERVED_RESTART_VERIFICATION_CLOCK_SKEW_MS >= reservationStartedTime;
}

function hasReservedRestartServerBeenVerified(
  key: 'test' | 'work-server',
  item: ServerCommandItem | null,
  reservationStartedAt: string | null | undefined,
) {
  if (
    !item ||
    item.availability !== 'online' ||
    !hasReservedRestartStartedAfterReservation(item.startedAt, reservationStartedAt)
  ) {
    return false;
  }

  if (key === 'test') {
    return Boolean(item.runningBuiltAt) && !item.buildRequired;
  }

  return Boolean(getWorkServerRuntimeMarker(item)) && !item.buildRequired && !item.updateAvailable;
}

function shouldResetClientStateAfterRestart(
  key: 'test' | 'prod' | 'work-server',
  previous: ServerCommandItem | null,
  next: ServerCommandItem | null,
) {
  if (key !== 'test' || !next || next.availability !== 'online' || next.buildRequired) {
    return false;
  }

  return (
    hasServerUpdateStateImproved(previous, next) ||
    previous?.runningBuiltAt !== next.runningBuiltAt ||
    previous?.latestBuiltAt !== next.latestBuiltAt
  );
}

function getRestartCompletionActionLabel(action: RestartCompletionAction) {
  if (action === 'reset-client-state') {
    return '캐시 초기화 후 새로고침';
  }

  if (action === 'custom') {
    return '재기동';
  }

  return '새로고침';
}

function getServerRestartTargetLabel(key: 'test' | 'prod' | 'work-server' | 'all') {
  switch (key) {
    case 'test':
      return 'TEST 서버';
    case 'prod':
      return 'PROD 컨테이너';
    case 'work-server':
      return 'WORK 서버';
    default:
      return 'TEST 서버와 WORK 서버';
  }
}

function getServerAvailabilityStatusLabel(item: ServerCommandItem | null) {
  if (!item) {
    return '상태 응답 대기 중';
  }

  if (item.availability === 'online') {
    return '정상 응답 확인';
  }

  if (item.availability === 'degraded') {
    return '부분 응답 상태';
  }

  return '오프라인 상태';
}

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 getClientNotificationPermissionLabel(permission: ClientNotificationPermissionState) {
  switch (permission) {
    case 'granted':
      return '허용됨';
    case 'denied':
      return '차단됨';
    case 'unsupported':
      return '미지원';
    default:
      return '미확인';
  }
}

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

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

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

function getAppHeaderDomainClassName() {
  if (typeof window === 'undefined') {
    return 'app-header--prod';
  }

  const hostname = window.location.hostname.toLowerCase();

  if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0') {
    return 'app-header--local';
  }

  if (hostname.startsWith('test.')) {
    return 'app-header--test';
  }

  if (hostname.startsWith('rel.')) {
    return 'app-header--rel';
  }

  if (hostname.startsWith('preview.')) {
    return 'app-header--preview';
  }

  return 'app-header--prod';
}

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

  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;
  const timeoutMs = 30000;
  const startedAt = Date.now();
  let registeredRegistration: ServiceWorkerRegistration | null = null;

  const waitForServiceWorkerReady = async (remainingTimeoutMs: number) => {
    const readyStartedAt = Date.now();

    while (Date.now() - readyStartedAt < remainingTimeoutMs) {
      try {
        const readyRegistration = await Promise.race([
          navigator.serviceWorker.ready,
          new Promise<null>((resolve) => {
            window.setTimeout(() => resolve(null), 1000);
          }),
        ]);

        if (readyRegistration) {
          return readyRegistration;
        }
      } catch {
        // keep polling until timeout
      }

      await new Promise((resolve) => {
        window.setTimeout(resolve, 300);
      });
    }

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

    const remainingTimeoutMs = Math.max(1500, timeoutMs - (Date.now() - startedAt));

    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, remainingTimeoutMs);
      installingWorker.addEventListener('statechange', onStateChange);
      onStateChange();
    });

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

    const readyRegistration = await waitForServiceWorkerReady(remainingTimeoutMs);
    return readyRegistration ?? registration;
  };

  const tryRegister = async (url: string, mode: string, options?: RegistrationOptions) => {
    try {
      registeredRegistration = await navigator.serviceWorker.register(url, options);
      return true;
    } catch (error) {
      lastPushSwRegisterAttempts = [
        ...lastPushSwRegisterAttempts,
        {
          url,
          mode,
          error: error instanceof Error ? error.message : 'unknown error',
        },
      ];
      return false;
    }
  };

  const devOptionsModule = { type: 'module', scope: '/', updateViaCache: 'none' } as const;
  const devOptionsClassic = { scope: '/', updateViaCache: 'none' } as const;

  if (import.meta.env.DEV) {
    lastPushSwRegisterAttempts = [];
    const registered =
      (await tryRegister(resolvedServiceWorkerUrl, 'module', devOptionsModule)) ||
      (await tryRegister(resolvedServiceWorkerUrl, 'classic', devOptionsClassic));

    if (!registered) {
      // registerSW helper in main.tsx may already be handling this path; if manual registration fails,
      // continue and try to resolve an existing active registration below.
    }
  } else {
    await tryRegister(resolvedServiceWorkerUrl, 'default');
  }

  const usableRegisteredRegistration = await resolveUsableRegistration(registeredRegistration);

  if (usableRegisteredRegistration) {
    return usableRegisteredRegistration;
  }

  while (Date.now() - startedAt < timeoutMs) {
    const existingRegistration = await navigator.serviceWorker.getRegistration();
    const usableExistingRegistration = await resolveUsableRegistration(existingRegistration);

    if (usableExistingRegistration) {
      return usableExistingRegistration;
    }

    const scopedRegistration = await navigator.serviceWorker.getRegistration('/');
    const usableScopedRegistration = await resolveUsableRegistration(scopedRegistration);

    if (usableScopedRegistration) {
      return usableScopedRegistration;
    }

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

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

      if (usableRegistration) {
        return usableRegistration;
      }
    }

    try {
      const remainingTimeoutMs = Math.max(1500, timeoutMs - (Date.now() - startedAt));
      const readyRegistration = await waitForServiceWorkerReady(remainingTimeoutMs);
      const usableReadyRegistration = await resolveUsableRegistration(readyRegistration);

      if (usableReadyRegistration) {
        return usableReadyRegistration;
      }
    } catch {
      // keep polling until timeout
    }

    await new Promise((resolve) => {
      window.setTimeout(resolve, 250);
    });
  }

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

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

    if (usableRegistration) {
      return usableRegistration;
    }
  }

  if (import.meta.env.DEV) {
    try {
      await Promise.all(registrations.map((registration) => registration.unregister()));
      await waitForDuration(300);

      await tryRegister(resolvedServiceWorkerUrl, 'module', devOptionsModule);
      if (!registeredRegistration) {
        await tryRegister(resolvedServiceWorkerUrl, 'classic', devOptionsClassic);
      }

      const remainingTimeoutMs = Math.max(1500, timeoutMs - (Date.now() - startedAt));
      const readyRegistration = await waitForServiceWorkerReady(remainingTimeoutMs);
      const usableReadyRegistration = await resolveUsableRegistration(readyRegistration ?? registeredRegistration);

      if (usableReadyRegistration) {
        return usableReadyRegistration;
      }
    } catch {
      // ignore and fallthrough to null
    }
  }

  return null;
}

function waitForDuration(durationMs: number) {
  return new Promise<void>((resolve) => {
    window.setTimeout(resolve, durationMs);
  });
}

export function MainHeader({
  activeTopMenu,
  sidebarCollapsed,
  contentExpanded,
  isMobileViewport,
  onToggleSidebar,
  onToggleContentExpanded,
  onChangeTopMenu,
  onOpenSearch,
  onOpenPlanQuickFilter,
}: MainHeaderProps) {
  void contentExpanded;
  void onToggleContentExpanded;
  const appHeaderDomainClassName = getAppHeaderDomainClassName();
  const screens = useBreakpoint();
  const [modalApi, modalContextHolder] = Modal.useModal();
  const navigate = useNavigate();
  const location = useLocation();
  const [headerTheme, setHeaderTheme] = useState<HeaderThemeKey>(() => getStoredHeaderTheme());
  const [desktopHeaderHeight, setDesktopHeaderHeight] = useState<number>(() => getStoredDesktopHeaderHeight());
  const [mobileHeaderHeight, setMobileHeaderHeight] = useState<number>(() => getStoredMobileHeaderHeight());
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [automationGroupExpanded, setAutomationGroupExpanded] = useState(false);
  const [settingsModalOpen, setSettingsModalOpen] = useState(false);
  const [activeSettingsModal, setActiveSettingsModal] = useState<SettingsModalKey>('appSettings');
  const [activeAppSettingsCategory, setActiveAppSettingsCategory] = useState<AppSettingsCategoryKey>('automation');
  const [activeAppSettingsSection, setActiveAppSettingsSection] = useState<AppSettingsSectionKey>('planDefaults');
  const [notificationEnabled, setNotificationEnabled] = useState(false);
  const [notificationPendingRegistration, setNotificationPendingRegistration] = useState(false);
  const [notificationLoading, setNotificationLoading] = useState(false);
  const [notificationFeedback, setNotificationFeedback] = useState<InlineFeedback | null>(null);
  const [notificationCopyFeedback, setNotificationCopyFeedback] = useState<InlineFeedback | null>(null);
  const [clientNotificationPermission, setClientNotificationPermission] = useState<ClientNotificationPermissionState>(
    () => getClientNotificationPermission(),
  );
  const [webPushConfigured, setWebPushConfigured] = useState(false);
  const [isStandaloneMode, setIsStandaloneMode] = useState(false);
  const [chatConnection, setChatConnection] = useState(() => chatConnectionGateway.getSnapshot());
  const [chatRuntimeSnapshot, setChatRuntimeSnapshot] = useState<ChatRuntimeSnapshot | null>(() =>
    chatConnectionGateway.getSharedRuntimeSnapshot(),
  );
  const [activeConversationSnapshot, setActiveConversationSnapshot] = useState(() =>
    getSharedActiveConversationSnapshot(),
  );
  const [isRuntimeModalOpen, setIsRuntimeModalOpen] = useState(false);
  const [runtimeConversationTitles, setRuntimeConversationTitles] = useState<Record<string, string>>({});
  const [isRuntimeLogDrawerOpen, setIsRuntimeLogDrawerOpen] = useState(false);
  const [runtimeLogLoading, setRuntimeLogLoading] = useState(false);
  const [runtimeLogError, setRuntimeLogError] = useState('');
  const [runtimeLogDetail, setRuntimeLogDetail] = useState<ChatRuntimeJobDetail | null>(null);
  const [updateCheckFeedback, setUpdateCheckFeedback] = useState<InlineFeedback | null>(null);
  const [updateCheckCopyFeedback, setUpdateCheckCopyFeedback] = useState<InlineFeedback | null>(null);
  const [testServerStatus, setTestServerStatus] = useState<ServerCommandItem | null>(null);
  const [prodServerStatus, setProdServerStatus] = useState<ServerCommandItem | null>(null);
  const [workServerStatus, setWorkServerStatus] = useState<ServerCommandItem | null>(null);
  const [workServerStatusLoading, setWorkServerStatusLoading] = useState(false);
  const [serverRestartingKey, setServerRestartingKey] = useState<'test' | 'prod' | 'work-server' | 'all' | null>(null);
  const [serverRestartFeedback, setServerRestartFeedback] = useState<InlineFeedback | null>(null);
  const [serverRestartCopyFeedback, setServerRestartCopyFeedback] = useState<InlineFeedback | null>(null);
  const [serverRestartReservation, setServerRestartReservation] = useState<ServerRestartReservation | null>(null);
  const [serverRestartReservationLoading, setServerRestartReservationLoading] = useState(false);
  const [serverRestartReservationFeedback, setServerRestartReservationFeedback] = useState<InlineFeedback | null>(null);
  const [serverRestartReservationReloadPending, setServerRestartReservationReloadPending] = useState(false);
  const [serverRestartReservationNowTimestamp, setServerRestartReservationNowTimestamp] = useState(() => Date.now());
  const [restartProgress, setRestartProgress] = useState<RestartProgressState | null>(null);
  const restartProgressTaskIdRef = useRef(0);
  const restartProgressCancelledRef = useRef(false);
  const restartProgressAbortRef = useRef<AbortController | null>(null);
  const serverRestartReservationCompletedBootstrapRef = useRef(false);
  const handledServerRestartReservationCompletedAtRef = useRef<string | null>(null);
  const { registeredToken, hasAccess } = useTokenAccess();
  const appConfig = useAppConfig();
  useEffect(() => {
    if (typeof document !== 'undefined') {
      document.documentElement.dataset.appTheme = headerTheme;
    }

    if (typeof window !== 'undefined') {
      window.localStorage.setItem(HEADER_THEME_STORAGE_KEY, headerTheme);
    }
  }, [headerTheme]);
  useEffect(() => {
    if (typeof document !== 'undefined') {
      document.documentElement.style.setProperty('--app-desktop-header-height', `${desktopHeaderHeight}px`);
    }

    if (typeof window !== 'undefined') {
      window.localStorage.setItem(DESKTOP_HEADER_HEIGHT_STORAGE_KEY, String(desktopHeaderHeight));
    }
  }, [desktopHeaderHeight]);
  useEffect(() => {
    if (typeof document !== 'undefined') {
      document.documentElement.style.setProperty('--app-mobile-header-height', `${mobileHeaderHeight}px`);
    }

    if (typeof window !== 'undefined') {
      window.localStorage.setItem(MOBILE_HEADER_HEIGHT_STORAGE_KEY, String(mobileHeaderHeight));
    }
  }, [mobileHeaderHeight]);
  const [appConfigDraft, setAppConfigDraft] = useState<AppConfig>(appConfig);
  const [appConfigFeedback, setAppConfigFeedback] = useState<InlineFeedback | null>(null);
  const [appConfigSaving, setAppConfigSaving] = useState(false);
  const [appConfigCopyFeedback, setAppConfigCopyFeedback] = useState<InlineFeedback | null>(null);
  const [tokenInput, setTokenInput] = useState('');
  const [tokenFeedback, setTokenFeedback] = useState<InlineFeedback | null>(null);
  const [planShortcutCounts, setPlanShortcutCounts] = useState({
    working: 0,
    releasePendingMain: 0,
    automationFailed: 0,
  });
  const headerTopMenu = !hasAccess ? 'docs' : activeTopMenu === 'apis' ? 'docs' : activeTopMenu === 'chat' ? 'plans' : activeTopMenu;
  const notificationStatusClassName = notificationEnabled
    ? 'app-header__status-dot--active'
    : 'app-header__status-dot--inactive';
  const chatConnectionStatusClassName =
    chatConnection.connectionState === 'connected'
      ? 'app-header__status-dot--active'
      : chatConnection.connectionState === 'connecting'
        ? 'app-header__status-dot--progress'
        : 'app-header__status-dot--inactive';
  const testServerPendingUpdateCount =
    testServerStatus && (testServerStatus.updateAvailable || testServerStatus.buildRequired) ? 1 : 0;
  const prodServerPendingUpdateCount =
    prodServerStatus && (prodServerStatus.updateAvailable || prodServerStatus.buildRequired) ? 1 : 0;
  const workServerPendingUpdateCount =
    workServerStatus && (workServerStatus.updateAvailable || workServerStatus.buildRequired) ? 1 : 0;
  const totalPendingUpdateCount = testServerPendingUpdateCount + prodServerPendingUpdateCount + workServerPendingUpdateCount;
  const hasBuildRequiredUpdate =
    Boolean(testServerStatus?.buildRequired) ||
    Boolean(prodServerStatus?.buildRequired) ||
    Boolean(workServerStatus?.buildRequired);
  const totalAutomationShortcutCount =
    planShortcutCounts.working + planShortcutCounts.releasePendingMain + planShortcutCounts.automationFailed;
  const settingsStatusClassName =
    hasBuildRequiredUpdate
      ? 'app-header__status-dot--inactive'
      : totalPendingUpdateCount >= 2
      ? 'app-header__status-dot--inactive'
      : totalPendingUpdateCount === 1
        ? 'app-header__status-dot--warning'
        : 'app-header__status-dot--active';
  const settingsStatusLabel =
    hasBuildRequiredUpdate
      ? '커밋 미반영 업데이트 존재'
      : totalPendingUpdateCount >= 2
        ? '모든 업데이트 존재'
        : totalPendingUpdateCount === 1
          ? '업데이트 1건 존재'
          : '최신 상태';
  const serverRestartReservationStatusClassName =
    serverRestartReservation?.enabled
      ? serverRestartReservation.status === 'failed'
        ? 'app-header__status-dot--inactive'
        : 'app-header__status-dot--progress'
      : 'app-header__status-dot--inactive';
  const serverRestartReservationStatusLabel =
    !serverRestartReservation || (!serverRestartReservation.enabled && serverRestartReservation.status === 'idle')
      ? '대기 중지'
      : serverRestartReservation.status === 'waiting'
        ? '대기 중'
        : serverRestartReservation.status === 'ready'
          ? '자동 실행 대기'
          : serverRestartReservation.status === 'recovering'
            ? 'Codex 자동 개선 중'
          : serverRestartReservation.status === 'executing'
          ? '실행 중'
          : serverRestartReservation.status === 'completed'
            ? '최근 실행 완료'
            : serverRestartReservation.status === 'cancelled'
            ? '예약 취소됨'
            : '실패';
  const serverRestartReservationItemLabel = '재기동 예약';
  const serverRestartReservationTimingLabel = getServerRestartReservationTimingLabel(
    serverRestartReservation,
    serverRestartReservationNowTimestamp,
  );
  const serverRestartReservationPendingSummary = getServerRestartReservationPendingSummary(
    serverRestartReservation,
    serverRestartReservationNowTimestamp,
  );
  const serverRestartReservationItemMetaParts =
    serverRestartReservation?.status === 'waiting' || serverRestartReservation?.status === 'ready'
      ? [serverRestartReservationStatusLabel, serverRestartReservationPendingSummary]
      : [serverRestartReservationStatusLabel, serverRestartReservationTimingLabel];
  const serverRestartReservationItemMeta = serverRestartReservationItemMetaParts
    .filter((value): value is string => Boolean(value))
    .join(' · ');
  const serverRestartReservationStatusDescription =
    serverRestartReservation?.status === 'waiting'
      ? serverRestartReservationPendingSummary
      : serverRestartReservation?.status === 'ready'
        ? serverRestartReservationPendingSummary
        : serverRestartReservation?.status === 'recovering'
          ? serverRestartReservation.autoFix.summary
            ?? serverRestartReservation.waitingReason
            ?? '빌드 오류 자동 개선을 진행 중입니다.'
      : serverRestartReservation?.status === 'executing'
        ? serverRestartReservationTimingLabel
        : serverRestartReservation?.status === 'completed'
          ? serverRestartReservationTimingLabel
          : serverRestartReservation?.status === 'cancelled'
            ? serverRestartReservationTimingLabel
            : serverRestartReservation?.status === 'failed'
              ? '실패'
              : null;
  const serverRestartReservationItemDescription =
    serverRestartReservationStatusDescription
    ?? serverRestartReservationFeedback?.message
    ?? serverRestartReservation?.waitingReason
    ?? serverRestartReservation?.lastError
    ?? serverRestartReservationItemMeta;
  const serverRestartReservationOverlayState = getServerRestartReservationOverlayState(
    serverRestartReservation,
    serverRestartReservationNowTimestamp,
    serverRestartReservationReloadPending,
  );
  const resolveServerManagementProgressLabel = (key: 'test' | 'work' | 'prod') => {
    if (
      (key === 'test' && serverRestartingKey === 'test')
      || (key === 'work' && serverRestartingKey === 'work-server')
      || (key === 'prod' && serverRestartingKey === 'prod')
    ) {
      return restartProgress?.stepLabel ?? '재기동 진행 중';
    }

    if (key === 'prod') {
      return null;
    }

    const matchesReservation = key === 'work'
      ? serverRestartReservation?.target === 'work-server' || serverRestartReservation?.target === 'all'
      : serverRestartReservation?.target === 'test' || serverRestartReservation?.target === 'all';

    if (!matchesReservation) {
      return null;
    }

    if (serverRestartReservation?.status === 'recovering') {
      return serverRestartReservation.autoFix.summary?.trim() || 'Codex 자동 개선 중';
    }

    if (serverRestartReservation?.status === 'executing') {
      if (serverRestartReservation.executionPhase === 'commit-main-worktree') {
        return '재기동 실행 준비 중';
      }

      if (serverRestartReservation.executionPhase === 'verify-runtime') {
        return '정상 기동 확인 중';
      }

      if (serverRestartReservation.executionPhase === 'restart-work-server') {
        return 'WORK 재기동 진행 중';
      }

      if (serverRestartReservation.executionPhase === 'restart-test') {
        return 'TEST 재기동 진행 중';
      }

      return '재기동 진행 중';
    }

    if (serverRestartReservation?.status === 'ready') {
      return '자동 실행 대기 중';
    }

    if (serverRestartReservation?.status === 'waiting') {
      return '재기동 대기 중';
    }

    if (serverRestartReservation?.status === 'completed') {
      return '최근 재기동 완료';
    }

    if (serverRestartReservation?.status === 'failed') {
      return '최근 재기동 실패';
    }

    if (serverRestartReservation?.status === 'cancelled') {
      return '최근 재기동 취소';
    }

    return null;
  };
  const serverManagementStatusItems = [
    {
      key: 'test',
      label: 'TEST',
      item: testServerStatus,
      checkedLabel: `확인 ${formatDateTimeLabel(testServerStatus?.checkedAt ?? null)}`,
      detailLabel: `소스 수정일 ${getServerLastSourceChangedDateLabel(testServerStatus)}`,
      hint: getServerLastSourceChangedHint(testServerStatus),
      progressLabel: resolveServerManagementProgressLabel('test'),
    },
    {
      key: 'work',
      label: 'WORK',
      item: workServerStatus,
      checkedLabel: `확인 ${formatDateTimeLabel(workServerStatus?.checkedAt ?? null)}`,
      detailLabel: `소스 수정일 ${getServerLastSourceChangedDateLabel(workServerStatus)}`,
      hint: getServerLastSourceChangedHint(workServerStatus),
      progressLabel: resolveServerManagementProgressLabel('work'),
    },
    {
      key: 'prod',
      label: 'PROD',
      item: prodServerStatus,
      checkedLabel: `확인 ${formatDateTimeLabel(prodServerStatus?.checkedAt ?? null)}`,
      detailLabel: `빌드 시각 ${formatDateTimeLabel(prodServerStatus?.runningBuiltAt ?? null)}`,
      hint: null,
      progressLabel: resolveServerManagementProgressLabel('prod'),
    },
  ] as const;
  const renderTopMenuOptionLabel = (
    menu: 'docs' | 'plans' | 'play',
    label: ReactNode,
    icon: ReactNode,
    iconLabel: string,
  ) => (
    <span
      className="app-header__menu-option"
      aria-label={iconLabel}
      onClick={() => {
        onChangeTopMenu(menu);
      }}
    >
      <span className="app-header__menu-option-icon" aria-hidden="true">
        {icon}
      </span>
      {isMobileViewport ? null : <span className="app-header__menu-option-label">{label}</span>}
    </span>
  );
  const headerTopMenuOptions = hasAccess
    ? [
        {
          label: renderTopMenuOptionLabel('docs', 'Docs', <BookOutlined />, 'Docs'),
          value: 'docs',
        },
        {
          label: renderTopMenuOptionLabel('plans', '작업', <ClusterOutlined />, '작업'),
          value: 'plans',
        },
        {
          label: renderTopMenuOptionLabel('play', 'Play', <RocketOutlined />, 'Play'),
          value: 'play',
        },
      ]
    : [
        {
          label: renderTopMenuOptionLabel('docs', 'Docs', <BookOutlined />, 'Docs'),
          value: 'docs',
        },
      ];
  const runningRuntimeCount = chatRuntimeSnapshot?.runningCount ?? 0;
  const queuedRuntimeCount = chatRuntimeSnapshot?.queuedCount ?? 0;
  const runtimeSessionCount = chatRuntimeSnapshot?.sessionCount ?? 0;
  const runningRuntimeBadgeLabel = runningRuntimeCount > 99 ? '99+' : String(runningRuntimeCount);
  const hasPendingRuntimeWork = runningRuntimeCount > 0 || queuedRuntimeCount > 0;
  const chatConnectionLabelParts = [
    chatConnection.connectionState === 'connected'
      ? 'Codex 채팅 실시간 연결됨'
      : chatConnection.connectionState === 'connecting'
        ? 'Codex 채팅 연결 중'
        : 'Codex 채팅 오프라인',
  ];

  if (runningRuntimeCount > 0) {
    chatConnectionLabelParts.push(`실행 ${runningRuntimeCount}건`);
  }

  if (queuedRuntimeCount > 0) {
    chatConnectionLabelParts.push(`대기 ${queuedRuntimeCount}건`);
  }

  const chatConnectionLabel = chatConnectionLabelParts.join(' · ');
  const searchShortcutLabel = appConfig.gestureShortcuts.openSearch || DEFAULT_APP_CONFIG.gestureShortcuts.openSearch;
  const connectionIndicatorClassName = `app-header__connection-indicator app-header__connection-indicator--${chatConnection.connectionState}${
    hasPendingRuntimeWork ? ' app-header__connection-indicator--busy' : ''
  }`;
  const connectionCountBadgeClassName = `app-header__connection-count-badge app-header__connection-count-badge--${chatConnection.connectionState}`;
  const getConversationLabel = (sessionId: string) => {
    if (sessionId === activeConversationSnapshot.sessionId && activeConversationSnapshot.title) {
      return activeConversationSnapshot.title;
    }

    return runtimeConversationTitles[sessionId] || sessionId;
  };
  const navigateToConversation = (sessionId: string) => {
    const searchParams = new URLSearchParams(location.search);
    searchParams.set('topMenu', 'chat');
    searchParams.set('sessionId', sessionId);
    searchParams.delete('chatView');
    searchParams.delete('runtimeRequestId');
    navigate({
      pathname: buildChatPath('live'),
      search: `?${searchParams.toString()}`,
    });
    setIsRuntimeModalOpen(false);
  };
  const openRuntimeLog = async (requestId: string) => {
    setIsRuntimeModalOpen(false);
    setIsRuntimeLogDrawerOpen(true);
    setRuntimeLogLoading(true);
    setRuntimeLogError('');

    try {
      const detail = await fetchChatRuntimeJobDetail(requestId);
      setRuntimeLogDetail(detail);
    } catch (error) {
      setRuntimeLogDetail(null);
      setRuntimeLogError(error instanceof Error ? error.message : '실행 로그를 불러오지 못했습니다.');
    } finally {
      setRuntimeLogLoading(false);
    }
  };
  const canRefreshWorkServerStatus =
    hasAccess &&
    !workServerStatusLoading &&
    !serverRestartReservationLoading &&
    !serverRestartingKey;
  const canRestartServers =
    hasAccess &&
    !workServerStatusLoading &&
    !serverRestartingKey;
  const chatSettingsDirty = !areChatSettingsEqual(appConfig.chat, appConfigDraft.chat);
  const worklogAutomationSettingsDirty = !areWorklogAutomationSettingsEqual(
    appConfig.worklogAutomation,
    appConfigDraft.worklogAutomation,
  );
  const automationRuntimeSettingsDirty = !areAutomationRuntimeSettingsEqual(appConfig.automation, appConfigDraft.automation);
  const chatSettingsDiffLabels = getChatSettingsDiffLabels(appConfig.chat, appConfigDraft.chat);
  const automationRuntimeDiffLabels = getAutomationRuntimeDiffLabels(appConfig.automation, appConfigDraft.automation);
  const planDefaultSettingsDirty = !arePlanDefaultSettingsEqual(appConfig.planDefaults, appConfigDraft.planDefaults);
  const planDefaultDiffLabels = getPlanDefaultDiffLabels(appConfig.planDefaults, appConfigDraft.planDefaults);
  const planCostSettingsDirty = !arePlanCostSettingsEqual(appConfig.planCost, appConfigDraft.planCost);
  const planCostDiffLabels = getPlanCostDiffLabels(appConfig.planCost, appConfigDraft.planCost);
  const worklogAutomationDiffLabels = getWorklogAutomationDiffLabels(
    appConfig.worklogAutomation,
    appConfigDraft.worklogAutomation,
  );
  const automationNotificationSettingsDirty = !areAutomationNotificationSettingsEqual(
    appConfig.automation,
    appConfigDraft.automation,
  );
  const automationNotificationDiffLabels = getAutomationNotificationDiffLabels(
    appConfig.automation,
    appConfigDraft.automation,
  );
  const gestureShortcutSettingsDirty = !areGestureShortcutSettingsEqual(
    appConfig.gestureShortcuts,
    appConfigDraft.gestureShortcuts,
  );
  const gestureShortcutDiffLabels = getGestureShortcutDiffLabels(
    appConfig.gestureShortcuts,
    appConfigDraft.gestureShortcuts,
  );
  const activeAppSettingsSectionOptions = getAppSettingsSectionOptions(activeAppSettingsCategory);

  const syncRegisteredWebPushStatus = async () => {
    const permission = getClientNotificationPermission();
    setClientNotificationPermission(permission);
    setIsStandaloneMode(isStandaloneDisplayMode());

    if (permission === 'unsupported') {
      setNotificationEnabled(false);
      setNotificationPendingRegistration(false);
      return;
    }

    try {
      const config = await fetchWebPushConfig();
      setWebPushConfigured(Boolean(config.enabled && config.publicKey));

      if (!config.enabled || !config.publicKey) {
        setNotificationEnabled(false);
        setNotificationPendingRegistration(false);
        return;
      }

      if (Notification.permission !== 'granted') {
        setNotificationEnabled(false);
        setNotificationPendingRegistration(false);
        return;
      }

      const registration = await getPushServiceWorkerRegistration();

      if (!registration) {
        setNotificationEnabled(false);
        setNotificationPendingRegistration(false);
        return;
      }

      const subscription = await registration.pushManager.getSubscription();

      if (!subscription) {
        setNotificationEnabled(false);
        setNotificationPendingRegistration(false);
        return;
      }

      await syncExistingWebPushSubscriptionRegistration(registration, {
        deviceId: getSavedNotificationDeviceId(),
      });
      setNotificationEnabled(true);
      setNotificationPendingRegistration(false);
      setNotificationFeedback(null);
    } catch {
      setNotificationEnabled(false);
      setNotificationPendingRegistration(false);
    }
  };

  useEffect(() => {
    void syncRegisteredWebPushStatus();
  }, []);

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

    if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
      setNotificationEnabled(false);
      setNotificationPendingRegistration(false);
      return;
    }

    let cancelled = false;

    const finalizePendingRegistration = async () => {
      try {
        await navigator.serviceWorker.ready;

        if (cancelled) {
          return;
        }

        setNotificationLoading(true);
        const registration = await getPushServiceWorkerRegistration();

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

        await ensureWebPushSubscriptionRegistered(registration, {
          deviceId: getSavedNotificationDeviceId(),
        });

        if (cancelled) {
          return;
        }

        setNotificationEnabled(true);
        setNotificationPendingRegistration(false);
        setNotificationFeedback({ tone: 'success', message: '이 기기의 웹 푸시 알림을 서버에 등록했습니다.' });
      } catch (error) {
        if (cancelled) {
          return;
        }

        setNotificationEnabled(false);
        setNotificationPendingRegistration(false);
        setNotificationFeedback({
          tone: 'error',
          message: error instanceof Error ? error.message : '알림 설정 저장에 실패했습니다.',
        });
      } finally {
        if (!cancelled) {
          setNotificationLoading(false);
        }
      }
    };

    void finalizePendingRegistration();

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

  useEffect(() => {
    const handleSync = () => {
      void syncRegisteredWebPushStatus();
    };

    window.addEventListener('focus', handleSync);
    window.addEventListener('online', handleSync);
    window.addEventListener('pageshow', handleSync);
    document.addEventListener('visibilitychange', handleSync);
    if ('serviceWorker' in navigator) {
      void navigator.serviceWorker.ready
        .then(() => {
          void syncRegisteredWebPushStatus();
        })
        .catch(() => undefined);
    }

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

  useEffect(() => {
    return chatConnectionGateway.subscribe(() => {
      setChatConnection(chatConnectionGateway.getSnapshot());
      setChatRuntimeSnapshot(chatConnectionGateway.getSharedRuntimeSnapshot());
    });
  }, []);

  useEffect(() => {
    return subscribeSharedActiveConversation(() => {
      setActiveConversationSnapshot(getSharedActiveConversationSnapshot());
    });
  }, []);

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

    let cancelled = false;
    void chatGateway.listConversations()
      .then((items) => {
        if (cancelled) {
          return;
        }

        setRuntimeConversationTitles(
          items.reduce<Record<string, string>>((acc, item) => {
            acc[item.sessionId] = item.title?.trim() || item.sessionId;
            return acc;
          }, {}),
        );
      })
      .catch(() => {
        if (!cancelled) {
          setRuntimeConversationTitles({});
        }
      });

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

  useEffect(() => {
    setTokenInput(registeredToken);
  }, [registeredToken]);

  useEffect(() => {
    setAppConfigDraft(appConfig);
  }, [appConfig]);

  useEffect(() => {
    if (getAppSettingsSectionCategory(activeAppSettingsSection) === activeAppSettingsCategory) {
      return;
    }

    const firstSection = APP_SETTINGS_SECTIONS.find((entry) => entry.category === activeAppSettingsCategory);

    if (firstSection) {
      setActiveAppSettingsSection(firstSection.value);
    }
  }, [activeAppSettingsCategory, activeAppSettingsSection]);

  useEffect(() => {
    if (!isSettingsModalAllowed(activeSettingsModal, hasAccess)) {
      setActiveSettingsModal('token');
    }
  }, [activeSettingsModal, hasAccess]);

  useEffect(() => {
    if (!hasAccess) {
      setTestServerStatus(null);
      setWorkServerStatus(null);
      setServerRestartReservation(null);
      return;
    }

    void refreshServerManagementStatus(true);
  }, [hasAccess]);

  useEffect(() => {
    if (!settingsModalOpen || activeSettingsModal !== 'update' || !hasAccess) {
      return;
    }

    void refreshServerManagementStatus(true);
  }, [activeSettingsModal, hasAccess, settingsModalOpen]);

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

    if (
      serverRestartReservation.status !== 'waiting'
      && serverRestartReservation.status !== 'ready'
      && serverRestartReservation.status !== 'executing'
    ) {
      return;
    }

    const timer = window.setInterval(() => {
      setServerRestartReservationNowTimestamp(Date.now());
    }, 1_000);

    return () => {
      window.clearInterval(timer);
    };
  }, [serverRestartReservation]);

  useEffect(() => {
    if (!serverRestartReservation) {
      setServerRestartReservationReloadPending(false);
      return;
    }

    if (serverRestartReservation.status !== 'completed') {
      setServerRestartReservationReloadPending(false);
    }

    if (!serverRestartReservationCompletedBootstrapRef.current) {
      serverRestartReservationCompletedBootstrapRef.current = true;
      handledServerRestartReservationCompletedAtRef.current = serverRestartReservation.completedAt;
      return;
    }

    if (
      serverRestartReservation.status !== 'completed'
      || !serverRestartReservation.completedAt
      || handledServerRestartReservationCompletedAtRef.current === serverRestartReservation.completedAt
    ) {
      return;
    }

    handledServerRestartReservationCompletedAtRef.current = serverRestartReservation.completedAt;
    void (async () => {
      setServerRestartReservationReloadPending(true);
      setServerRestartReservationFeedback({
        tone: 'info',
        message: '예약된 재기동 완료 신호를 받았습니다. 자동 새로고침 없이 현재 상태만 한 번 다시 확인합니다.',
      });

      try {
        const statuses = await refreshServerStatuses();
        const testVerified = hasReservedRestartServerBeenVerified('test', statuses.test, serverRestartReservation.startedAt);
        const workVerified = hasReservedRestartServerBeenVerified(
          'work-server',
          statuses['work-server'],
          serverRestartReservation.startedAt,
        );

        setServerRestartReservationReloadPending(false);
        if (testVerified && workVerified) {
          setServerRestartReservationFeedback({
            tone: 'success',
            message: '예약된 재기동 뒤 TEST/WORK 서버 새 런타임을 확인했습니다. 필요할 때만 화면 새로고침을 직접 실행해 주세요.',
          });
          return;
        }

        const pendingTargets = [
          !testVerified ? 'TEST' : null,
          !workVerified ? 'WORK' : null,
        ].filter((value): value is string => Boolean(value));
        setServerRestartReservationFeedback({
          tone: 'warning',
          message: `${pendingTargets.join('/')} 서버 새 런타임은 아직 확인되지 않았습니다. 자동 재시도 없이 상태 새로고침에서 다시 확인해 주세요.`,
        });
      } catch {
        setServerRestartReservationReloadPending(false);
        setServerRestartReservationFeedback({
          tone: 'warning',
          message: '재기동 뒤 상태를 한 번 다시 확인하지 못했습니다. 자동 재시도 없이 상태 새로고침에서 다시 확인해 주세요.',
        });
      }
    })();
  }, [serverRestartReservation]);

  const ensureClientNotificationPermission = async () => {
    const currentPermission = getClientNotificationPermission();
    setClientNotificationPermission(currentPermission);

    if (currentPermission === 'unsupported') {
      setNotificationFeedback({ tone: 'error', message: '현재 브라우저에서는 Web Push를 지원하지 않습니다.' });
      return false;
    }

    if (!hasSecureOrigin()) {
      setNotificationFeedback({ tone: 'error', message: '알림은 HTTPS 또는 localhost 환경에서만 사용할 수 있습니다.' });
      return false;
    }

    if (isAppleMobileDevice() && !isStandaloneDisplayMode()) {
      setNotificationFeedback({
        tone: 'warning',
        message: '아이폰에서는 홈 화면에 추가한 뒤 실행한 PWA에서만 웹 푸시를 사용할 수 있습니다.',
      });
      return false;
    }

    if (currentPermission === 'denied') {
      setNotificationFeedback({
        tone: 'warning',
        message: '브라우저 또는 기기 설정에서 알림 권한을 먼저 허용해 주세요.',
      });
      return false;
    }

    if (currentPermission === 'granted') {
      setNotificationFeedback(null);
      return true;
    }

    try {
      const permission = await Notification.requestPermission();
      const nextPermission =
        permission === 'granted' ? 'granted' : permission === 'denied' ? 'denied' : 'default';
      setClientNotificationPermission(nextPermission);

      if (nextPermission !== 'granted') {
        setNotificationFeedback({ tone: 'warning', message: '알림 권한이 허용되지 않아 알림을 켤 수 없습니다.' });
        return false;
      }

      setNotificationFeedback(null);
      return true;
    } catch {
      setNotificationFeedback({ tone: 'error', message: '알림 권한 요청 중 오류가 발생했습니다.' });
      return false;
    }
  };

  const syncNotificationEnabled = async (nextEnabled: boolean) => {
    if (notificationLoading) {
      return;
    }

    if (isPreviewRuntime()) {
      setNotificationEnabled(false);
      setNotificationCopyFeedback(null);
      setNotificationLoading(false);
      setNotificationFeedback({
        tone: 'warning',
        message: '미리보기 런타임에서는 알림 등록을 지원하지 않습니다. 실제 앱 화면에서 다시 시도해 주세요.',
      });
      return;
    }

    const previousEnabled = notificationEnabled;
    setNotificationCopyFeedback(null);
    setNotificationLoading(true);
    setNotificationPendingRegistration(false);
    setNotificationEnabled(nextEnabled);

    if (nextEnabled) {
      setNotificationFeedback({ tone: 'info', message: '알림 권한과 Web Push 등록 상태를 확인하는 중입니다.' });
      const permissionGranted = await ensureClientNotificationPermission();

      if (!permissionGranted) {
        setNotificationEnabled(false);
        setNotificationLoading(false);
        return;
      }
    }

    try {
      let registration = await getPushServiceWorkerRegistration();

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

      if (!registration) {
        if (nextEnabled) {
          setNotificationEnabled(true);
          setNotificationPendingRegistration(true);
          setNotificationFeedback({
            tone: 'info',
            message: '알림 서비스워커를 준비 중입니다. 준비가 끝나는 대로 자동으로 등록합니다.',
          });
          return;
        }

        setNotificationEnabled(false);
        setNotificationPendingRegistration(false);
        setNotificationFeedback({ tone: 'success', message: '이 기기의 웹 푸시 알림을 해제했습니다.' });
        return;
      }

      if (nextEnabled) {
        await ensureWebPushSubscriptionRegistered(registration, {
          deviceId: getSavedNotificationDeviceId(),
        });
        setNotificationEnabled(true);
        setNotificationPendingRegistration(false);
        setNotificationFeedback({ tone: 'success', message: '이 기기의 웹 푸시 알림을 서버에 등록했습니다.' });
        return;
      }

      await clearWebPushSubscriptionRegistration(registration);

      setNotificationEnabled(false);
      setNotificationPendingRegistration(false);
      setNotificationFeedback({ tone: 'success', message: '이 기기의 웹 푸시 알림을 해제했습니다.' });
    } catch (error) {
      setNotificationEnabled(previousEnabled);
      setNotificationPendingRegistration(false);
      setNotificationFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : '알림 설정 저장에 실패했습니다.',
      });
    } finally {
      setNotificationLoading(false);
    }
  };

  const refreshServerStatuses = async () => {
    const items = await fetchServerCommands();
    const nextTestServerStatus = items.find((item) => item.key === 'test') ?? null;
    const nextProdServerStatus = items.find((item) => item.key === 'prod') ?? null;
    const nextWorkServerStatus = items.find((item) => item.key === 'work-server') ?? null;
    setTestServerStatus(nextTestServerStatus);
    setProdServerStatus(nextProdServerStatus);
    setWorkServerStatus(nextWorkServerStatus);
    return {
      test: nextTestServerStatus,
      prod: nextProdServerStatus,
      'work-server': nextWorkServerStatus,
    } satisfies Record<'test' | 'prod' | 'work-server', ServerCommandItem | null>;
  };

  const refreshServerRestartReservation = async (silent = false) => {
    if (!hasAccess) {
      setServerRestartReservation(null);
      if (!silent) {
        setServerRestartReservationFeedback({ tone: 'warning', message: '재기동 예약은 권한 토큰 등록 후 사용할 수 있습니다.' });
      }
      return null;
    }

    if (!silent) {
      setServerRestartReservationLoading(true);
    }

    try {
      const nextReservation = await fetchServerRestartReservation();
      setServerRestartReservation(nextReservation);

      if (!silent) {
        setServerRestartReservationFeedback(null);
      }

      return nextReservation;
    } catch (error) {
      if (!silent) {
        setServerRestartReservationFeedback({
          tone: 'error',
          message: error instanceof Error ? error.message : '재기동 예약 상태를 불러오지 못했습니다.',
        });
      }
      return null;
    } finally {
      if (!silent) {
        setServerRestartReservationLoading(false);
      }
    }
  };

  const refreshUpdateTargets = async (silent = false) => {
    if (!hasAccess) {
      setTestServerStatus(null);
      setProdServerStatus(null);
      setWorkServerStatus(null);
      if (!silent) {
        setUpdateCheckFeedback({ tone: 'warning', message: '업데이트 확인은 권한 토큰 등록 후 사용할 수 있습니다.' });
      }
      return null;
    }

    setWorkServerStatusLoading(true);

    try {
      const nextStatuses = await refreshServerStatuses();

      if (!silent) {
        setUpdateCheckFeedback(null);
      }

      return nextStatuses;
    } catch (error) {
      if (!silent) {
        setUpdateCheckFeedback({
          tone: 'error',
          message: error instanceof Error ? error.message : 'TEST/PROD/WORK 서버 업데이트 상태를 불러오지 못했습니다.',
        });
      }
      return null;
    } finally {
      setWorkServerStatusLoading(false);
    }
  };

  const refreshServerManagementStatus = async (silent = false) => {
    await Promise.all([
      refreshUpdateTargets(silent),
      refreshServerRestartReservation(silent),
    ]);
  };

  const waitForServerRestart = async (
    key: 'test' | 'prod' | 'work-server',
    baseline: ServerCommandItem | null,
    progressTaskId: number,
  ) => {
    for (let attempt = 0; attempt < 16; attempt += 1) {
      if (isRestartProgressCancelled(progressTaskId)) {
        return {
          ok: false,
          cancelled: true,
          item: key === 'test' ? testServerStatus : key === 'prod' ? prodServerStatus : workServerStatus,
        };
      }

      updateRestartProgress(
        progressTaskId,
        '서버 부팅 대기 중',
        `현재 진행상태: ${attempt + 1}/16회 대기 중입니다. 재기동 후 응답이 돌아올 때까지 잠시 기다립니다.`,
      );
      await waitForDuration(2500);

      try {
        if (isRestartProgressCancelled(progressTaskId)) {
          return {
            ok: false,
            cancelled: true,
            item: key === 'test' ? testServerStatus : key === 'prod' ? prodServerStatus : workServerStatus,
          };
        }

        updateRestartProgress(
          progressTaskId,
          '서버 상태 확인 중',
          `현재 진행상태: ${attempt + 1}/16회 상태 조회 중입니다. 실제 부팅 완료 응답을 확인하고 있습니다.`,
        );
        const nextStatuses = await refreshServerStatuses();
        const nextStatus = nextStatuses[key];

        if (!nextStatus) {
          updateRestartProgress(
            progressTaskId,
            '서버 상태 재확인 중',
            `현재 진행상태: ${attempt + 1}/16회 확인 결과 응답 정보가 없어 다시 확인합니다.`,
          );
          continue;
        }

        const restarted = hasServerRestartBeenVerified(key, baseline, nextStatus);

        if (restarted) {
          updateRestartProgress(
            progressTaskId,
            '부팅 완료 확인',
            `현재 진행상태: ${attempt + 1}/16회 확인에서 정상 응답을 확인했습니다.`,
            { cancellable: false },
          );
          return { ok: true, cancelled: false, item: nextStatus };
        }

        const statusLabel = getServerAvailabilityStatusLabel(nextStatus);
        const checkedAtLabel = formatDateTimeLabel(nextStatus.checkedAt);
        updateRestartProgress(
          progressTaskId,
          '서버 상태 재확인 중',
          `현재 진행상태: ${attempt + 1}/16회 확인 결과 ${statusLabel}${checkedAtLabel ? ` (${checkedAtLabel})` : ''}. 실제 재기동/최신 빌드 반영을 계속 확인합니다.`,
        );
      } catch {
        // 서버 재기동 중에는 일시적으로 조회가 실패할 수 있습니다.
        updateRestartProgress(
          progressTaskId,
          '서버 응답 재시도 중',
          `현재 진행상태: ${attempt + 1}/16회 확인에서 응답이 불안정해 다시 확인합니다.`,
        );
      }
    }

    return {
      ok: false,
      cancelled: false,
      item: key === 'test' ? testServerStatus : key === 'prod' ? prodServerStatus : workServerStatus,
    };
  };

  const executeRestartCompletionAction = async (action: RestartCompletionAction) => {
    if (action === 'reset-client-state') {
      await resetNonAuthClientState();
      window.location.replace(buildCacheBustedReloadUrl());
      return;
    }

    if (action === 'custom') {
      return;
    }

    window.location.replace(buildCacheBustedReloadUrl());
  };

  const openRestartCompletionConfirm = (options: RestartCompletionConfirmOptions) => {
    const {
      title,
      targetLabel,
      action,
      okText,
      cancelText,
      promptText,
      customAction,
      onCancel,
      onActionError,
    } = options;
    const actionLabel = getRestartCompletionActionLabel(action);
    let settled = false;

    const buildContent = () => (
      <Space direction="vertical" size={8}>
        <Text>{promptText ?? `실제로 ${targetLabel} 부팅 완료까지 확인했습니다. 지금 ${actionLabel.toLowerCase()}할까요?`}</Text>
        <Text type="secondary">자동으로 진행하지 않습니다. 필요할 때만 직접 선택해 주세요.</Text>
      </Space>
    );

    const runAction = async () => {
      if (settled) {
        return;
      }

      settled = true;
      try {
        if (action === 'custom') {
          await customAction?.();
          return;
        }

        await executeRestartCompletionAction(action);
      } catch (error) {
        const message =
          error instanceof Error
            ? error.message
            : `${targetLabel} 재기동 후 ${actionLabel.toLowerCase()}를 진행하지 못했습니다.`;
        onActionError?.(message);
        setServerRestartFeedback({
          tone: 'error',
          message,
        });
      }
    };

    const confirmModal = modalApi.confirm({
      title,
      content: buildContent(),
      okText: okText ?? actionLabel,
      cancelText: cancelText ?? '나중에',
      autoFocusButton: 'ok',
      modalRender: renderModalWithEnterConfirm,
      onOk: () => runAction(),
      onCancel: () => {
        settled = true;
        onCancel?.();
      },
    });
  };

  const restartServerWithVerification = async (
    key: 'test' | 'prod' | 'work-server',
    busyKey: 'test' | 'prod' | 'work-server' | 'all',
    progressTaskId: number,
  ) => {
    const baseline = key === 'test' ? testServerStatus : key === 'prod' ? prodServerStatus : workServerStatus;
    const targetLabel = getServerRestartTargetLabel(key);

    updateRestartProgress(progressTaskId, '재기동 요청 전송 중', `${targetLabel} 재기동 명령을 전달하고 있습니다.`);
    const result = await restartServerCommand(key, { signal: restartProgressAbortRef.current?.signal });

    if (key === 'test') {
      setTestServerStatus(result.item);
    } else if (key === 'prod') {
      setProdServerStatus(result.item);
    } else {
      setWorkServerStatus(result.item);
    }

    setServerRestartFeedback({
      tone: 'info',
      message:
        result.restartState === 'accepted'
          ? `${targetLabel} 재기동 요청을 접수했습니다. 실제 정상 응답 여부를 확인하는 중입니다.`
          : `${targetLabel} 재기동을 실행했습니다. 실제 정상 응답 여부를 확인하는 중입니다.`,
    });
    setServerRestartingKey(busyKey);

    updateRestartProgress(
      progressTaskId,
      '재기동 요청 접수',
      `${targetLabel} 재기동 요청이 접수되었습니다. 실제 부팅 완료를 확인할 때까지 기다립니다.`,
    );
    const verificationBaseline = result.restartState === 'accepted' ? result.item : baseline;
    const verified = await waitForServerRestart(key, verificationBaseline, progressTaskId);

    if (verified.cancelled || isRestartProgressCancelled(progressTaskId)) {
      setServerRestartFeedback({
        tone: 'warning',
        message: `${targetLabel} 재기동 대기를 취소했습니다. 서버 재기동 요청은 이미 접수됐을 수 있으니 상태를 다시 확인해 주세요.`,
      });
      return null;
    }

    if (!verified.ok || !verified.item || verified.item.availability !== 'online') {
      setServerRestartFeedback({
        tone: 'error',
        message: `${targetLabel} 재기동 후 정상 응답을 확인하지 못했습니다. 상태를 다시 확인해 주세요.`,
      });
      return null;
    }

    const followupMessage = getServerRestartFollowupMessage(verified.item, targetLabel);
    setServerRestartFeedback({
      tone: followupMessage ? 'warning' : 'success',
      message: followupMessage
        ? `${targetLabel} 재기동 성공을 확인했습니다. ${followupMessage} 확인 시각 ${formatDateTimeLabel(verified.item.checkedAt)}`
        : `${targetLabel} 재기동 성공을 확인했습니다. 확인 시각 ${formatDateTimeLabel(verified.item.checkedAt)}`,
    });
    return verified.item;
  };

  const handleRestartSingleServer = async (key: 'test' | 'prod' | 'work-server') => {
    if (!hasAccess || serverRestartingKey) {
      return false;
    }

    const targetLabel = getServerRestartTargetLabel(key);
    const baselineStatus = key === 'test' ? testServerStatus : key === 'prod' ? prodServerStatus : workServerStatus;
    const progressTaskId = beginRestartProgress(targetLabel);
    setServerRestartCopyFeedback(null);
    setServerRestartFeedback(null);
    setServerRestartingKey(key);

    try {
      const restartedItem = await restartServerWithVerification(key, key, progressTaskId);

      if (!restartedItem || isRestartProgressCancelled(progressTaskId)) {
        closeRestartProgress();
        return false;
      }

      const completionAction = shouldResetClientStateAfterRestart(key, baselineStatus, restartedItem)
        ? 'reset-client-state'
        : 'reload';

      updateRestartProgress(
        progressTaskId,
        '재기동 확인 완료',
        `${targetLabel} 부팅 완료를 확인했습니다. ${getRestartCompletionActionLabel(completionAction)} 여부를 선택해 주세요.`,
        { cancellable: false },
      );
      closeRestartProgress();
      openRestartCompletionConfirm({
        title: `${targetLabel} 재기동 완료`,
        targetLabel,
        action: completionAction,
      });
      return true;
    } catch (error) {
      if (error instanceof DOMException && error.name === 'AbortError') {
        setServerRestartFeedback({
          tone: 'warning',
          message: `${targetLabel} 재기동 대기를 취소했습니다. 서버 재기동 요청은 이미 접수됐을 수 있으니 상태를 다시 확인해 주세요.`,
        });
        return false;
      }

      setServerRestartFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : `${targetLabel} 재기동에 실패했습니다.`,
      });
      return false;
    } finally {
      closeRestartProgress();
      setServerRestartingKey(null);
    }
  };

  const handleConfirmRestartProdServer = () => {
    if (!hasAccess || serverRestartingKey) {
      return;
    }

    modalApi.confirm({
      title: 'PROD 빌드 반영',
      content: 'PROD 컨테이너를 빌드 후 재기동합니다. 진행할까요?',
      okText: '빌드 및 재기동',
      cancelText: '취소',
      autoFocusButton: 'ok',
      modalRender: renderModalWithEnterConfirm,
      okButtonProps: { danger: true },
      onOk: async () => {
        await handleRestartSingleServer('prod');
      },
    });
  };

  const handleScheduleServerRestartReservation = async (
    target: 'all' | 'work-server' = 'all',
  ) => {
    if (!hasAccess || serverRestartReservationLoading || serverRestartingKey) {
      return;
    }

    setServerRestartReservationLoading(true);
    setServerRestartReservationFeedback(null);
    setServerRestartFeedback(null);

    try {
      const nextReservation = await scheduleServerRestartReservation({
        target,
        autoExecuteDelaySeconds: appConfig.chat.restartReservationCompletionDelaySeconds,
      });
      setServerRestartReservation(nextReservation);
      setServerRestartReservationFeedback({
        tone: 'success',
        message:
          target === 'work-server'
            ? `WORK 서버 무중단 재기동 예약을 등록했습니다. 작업이 모두 끝나면 ${appConfig.chat.restartReservationCompletionDelaySeconds}초 뒤 자동 실행합니다.`
            : `전체 재기동 예약을 등록했습니다. 작업이 모두 끝나면 ${appConfig.chat.restartReservationCompletionDelaySeconds}초 뒤 자동 실행합니다.`,
      });
    } catch (error) {
      setServerRestartReservationFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : '재기동 예약 등록에 실패했습니다.',
      });
    } finally {
      setServerRestartReservationLoading(false);
    }
  };

  const handleCancelServerRestartReservation = async () => {
    if (!hasAccess || serverRestartReservationLoading) {
      return;
    }

    setServerRestartReservationLoading(true);
    setServerRestartReservationFeedback(null);

    try {
      const nextReservation = await cancelServerRestartReservation();
      setServerRestartReservation(nextReservation);
      setServerRestartReservationFeedback({
        tone: 'success',
        message: '재기동 예약을 취소했습니다.',
      });
    } catch (error) {
      setServerRestartReservationFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : '재기동 예약 취소에 실패했습니다.',
      });
    } finally {
      setServerRestartReservationLoading(false);
    }
  };

  const handleToggleServerRestartReservation = async () => {
    if (serverRestartReservation?.enabled) {
      await handleCancelServerRestartReservation();
      return;
    }

    await handleScheduleServerRestartReservation();
  };

  const openSettingsModal = (
    modal: SettingsModalKey,
    nextAppSettingsSection: AppSettingsSectionKey = 'planDefaults',
  ) => {
    setSettingsOpen(false);
    setActiveSettingsModal(isSettingsModalAllowed(modal, hasAccess) ? modal : 'token');
    setActiveAppSettingsCategory(getAppSettingsSectionCategory(nextAppSettingsSection));
    setActiveAppSettingsSection(nextAppSettingsSection);
    setAppConfigDraft(appConfig);
    setAppConfigFeedback(null);
    setSettingsModalOpen(true);
  };

  const updateAppConfigDraft = (updater: (current: AppConfig) => AppConfig) => {
    setAppConfigDraft((current) => updater(current));
  };

  const handleSaveAppConfig = async () => {
    if (appConfigSaving) {
      return;
    }

    setAppConfigSaving(true);
    setAppConfigFeedback(null);
    setAppConfigCopyFeedback(null);

    try {
      const savedConfig =
        activeAppSettingsSection === 'automationNotifications'
          ? await saveAutomationNotificationPreferenceToServer(appConfigDraft)
          : await saveAppConfigToServer(appConfigDraft);
      setStoredAppConfig(savedConfig);
      setAppConfigFeedback({
        tone: 'success',
        message:
          activeAppSettingsSection === 'automationNotifications'
            ? '자동화 알림 설정을 알림 토큰과 클라이언트별로 저장했습니다.'
            : '앱 설정을 DB에 저장했습니다.',
      });
    } catch (error) {
      setAppConfigDraft(appConfig);
      setAppConfigFeedback({
        tone: 'error',
        message: error instanceof Error ? error.message : '앱 설정 저장에 실패했습니다.',
      });
    } finally {
      setAppConfigSaving(false);
    }
  };

  const handleResetNotificationIdentity = () => {
    modalApi.confirm({
      title: '알림 클라이언트 초기화',
      content: '현재 브라우저의 알림 토큰/디바이스 식별자를 초기화합니다. 다시 접속하면 새로 등록됩니다.',
      okText: '초기화',
      cancelText: '취소',
      autoFocusButton: 'ok',
      modalRender: renderModalWithEnterConfirm,
      onOk: () => {
        clearNotificationIdentity();
        window.location.reload();
      },
    });
  };

  const handleResetAppConfig = () => {
    setAppConfigDraft(DEFAULT_APP_CONFIG);
    setAppConfigFeedback({ tone: 'info', message: '추천 기본값으로 되돌렸습니다. 저장 후 반영됩니다.' });
    setAppConfigCopyFeedback(null);
  };

  useEffect(() => {
    void syncAppConfigFromServer().then((synced) => {
      if (!synced) {
        setAppConfigFeedback({ tone: 'warning', message: '서버 설정을 불러오지 못해 로컬 설정을 사용합니다.' });
      }
    });
  }, []);

  useEffect(
    () => () => {
      restartProgressAbortRef.current?.abort();
    },
    [],
  );

  useEffect(() => {
    if (!hasAccess || !settingsOpen) {
      setPlanShortcutCounts({
        working: 0,
        releasePendingMain: 0,
        automationFailed: 0,
      });
      return;
    }

    let cancelled = false;

    void fetchPlanItems('all')
      .then((items) => {
        if (cancelled) {
          return;
        }

        setPlanShortcutCounts({
          working: items.filter(isWorkingPlanItem).length,
          releasePendingMain: items.filter(isReleasePendingMainItem).length,
          automationFailed: items.filter(isAutomationFailedItem).length,
        });
      })
      .catch(() => {
        if (!cancelled) {
          setPlanShortcutCounts({
            working: 0,
            releasePendingMain: 0,
            automationFailed: 0,
          });
        }
      });

    return () => {
      cancelled = true;
    };
  }, [hasAccess, settingsOpen]);

  const renderFeedback = (
    feedback: InlineFeedback | null,
    copyFeedback: InlineFeedback | null,
    setCopyFeedback: (feedback: InlineFeedback | null) => void,
  ) => {
    if (!feedback) {
      return null;
    }

    const handleCopyFeedbackMessage = () => {
      void copyTextToClipboard(feedback.message)
        .then(() => {
          setCopyFeedback({
            tone: 'success',
            message: '메시지를 복사했습니다.',
          });
        })
        .catch(() => {
          setCopyFeedback({ tone: 'error', message: '메시지 복사에 실패했습니다.' });
        });
    };

    return (
      <Space direction="vertical" size={8} style={{ width: '100%' }}>
        <Alert
          showIcon
          type={feedback.tone}
          message={feedback.message}
          action={!screens.xs ? (
            <Button
              type="text"
              size="small"
              aria-label="메시지 복사"
              icon={<CopyOutlined />}
              onClick={handleCopyFeedbackMessage}
            />
          ) : null}
        />
        {screens.xs ? (
          <Button block icon={<CopyOutlined />} onClick={handleCopyFeedbackMessage}>
            메시지 복사
          </Button>
        ) : null}
        {copyFeedback ? <Alert showIcon type={copyFeedback.tone} message={copyFeedback.message} /> : null}
      </Space>
    );
  };

  const beginRestartProgress = (targetLabel: string) => {
    restartProgressTaskIdRef.current += 1;
    restartProgressCancelledRef.current = false;
    restartProgressAbortRef.current = new AbortController();
    setRestartProgress({
      targetLabel,
      stepLabel: '재기동 요청 전송 중',
      detail: '서버 명령 API에 재기동을 요청하고 있습니다.',
      cancelPending: false,
      cancellable: true,
    });
    return restartProgressTaskIdRef.current;
  };

  const updateRestartProgress = (
    taskId: number,
    stepLabel: string,
    detail: string,
    options?: { cancellable?: boolean; cancelPending?: boolean },
  ) => {
    if (restartProgressTaskIdRef.current !== taskId) {
      return;
    }

    setRestartProgress((current) => {
      if (!current) {
        return current;
      }

      return {
        ...current,
        stepLabel,
        detail,
        cancellable: options?.cancellable ?? current.cancellable,
        cancelPending: options?.cancelPending ?? current.cancelPending,
      };
    });
  };

  const isRestartProgressCancelled = (taskId: number) =>
    restartProgressTaskIdRef.current !== taskId || restartProgressCancelledRef.current;

  const closeRestartProgress = () => {
    setRestartProgress(null);
    restartProgressAbortRef.current = null;
    restartProgressCancelledRef.current = false;
  };

  const requestCancelRestartProgress = () => {
    if (!restartProgress || restartProgress.cancelPending || !restartProgress.cancellable) {
      return;
    }

    modalApi.confirm({
      title: '재기동 대기를 취소할까요?',
      content: '이미 접수된 서버 재기동 자체는 계속될 수 있습니다. 현재 화면의 대기와 마지막 새로고침만 중단합니다.',
      okText: '대기 취소',
      cancelText: '계속 진행',
      autoFocusButton: 'cancel',
      modalRender: renderModalWithEnterConfirm,
      onOk: () => {
        restartProgressCancelledRef.current = true;
        restartProgressAbortRef.current?.abort();
        setRestartProgress((current) =>
          current
            ? {
                ...current,
                stepLabel: '취소 처리 중',
                detail: '진행 중인 재기동 확인과 마지막 새로고침 안내를 중단하고 있습니다.',
                cancelPending: true,
                cancellable: false,
              }
            : current,
        );
      },
    });
  };

  const handleRegisterToken = () => {
    const trimmedToken = tokenInput.trim();

    if (!trimmedToken) {
      setTokenFeedback({ tone: 'warning', message: '등록할 토큰을 입력해 주세요.' });
      return;
    }

    if (trimmedToken !== ALLOWED_REGISTRATION_TOKEN) {
      setTokenFeedback({ tone: 'error', message: '허용되지 않은 토큰입니다.' });
      return;
    }

    setRegisteredAccessToken(trimmedToken);
    setTokenFeedback({ tone: 'success', message: '권한 토큰을 등록했습니다.' });
  };

  const handleClearToken = () => {
    setRegisteredAccessToken('');
    setTokenInput('');
    setTokenFeedback({ tone: 'info', message: '권한 토큰을 제거했습니다.' });
  };

  const handleClearActiveConversation = () => {
    const sessionId = activeConversationSnapshot.sessionId.trim();
    const conversationTitle = activeConversationSnapshot.title.trim();

    if (!sessionId) {
      return;
    }

    modalApi.confirm({
      title: '채팅방 데이터를 초기화할까요?',
      content: conversationTitle
        ? `"${conversationTitle}" 채팅방의 이름과 설정은 유지되고, 메시지·요청·활동 로그만 초기화됩니다.`
        : '채팅방 이름과 설정은 유지하고, 메시지·요청·활동 로그만 초기화됩니다.',
      okText: '초기화',
      cancelText: '취소',
      autoFocusButton: 'cancel',
      modalRender: renderModalWithEnterConfirm,
      okButtonProps: { danger: true },
      onOk: async () => {
        const clearedItem = await chatGateway.clearConversation(sessionId);
        emitChatConversationCleared(clearedItem);
        setSettingsOpen(false);

        void chatGateway
          .listConversations()
          .then((items) => {
            emitChatConversationsUpdated(items);
          })
          .catch(() => undefined);
      },
    });
  };

  const settingsTriggerButton = (
    <Button
      type="text"
      aria-label={`설정 · ${settingsStatusLabel}`}
      icon={
        <span className="app-header__settings-icon" aria-hidden="true">
          <SettingOutlined />
          <span className={`app-header__status-dot ${settingsStatusClassName}`} />
        </span>
      }
    />
  );

  const activeHeaderThemeOption = HEADER_THEME_OPTIONS.find((option) => option.key === headerTheme) ?? HEADER_THEME_OPTIONS[0];
  const renderHeaderHeightControls = ({
    label,
    description,
    value,
    min,
    max,
    onChange,
  }: {
    label: string;
    description: string;
    value: number;
    min: number;
    max: number;
    onChange: (nextValue: number) => void;
  }) => (
    <div className="app-header__theme-height-panel">
      <div className="app-header__theme-height-copy">
        <span className="app-header__theme-height-label">{label}</span>
        <span className="app-header__theme-height-meta">{description}</span>
      </div>
      <div className="app-header__theme-height-controls">
        <Button
          type="text"
          className="app-header__theme-height-step"
          onClick={() => onChange(value - 1)}
          aria-label={`${label} 1픽셀 줄이기`}
        >
          -1px
        </Button>
        <InputNumber
          min={min}
          max={max}
          step={1}
          value={value}
          controls={false}
          className="app-header__theme-height-input"
          addonAfter="px"
          onChange={(nextValue) => {
            if (typeof nextValue === 'number') {
              onChange(nextValue);
            }
          }}
          onStep={(nextValue) => onChange(nextValue)}
        />
        <Button
          type="text"
          className="app-header__theme-height-step"
          onClick={() => onChange(value + 1)}
          aria-label={`${label} 1픽셀 늘리기`}
        >
          +1px
        </Button>
      </div>
    </div>
  );

  const tokenTriggerButton = (
    <Button
      type="text"
      aria-label={`권한 · ${hasAccess ? '허용됨' : '차단됨'}`}
      icon={
        <span className="app-header__settings-icon" aria-hidden="true">
          <ProfileOutlined />
          <span
            className={`app-header__status-dot ${
              hasAccess ? 'app-header__status-dot--active' : 'app-header__status-dot--inactive'
            }`}
          />
        </span>
      }
      onClick={() => {
        openSettingsModal('token');
      }}
    />
  );

  const worklogAutomationPanel = (
    <Space direction="vertical" size={12} style={{ width: '100%' }}>
      <Alert
        showIcon
        type="info"
        message="업무일지 자동화 기본값을 설정합니다."
        description="현재는 앱 설정의 기준값을 저장하는 단계이며, 자동 생성/캡처 흐름에서 공통 기본값으로 사용하기 위한 설정입니다."
      />
      <Alert
        showIcon
        type={worklogAutomationSettingsDirty ? 'warning' : 'success'}
        message={
          worklogAutomationSettingsDirty
            ? 'DB 저장값과 편집 중인 업무일지 자동화 설정이 다릅니다.'
            : '업무일지 자동화 설정이 현재 DB 저장값과 같습니다.'
        }
        description={
          worklogAutomationSettingsDirty
            ? `변경 항목: ${worklogAutomationDiffLabels.join(', ')} / DB 저장값 기준: ${
                appConfig.worklogAutomation.autoCreateDailyWorklog ? '자동 생성 On' : '자동 생성 Off'
              }, 실행 시각 ${appConfig.worklogAutomation.dailyCreateTime}, 템플릿 ${
                appConfig.worklogAutomation.template === 'detailed' ? '상세형' : '간단형'
              } / 편집 중: ${
                appConfigDraft.worklogAutomation.autoCreateDailyWorklog ? '자동 생성 On' : '자동 생성 Off'
              }, 실행 시각 ${appConfigDraft.worklogAutomation.dailyCreateTime}, 템플릿 ${
                appConfigDraft.worklogAutomation.template === 'detailed' ? '상세형' : '간단형'
              }`
            : `DB 저장값 기준: ${
                appConfig.worklogAutomation.autoCreateDailyWorklog ? '자동 생성 On' : '자동 생성 Off'
              }, 실행 시각 ${appConfig.worklogAutomation.dailyCreateTime}, 템플릿 ${
                appConfig.worklogAutomation.template === 'detailed' ? '상세형' : '간단형'
              }`
        }
      />
      {appConfigFeedback ? (
        <Space direction="vertical" size={8} style={{ width: '100%' }}>
          <Alert
            showIcon
            type={appConfigFeedback.tone}
            message={appConfigFeedback.message}
            action={
              <Button
                type="text"
                size="small"
                aria-label="메시지 복사"
                icon={<CopyOutlined />}
                onClick={() => {
                  void copyTextToClipboard(appConfigFeedback.message)
                    .then(() => {
                      setAppConfigCopyFeedback({
                        tone: 'success',
                        message: '메시지를 복사했습니다.',
                      });
                    })
                    .catch(() => {
                      setAppConfigCopyFeedback({ tone: 'error', message: '메시지 복사에 실패했습니다.' });
                    });
                }}
              />
            }
          />
          {appConfigCopyFeedback ? (
            <Alert showIcon type={appConfigCopyFeedback.tone} message={appConfigCopyFeedback.message} />
          ) : null}
        </Space>
      ) : null}
      <Checkbox
        checked={appConfigDraft.worklogAutomation.autoCreateDailyWorklog}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            worklogAutomation: {
              ...current.worklogAutomation,
              autoCreateDailyWorklog: event.target.checked,
            },
          }));
        }}
      >
        일일 업무일지 파일 자동 생성
      </Checkbox>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>자동 생성 시각</Text>
        <Input
          type="time"
          value={appConfigDraft.worklogAutomation.dailyCreateTime}
          onChange={(event) => {
            updateAppConfigDraft((current) => ({
              ...current,
              worklogAutomation: {
                ...current.worklogAutomation,
                dailyCreateTime: event.target.value || DEFAULT_APP_CONFIG.worklogAutomation.dailyCreateTime,
              },
            }));
          }}
        />
        <Text type="secondary">설정한 시각 이후 서버 워커가 오늘자 업무일지 요청을 자동 등록합니다.</Text>
      </Space>
      <Text type="secondary">반복 등록은 별도 서버 작업으로 관리합니다.</Text>
      <Checkbox
        checked={appConfigDraft.worklogAutomation.includeScreenshots}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            worklogAutomation: {
              ...current.worklogAutomation,
              includeScreenshots: event.target.checked,
            },
          }));
        }}
      >
        자동 캡처 스크린샷 포함
      </Checkbox>
      <Checkbox
        checked={appConfigDraft.worklogAutomation.includeChangedFiles}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            worklogAutomation: {
              ...current.worklogAutomation,
              includeChangedFiles: event.target.checked,
            },
          }));
        }}
      >
        변경 파일 목록 자동 포함
      </Checkbox>
      <Checkbox
        checked={appConfigDraft.worklogAutomation.includeCommandLogs}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            worklogAutomation: {
              ...current.worklogAutomation,
              includeCommandLogs: event.target.checked,
            },
          }));
        }}
      >
        실행 커맨드 로그 자동 포함
      </Checkbox>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>기본 템플릿</Text>
        <Segmented
          block
          value={appConfigDraft.worklogAutomation.template}
          options={[
            { label: '상세형', value: 'detailed' },
            { label: '간단형', value: 'simple' },
          ]}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              worklogAutomation: {
                ...current.worklogAutomation,
                template: value as AppConfig['worklogAutomation']['template'],
              },
            }));
          }}
        />
        <Text type="secondary">
          상세형은 스크린샷, 소스, 실행 커맨드, 변경 파일 중심으로 남기고 간단형은 핵심 작업 요약 위주로 정리합니다.
        </Text>
      </Space>
      <Space wrap>
        <Button type="primary" onClick={handleSaveAppConfig} loading={appConfigSaving}>
          설정 저장
        </Button>
        <Button onClick={handleResetAppConfig}>추천값 불러오기</Button>
        <Button onClick={handleResetNotificationIdentity}>클라이언트 초기화</Button>
      </Space>
    </Space>
  );

  const chatSettingsPanel = (
    <Space direction="vertical" size={16} style={{ width: '100%' }}>
      <Alert
        type={chatSettingsDirty ? 'warning' : 'success'}
        showIcon
        message={chatSettingsDirty ? '채팅 문맥 설정에 저장되지 않은 변경이 있습니다.' : '채팅 문맥 설정이 저장되었습니다.'}
        description={
          chatSettingsDirty
            ? `변경 항목: ${chatSettingsDiffLabels.join(', ')} / DB 저장값 기준: 최근 ${appConfig.chat.maxContextMessages}개, ${appConfig.chat.maxContextChars}자, 최대 실행 ${appConfig.chat.codexLiveMaxExecutionSeconds}초, 무출력 실패 ${appConfig.chat.codexLiveIdleTimeoutSeconds}초, 채팅방 알림 ${appConfig.chat.receiveRoomNotifications ? '수신' : '차단'}, PWA 유도 ${appConfig.chat.guidePwaInstallForNotifications ? '사용' : '끔'}, 권한 유도 ${appConfig.chat.guideWebPushPermission ? '사용' : '끔'}, 등록 유도 ${appConfig.chat.guideWebPushRegistration ? '사용' : '끔'}, 재기동 예약 자동 실행 대기 ${appConfig.chat.restartReservationCompletionDelaySeconds}초 / 편집 중: 최근 ${appConfigDraft.chat.maxContextMessages}개, ${appConfigDraft.chat.maxContextChars}자, 최대 실행 ${appConfigDraft.chat.codexLiveMaxExecutionSeconds}초, 무출력 실패 ${appConfigDraft.chat.codexLiveIdleTimeoutSeconds}초, 채팅방 알림 ${appConfigDraft.chat.receiveRoomNotifications ? '수신' : '차단'}, PWA 유도 ${appConfigDraft.chat.guidePwaInstallForNotifications ? '사용' : '끔'}, 권한 유도 ${appConfigDraft.chat.guideWebPushPermission ? '사용' : '끔'}, 등록 유도 ${appConfigDraft.chat.guideWebPushRegistration ? '사용' : '끔'}, 재기동 예약 자동 실행 대기 ${appConfigDraft.chat.restartReservationCompletionDelaySeconds}초`
            : `현재 저장값: 최근 ${appConfig.chat.maxContextMessages}개 메시지, 최대 ${appConfig.chat.maxContextChars}자까지 채팅 문맥으로 참조하고 Codex Live 실행 시간은 최대 ${appConfig.chat.codexLiveMaxExecutionSeconds}초, 무출력 실패 시간은 ${appConfig.chat.codexLiveIdleTimeoutSeconds}초이며 채팅방 알림은 ${appConfig.chat.receiveRoomNotifications ? '수신' : '차단'} 상태, PWA 유도는 ${appConfig.chat.guidePwaInstallForNotifications ? '사용' : '끔'}, 권한 유도는 ${appConfig.chat.guideWebPushPermission ? '사용' : '끔'}, 등록 유도는 ${appConfig.chat.guideWebPushRegistration ? '사용' : '끔'}, 재기동 예약 자동 실행 대기 시간은 ${appConfig.chat.restartReservationCompletionDelaySeconds}초`
        }
      />

      <div>
        <Text strong>최근 문맥 메시지 수</Text>
        <Paragraph type="secondary">최근 대화 중 몇 개의 메시지를 채팅 문맥으로 참조할지 설정합니다.</Paragraph>
        <InputNumber
          min={1}
          max={50}
          value={appConfigDraft.chat.maxContextMessages}
          onChange={(value) => {
            setAppConfigDraft((current) => ({
              ...current,
              chat: {
                ...current.chat,
                maxContextMessages:
                  typeof value === 'number' && Number.isFinite(value)
                    ? Math.min(50, Math.max(1, Math.round(value)))
                    : DEFAULT_APP_CONFIG.chat.maxContextMessages,
              },
            }));
          }}
        />
      </div>

      <div>
        <Text strong>최근 문맥 글자 수</Text>
        <Paragraph type="secondary">최근 대화 전체에서 참조할 최대 글자 수입니다. 초과하면 전송 전에 확인 모달이 표시됩니다.</Paragraph>
        <InputNumber
          min={500}
          max={20000}
          step={100}
          value={appConfigDraft.chat.maxContextChars}
          onChange={(value) => {
            setAppConfigDraft((current) => ({
              ...current,
              chat: {
                ...current.chat,
                maxContextChars:
                  typeof value === 'number' && Number.isFinite(value)
                    ? Math.min(20_000, Math.max(500, Math.round(value)))
                    : DEFAULT_APP_CONFIG.chat.maxContextChars,
              },
            }));
          }}
        />
      </div>

      <div>
        <Checkbox
          checked={appConfigDraft.chat.receiveRoomNotifications}
          onChange={(event) => {
            setAppConfigDraft((current) => ({
              ...current,
              chat: {
                ...current.chat,
                receiveRoomNotifications: event.target.checked,
              },
            }));
          }}
        >
          채팅방 알림 수신
        </Checkbox>
        <Paragraph type="secondary" style={{ marginTop: 8, marginBottom: 0 }}>
          꺼두면 채팅방별 벨 설정과 관계없이 Codex Live 채팅방 알림을 이 기기에서 받지 않습니다.
        </Paragraph>
      </div>

      <div>
        <Checkbox
          checked={appConfigDraft.chat.guidePwaInstallForNotifications}
          onChange={(event) => {
            setAppConfigDraft((current) => ({
              ...current,
              chat: {
                ...current.chat,
                guidePwaInstallForNotifications: event.target.checked,
              },
            }));
          }}
        >
          PWA 설치 유도
        </Checkbox>
        <Paragraph type="secondary" style={{ marginTop: 8, marginBottom: 0 }}>
          브라우저 탭에서 알림 설정이 막히는 환경이면 홈 화면 앱으로 열도록 안내하는 개인 기본값입니다.
        </Paragraph>
      </div>

      <div>
        <Checkbox
          checked={appConfigDraft.chat.guideWebPushPermission}
          onChange={(event) => {
            setAppConfigDraft((current) => ({
              ...current,
              chat: {
                ...current.chat,
                guideWebPushPermission: event.target.checked,
              },
            }));
          }}
        >
          웹푸시 권한 유도
        </Checkbox>
        <Paragraph type="secondary" style={{ marginTop: 8, marginBottom: 0 }}>
          알림 권한이 미확인 상태면 브라우저 또는 기기 권한 허용을 먼저 안내하는 개인 기본값입니다.
        </Paragraph>
      </div>

      <div>
        <Checkbox
          checked={appConfigDraft.chat.guideWebPushRegistration}
          onChange={(event) => {
            setAppConfigDraft((current) => ({
              ...current,
              chat: {
                ...current.chat,
                guideWebPushRegistration: event.target.checked,
              },
            }));
          }}
        >
          웹푸시 등록 유도
        </Checkbox>
        <Paragraph type="secondary" style={{ marginTop: 8, marginBottom: 0 }}>
          권한은 허용됐지만 현재 기기 구독이 없으면 웹푸시 등록까지 이어서 안내하는 개인 기본값입니다.
        </Paragraph>
      </div>

      <div>
        <Text strong>Codex Live 최대 실행 시간(초)</Text>
        <Paragraph type="secondary">Codex Live 요청 1건이 강제 종료되기 전까지 허용할 최대 실행 시간입니다.</Paragraph>
        <InputNumber
          min={60}
          max={7200}
          step={30}
          value={appConfigDraft.chat.codexLiveMaxExecutionSeconds}
          onChange={(value) => {
            setAppConfigDraft((current) => ({
              ...current,
              chat: {
                ...current.chat,
                codexLiveMaxExecutionSeconds:
                  typeof value === 'number' && Number.isFinite(value)
                    ? Math.min(7200, Math.max(60, Math.round(value)))
                    : DEFAULT_APP_CONFIG.chat.codexLiveMaxExecutionSeconds,
              },
            }));
          }}
        />
      </div>

      <div>
        <Text strong>Codex Live 무출력 실패 시간(초)</Text>
        <Paragraph type="secondary">이 시간 동안 출력이나 활동 로그가 없으면 해당 요청을 실패 처리합니다.</Paragraph>
        <InputNumber
          min={30}
          max={3600}
          step={10}
          value={appConfigDraft.chat.codexLiveIdleTimeoutSeconds}
          onChange={(value) => {
            setAppConfigDraft((current) => ({
              ...current,
              chat: {
                ...current.chat,
                codexLiveIdleTimeoutSeconds:
                  typeof value === 'number' && Number.isFinite(value)
                    ? Math.min(3600, Math.max(30, Math.round(value)))
                    : DEFAULT_APP_CONFIG.chat.codexLiveIdleTimeoutSeconds,
              },
            }));
          }}
        />
      </div>

      <div>
        <Text strong>재기동 예약 자동 실행 대기 시간(초)</Text>
        <Paragraph type="secondary">
          예약 작업이 끝나면 자동화 확인 없이 이 시간 뒤 TEST/WORK 서버 재기동을 자동으로 시작합니다.
        </Paragraph>
        <InputNumber
          min={1}
          max={300}
          step={1}
          value={appConfigDraft.chat.restartReservationCompletionDelaySeconds}
          onChange={(value) => {
            setAppConfigDraft((current) => ({
              ...current,
              chat: {
                ...current.chat,
                restartReservationCompletionDelaySeconds:
                  typeof value === 'number' && Number.isFinite(value)
                    ? Math.min(300, Math.max(1, Math.round(value)))
                    : DEFAULT_APP_CONFIG.chat.restartReservationCompletionDelaySeconds,
              },
            }));
          }}
        />
      </div>
    </Space>
  );

  const planDefaultsPanel = (
    <Space direction="vertical" size={12} style={{ width: '100%' }}>
      <Alert
        showIcon
        type="info"
        message="작업 자동화 기본값을 설정합니다."
        description="새 작업 요청을 등록할 때 기본으로 채워지는 자동화 옵션입니다."
      />
      <Alert
        showIcon
        type={planDefaultSettingsDirty ? 'warning' : 'success'}
        message={
          planDefaultSettingsDirty
            ? 'DB 저장값과 편집 중인 자동화 기본값이 다릅니다.'
            : '자동화 기본값이 현재 DB 저장값과 같습니다.'
        }
        description={
          planDefaultSettingsDirty
            ? `변경 항목: ${planDefaultDiffLabels.join(', ')} / DB 저장값 기준: 기능동작확인 ${
                appConfig.planDefaults.jangsingProcessingRequired ? '완료' : '오동작'
              }, ${appConfig.planDefaults.autoDeployToMain ? '메인까지 자동등록' : 'release만 반영'}, 등록 후 편집기 ${
                appConfig.planDefaults.openEditorAfterCreate ? '열기' : '닫기'
              } / 편집 중: 기능동작확인 ${
                appConfigDraft.planDefaults.jangsingProcessingRequired ? '완료' : '오동작'
              }, ${
                appConfigDraft.planDefaults.autoDeployToMain ? '메인까지 자동등록' : 'release만 반영'
              }, 등록 후 편집기 ${appConfigDraft.planDefaults.openEditorAfterCreate ? '열기' : '닫기'}`
            : `DB 저장값 기준: 기능동작확인 ${
                appConfig.planDefaults.jangsingProcessingRequired ? '완료' : '오동작'
              }, ${appConfig.planDefaults.autoDeployToMain ? '메인까지 자동등록' : 'release만 반영'}, 등록 후 편집기 ${
                appConfig.planDefaults.openEditorAfterCreate ? '열기' : '닫기'
              }`
        }
      />
      {renderFeedback(appConfigFeedback, appConfigCopyFeedback, setAppConfigCopyFeedback)}
      <Checkbox
        checked={appConfigDraft.planDefaults.jangsingProcessingRequired}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            planDefaults: {
              ...current.planDefaults,
              jangsingProcessingRequired: event.target.checked,
            },
          }));
        }}
      >
        기능동작확인 기본값을 완료로 설정
      </Checkbox>
      <Checkbox
        checked={appConfigDraft.planDefaults.autoDeployToMain}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            planDefaults: {
              ...current.planDefaults,
              autoDeployToMain: event.target.checked,
            },
          }));
        }}
      >
        메인까지 자동등록
      </Checkbox>
      <Checkbox
        checked={appConfigDraft.planDefaults.openEditorAfterCreate}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            planDefaults: {
              ...current.planDefaults,
              openEditorAfterCreate: event.target.checked,
            },
          }));
        }}
      >
        새 항목 등록 후 편집기 자동 열기
      </Checkbox>
      <Space wrap>
        <Button type="primary" onClick={handleSaveAppConfig} loading={appConfigSaving}>
          설정 저장
        </Button>
        <Button onClick={handleResetAppConfig}>추천값 불러오기</Button>
      </Space>
    </Space>
  );

  const automationRuntimePanel = (
    <Space direction="vertical" size={12} style={{ width: '100%' }}>
      <Alert
        showIcon
        type="info"
        message="자동접수 워커 주기와 스케줄을 설정합니다."
        description="자동화 worker의 polling 주기와 등록된 요청을 언제 실행 큐로 넘길지 함께 조정합니다."
      />
      <Alert
        showIcon
        type={automationRuntimeSettingsDirty ? 'warning' : 'success'}
        message={
          automationRuntimeSettingsDirty
            ? 'DB 저장값과 편집 중인 자동접수 설정이 다릅니다.'
            : '자동접수 설정이 현재 DB 저장값과 같습니다.'
        }
        description={
          automationRuntimeSettingsDirty
            ? `변경 항목: ${automationRuntimeDiffLabels.join(', ')} / DB 저장값 기준: 워커 ${
                appConfig.automation.autoRefreshEnabled ? '실행' : '중지'
              }, ${appConfig.automation.autoRefreshIntervalSeconds}초 새로고침, 자동접수 ${formatAutomationRuntimeSchedule(
                appConfig.automation,
              )} / 편집 중: 워커 ${appConfigDraft.automation.autoRefreshEnabled ? '실행' : '중지'}, ${
                appConfigDraft.automation.autoRefreshIntervalSeconds
              }초 새로고침, 자동접수 ${formatAutomationRuntimeSchedule(appConfigDraft.automation)}`
            : `DB 저장값 기준: 워커 ${appConfig.automation.autoRefreshEnabled ? '실행' : '중지'}, ${
                appConfig.automation.autoRefreshIntervalSeconds
              }초 새로고침, 자동접수 ${formatAutomationRuntimeSchedule(appConfig.automation)}`
        }
      />
      {renderFeedback(appConfigFeedback, appConfigCopyFeedback, setAppConfigCopyFeedback)}
      <Checkbox
        checked={appConfigDraft.automation.autoRefreshEnabled}
        onChange={(event) => {
          updateAppConfigDraft((current) => ({
            ...current,
            automation: {
              ...current.automation,
              autoRefreshEnabled: event.target.checked,
            },
          }));
        }}
      >
        자동화 워커 실행 유지
      </Checkbox>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>워커 새로고침 주기(초)</Text>
        <InputNumber
          min={1}
          max={3600}
          step={1}
          style={{ width: '100%' }}
          value={appConfigDraft.automation.autoRefreshIntervalSeconds}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              automation: {
                ...current.automation,
                autoRefreshIntervalSeconds:
                  typeof value === 'number' && Number.isFinite(value)
                    ? Math.min(3600, Math.max(1, Math.round(value)))
                    : DEFAULT_APP_CONFIG.automation.autoRefreshIntervalSeconds,
              },
            }));
          }}
        />
        <Text type="secondary">Plan worker가 대기열, 자동접수, 후속 단계를 다시 확인하는 간격입니다.</Text>
      </Space>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>자동접수 스케줄</Text>
        <Select
          style={{ width: '100%' }}
          value={appConfigDraft.automation.autoReceiveScheduleType}
          options={[
            { value: 'interval', label: '간격 기준' },
            { value: 'daily', label: '매일' },
            { value: 'weekly', label: '매주' },
          ]}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              automation: {
                ...current.automation,
                autoReceiveScheduleType: value,
              },
            }));
          }}
        />
        {appConfigDraft.automation.autoReceiveScheduleType === 'interval' ? (
          <>
            <InputNumber
              min={1}
              max={3600}
              step={1}
              style={{ width: '100%' }}
              addonAfter="초"
              value={appConfigDraft.automation.autoReceiveIntervalSeconds}
              onChange={(value) => {
                updateAppConfigDraft((current) => ({
                  ...current,
                  automation: {
                    ...current.automation,
                    autoReceiveIntervalSeconds:
                      typeof value === 'number' && Number.isFinite(value)
                        ? Math.min(3600, Math.max(1, Math.round(value)))
                        : DEFAULT_APP_CONFIG.automation.autoReceiveIntervalSeconds,
                  },
                }));
              }}
            />
            <Text type="secondary">등록된 요청을 자동으로 집어가는 기준 간격입니다.</Text>
          </>
        ) : null}
        {appConfigDraft.automation.autoReceiveScheduleType === 'daily' ? (
          <>
            <Input
              type="time"
              value={appConfigDraft.automation.autoReceiveDailyTime}
              onChange={(event) => {
                updateAppConfigDraft((current) => ({
                  ...current,
                  automation: {
                    ...current.automation,
                    autoReceiveDailyTime: event.target.value || DEFAULT_APP_CONFIG.automation.autoReceiveDailyTime,
                  },
                }));
              }}
            />
            <Text type="secondary">매일 한 번 자동접수를 시작할 시각입니다.</Text>
          </>
        ) : null}
        {appConfigDraft.automation.autoReceiveScheduleType === 'weekly' ? (
          <>
            <Select
              style={{ width: '100%' }}
              value={appConfigDraft.automation.autoReceiveWeeklyDay}
              options={[
                { value: 'mon', label: '월요일' },
                { value: 'tue', label: '화요일' },
                { value: 'wed', label: '수요일' },
                { value: 'thu', label: '목요일' },
                { value: 'fri', label: '금요일' },
                { value: 'sat', label: '토요일' },
                { value: 'sun', label: '일요일' },
              ]}
              onChange={(value) => {
                updateAppConfigDraft((current) => ({
                  ...current,
                  automation: {
                    ...current.automation,
                    autoReceiveWeeklyDay: value,
                  },
                }));
              }}
            />
            <Input
              type="time"
              value={appConfigDraft.automation.autoReceiveWeeklyTime}
              onChange={(event) => {
                updateAppConfigDraft((current) => ({
                  ...current,
                  automation: {
                    ...current.automation,
                    autoReceiveWeeklyTime: event.target.value || DEFAULT_APP_CONFIG.automation.autoReceiveWeeklyTime,
                  },
                }));
              }}
            />
            <Text type="secondary">선택한 요일과 시각에 자동접수를 시작합니다.</Text>
          </>
        ) : null}
      </Space>
      <Space wrap>
        <Button type="primary" onClick={handleSaveAppConfig} loading={appConfigSaving}>
          설정 저장
        </Button>
        <Button onClick={handleResetAppConfig}>추천값 불러오기</Button>
      </Space>
    </Space>
  );

  const planCostPanel = (
    <Space direction="vertical" size={12} style={{ width: '100%' }}>
      <Alert
        showIcon
        type="info"
        message="자동화 비용 표시 기준을 설정합니다."
        description="진행 중 작업과 완료된 작업 모두 누적 토큰 사용량, 재처리 횟수, 종료 시각 기준 처리 시간을 반영해 예상 비용을 계산합니다. 재처리/시간 가산 비율과 상태 구간 기준값은 아래에서 직접 조정할 수 있습니다."
      />
      <Alert
        showIcon
        type={planCostSettingsDirty ? 'warning' : 'success'}
        message={
          planCostSettingsDirty ? 'DB 저장값과 편집 중인 비용 설정이 다릅니다.' : '비용 설정이 현재 DB 저장값과 같습니다.'
        }
        description={
          planCostSettingsDirty
            ? `변경 항목: ${planCostDiffLabels.join(', ')} / DB 저장값 기준: 백만 토큰당 ${formatCostAmount(
                appConfig.planCost.baseCostPerMillionTokens,
              )}, 재처리 ${appConfig.planCost.retryCostMultiplierPercent}%, ${formatPlanCostTimeMultiplierLabel(appConfig.planCost)}, 관심 ${appConfig.planCost.attentionCostThresholdMultiplier}배, 주의 ${appConfig.planCost.warningCostThresholdMultiplier}배, 높음 ${appConfig.planCost.highCostThresholdMultiplier}배 / 편집 중: 백만 토큰당 ${formatCostAmount(appConfigDraft.planCost.baseCostPerMillionTokens)}, 재처리 ${appConfigDraft.planCost.retryCostMultiplierPercent}%, ${formatPlanCostTimeMultiplierLabel(appConfigDraft.planCost)}, 관심 ${appConfigDraft.planCost.attentionCostThresholdMultiplier}배, 주의 ${appConfigDraft.planCost.warningCostThresholdMultiplier}배, 높음 ${appConfigDraft.planCost.highCostThresholdMultiplier}배`
            : `DB 저장값 기준: 백만 토큰당 ${formatCostAmount(appConfig.planCost.baseCostPerMillionTokens)}, 재처리 ${appConfig.planCost.retryCostMultiplierPercent}%, ${formatPlanCostTimeMultiplierLabel(appConfig.planCost)}, 관심 ${appConfig.planCost.attentionCostThresholdMultiplier}배, 주의 ${appConfig.planCost.warningCostThresholdMultiplier}배, 높음 ${appConfig.planCost.highCostThresholdMultiplier}배`
        }
      />
      {renderFeedback(appConfigFeedback, appConfigCopyFeedback, setAppConfigCopyFeedback)}
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>백만 토큰당 기준 비용</Text>
        <InputNumber
          min={100}
          max={1000000}
          step={100}
          style={{ width: '100%' }}
          addonAfter="원"
          value={appConfigDraft.planCost.baseCostPerMillionTokens}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              planCost: {
                ...current.planCost,
                baseCostPerMillionTokens: typeof value === 'number' ? value : current.planCost.baseCostPerMillionTokens,
              },
            }));
          }}
        />
        <Text type="secondary">
          예: {formatCostAmount(appConfigDraft.planCost.baseCostPerMillionTokens)} 기준이면 누적 토큰 100만개에서 재처리
          가산 전 기본 비용이 동일하게 계산됩니다.
        </Text>
      </Space>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>재처리 가산 비율</Text>
        <InputNumber
          min={0}
          max={500}
          step={1}
          style={{ width: '100%' }}
          addonAfter="%"
          value={appConfigDraft.planCost.retryCostMultiplierPercent}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              planCost: {
                ...current.planCost,
                retryCostMultiplierPercent:
                  typeof value === 'number' ? value : current.planCost.retryCostMultiplierPercent,
              },
            }));
          }}
        />
        <Text type="secondary">재처리 1회마다 기본 비용에 가산할 비율입니다.</Text>
      </Space>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>시간 가산 비율</Text>
        <Select
          style={{ width: '100%' }}
          value={appConfigDraft.planCost.timeCostUnit}
          options={[
            { value: 'hour', label: '시간 기준' },
            { value: 'minute', label: '분 기준' },
            { value: 'second', label: '초 기준' },
          ]}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              planCost: {
                ...current.planCost,
                timeCostUnit: value,
              },
            }));
          }}
        />
        <InputNumber
          min={0}
          max={500}
          step={1}
          style={{ width: '100%' }}
          addonAfter="%"
          value={appConfigDraft.planCost.hourlyCostMultiplierPercent}
          onChange={(value) => {
            updateAppConfigDraft((current) => ({
              ...current,
              planCost: {
                ...current.planCost,
                hourlyCostMultiplierPercent:
                  typeof value === 'number' ? value : current.planCost.hourlyCostMultiplierPercent,
              },
            }));
          }}
        />
        <Text type="secondary">
          종료 시각 기준 처리시간 {PLAN_COST_TIME_UNIT_LABELS[appConfigDraft.planCost.timeCostUnit]}마다 기본 비용에 가산할
          비율입니다.
        </Text>
      </Space>
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>상태 구간 기준 배수</Text>
        <Space direction={screens.sm ? 'horizontal' : 'vertical'} size={8} style={{ width: '100%' }}>
          <InputNumber
            min={0.1}
            max={100}
            step={0.1}
            style={{ width: screens.sm ? '33.33%' : '100%' }}
            addonBefore="관심"
            addonAfter="배"
            value={appConfigDraft.planCost.attentionCostThresholdMultiplier}
            onChange={(value) => {
              updateAppConfigDraft((current) => ({
                ...current,
                planCost: {
                  ...current.planCost,
                  attentionCostThresholdMultiplier:
                    typeof value === 'number' ? value : current.planCost.attentionCostThresholdMultiplier,
                },
              }));
            }}
          />
          <InputNumber
            min={0.1}
            max={100}
            step={0.1}
            style={{ width: screens.sm ? '33.33%' : '100%' }}
            addonBefore="주의"
            addonAfter="배"
            value={appConfigDraft.planCost.warningCostThresholdMultiplier}
            onChange={(value) => {
              updateAppConfigDraft((current) => ({
                ...current,
                planCost: {
                  ...current.planCost,
                  warningCostThresholdMultiplier:
                    typeof value === 'number' ? value : current.planCost.warningCostThresholdMultiplier,
                },
              }));
            }}
          />
          <InputNumber
            min={0.1}
            max={100}
            step={0.1}
            style={{ width: screens.sm ? '33.33%' : '100%' }}
            addonBefore="높음"
            addonAfter="배"
            value={appConfigDraft.planCost.highCostThresholdMultiplier}
            onChange={(value) => {
              updateAppConfigDraft((current) => ({
                ...current,
                planCost: {
                  ...current.planCost,
                  highCostThresholdMultiplier:
                    typeof value === 'number' ? value : current.planCost.highCostThresholdMultiplier,
                },
              }));
            }}
          />
        </Space>
        <Text type="secondary">기준 비용 대비 몇 배부터 관심, 주의, 높음 상태로 표시할지 설정합니다.</Text>
        <Text type="secondary">현재 편집값 기준: {getPlanCostThresholdPreview(appConfigDraft.planCost)}</Text>
      </Space>
      <Space wrap>
        <Button type="primary" onClick={handleSaveAppConfig} loading={appConfigSaving}>
          설정 저장
        </Button>
        <Button onClick={handleResetAppConfig}>추천값 불러오기</Button>
      </Space>
    </Space>
  );

  const automationNotificationPanel = (
    <Space direction="vertical" size={12} style={{ width: '100%' }}>
      <Alert
        showIcon
        type="info"
        message="자동화 알림을 항목별로 설정합니다."
        description="알림 사용이 켜진 기기에서만 Web Push를 받을 수 있습니다."
      />
      <Alert
        showIcon
        type={automationNotificationSettingsDirty ? 'warning' : 'success'}
        message={
          automationNotificationSettingsDirty
            ? 'DB 저장값과 편집 중인 자동화 알림 설정이 다릅니다.'
            : '자동화 알림 설정이 현재 DB 저장값과 같습니다.'
        }
        description={
          automationNotificationSettingsDirty
            ? `변경 항목: ${automationNotificationDiffLabels.join(', ')}`
            : `알림 항목: ${AUTOMATION_NOTIFICATION_OPTIONS.map((option) => option.label).join(', ')}`
        }
      />
      {appConfigFeedback ? (
        <Space direction="vertical" size={8} style={{ width: '100%' }}>
          <Alert
            showIcon
            type={appConfigFeedback.tone}
            message={appConfigFeedback.message}
            action={
              <Button
                type="text"
                size="small"
                aria-label="메시지 복사"
                icon={<CopyOutlined />}
                onClick={() => {
                  void copyTextToClipboard(appConfigFeedback.message)
                    .then(() => {
                      setAppConfigCopyFeedback({
                        tone: 'success',
                        message: '메시지를 복사했습니다.',
                      });
                    })
                    .catch(() => {
                      setAppConfigCopyFeedback({ tone: 'error', message: '메시지 복사에 실패했습니다.' });
                    });
                }}
              />
            }
          />
          {appConfigCopyFeedback ? (
            <Alert showIcon type={appConfigCopyFeedback.tone} message={appConfigCopyFeedback.message} />
          ) : null}
        </Space>
      ) : null}
      <Space direction="vertical" size={10} style={{ width: '100%' }}>
        {AUTOMATION_NOTIFICATION_OPTIONS.map((option) => (
          <Checkbox
            key={option.key}
            checked={appConfigDraft.automation[option.key]}
            onChange={(event) => {
              updateAppConfigDraft((current) => ({
                ...current,
                automation: {
                  ...current.automation,
                  [option.key]: event.target.checked,
                },
              }));
            }}
          >
            <Space direction="vertical" size={0}>
              <Text>{option.label}</Text>
              <Text type="secondary">{option.description}</Text>
            </Space>
          </Checkbox>
        ))}
      </Space>
      <Space wrap>
        <Button type="primary" onClick={handleSaveAppConfig} loading={appConfigSaving}>
          설정 저장
        </Button>
        <Button onClick={handleResetAppConfig}>추천값 불러오기</Button>
      </Space>
    </Space>
  );

  const gestureShortcutsPanel = (
    <Space direction="vertical" size={12} style={{ width: '100%' }}>
      <Alert
        showIcon
        type="info"
        message="모바일 제스처와 같은 액션을 키보드 단축키로도 실행합니다."
        description="저장 후 즉시 반영됩니다. `Mod`는 macOS에서는 Command, Windows/Linux에서는 Ctrl로 처리합니다."
      />
      <Alert
        showIcon
        type={gestureShortcutSettingsDirty ? 'warning' : 'success'}
        message={
          gestureShortcutSettingsDirty
            ? 'DB 저장값과 편집 중인 제스처 단축키 설정이 다릅니다.'
            : '제스처 단축키 설정이 현재 DB 저장값과 같습니다.'
        }
        description={
          gestureShortcutSettingsDirty
            ? `변경 항목: ${gestureShortcutDiffLabels.join(', ')} / DB 저장값 기준: 통합 검색 ${appConfig.gestureShortcuts.openSearch} / 편집 중: 통합 검색 ${appConfigDraft.gestureShortcuts.openSearch}`
            : `DB 저장값 기준: 통합 검색 ${appConfig.gestureShortcuts.openSearch}`
        }
      />
      {appConfigFeedback ? (
        <Space direction="vertical" size={8} style={{ width: '100%' }}>
          <Alert
            showIcon
            type={appConfigFeedback.tone}
            message={appConfigFeedback.message}
            action={
              <Button
                type="text"
                size="small"
                aria-label="메시지 복사"
                icon={<CopyOutlined />}
                onClick={() => {
                  void copyTextToClipboard(appConfigFeedback.message)
                    .then(() => {
                      setAppConfigCopyFeedback({
                        tone: 'success',
                        message: '메시지를 복사했습니다.',
                      });
                    })
                    .catch(() => {
                      setAppConfigCopyFeedback({ tone: 'error', message: '메시지 복사에 실패했습니다.' });
                    });
                }}
              />
            }
          />
          {appConfigCopyFeedback ? (
            <Alert showIcon type={appConfigCopyFeedback.tone} message={appConfigCopyFeedback.message} />
          ) : null}
        </Space>
      ) : null}
      <Space direction="vertical" size={6} style={{ width: '100%' }}>
        <Text strong>상단 우측 당기기 액션</Text>
        <Input
          value={appConfigDraft.gestureShortcuts.openSearch}
          placeholder={DEFAULT_APP_CONFIG.gestureShortcuts.openSearch}
          onChange={(event) => {
            updateAppConfigDraft((current) => ({
              ...current,
              gestureShortcuts: {
                ...current.gestureShortcuts,
                openSearch: event.target.value,
              },
            }));
          }}
        />
        <Text type="secondary">통합 검색을 엽니다. 예: `Mod+K`, `Alt+/`</Text>
      </Space>
      <Space wrap>
        <Button type="primary" onClick={handleSaveAppConfig} loading={appConfigSaving}>
          설정 저장
        </Button>
        <Button onClick={handleResetAppConfig}>추천값 불러오기</Button>
      </Space>
    </Space>
  );

  const headerAppearancePanel = (
    <Space direction="vertical" size={16} style={{ width: '100%' }}>
      <Paragraph type="secondary" style={{ marginBottom: 0 }}>
        헤더 테마와 높이는 현재 브라우저 기준으로 즉시 반영되며 로컬 저장소에 유지됩니다.
      </Paragraph>
      <div className="app-header__settings-theme-group">
        <div className="app-header__settings-theme-header">
          <span className="app-header__settings-theme-title">헤더 테마</span>
          <span className="app-header__settings-theme-meta">{activeHeaderThemeOption.label} 적용 중</span>
        </div>
        <div className="app-header__settings-theme-options">
          {HEADER_THEME_OPTIONS.map((option) => {
            const isActive = option.key === headerTheme;

            return (
              <button
                key={`header-appearance-${option.key}`}
                type="button"
                className={`app-header__theme-option app-header__theme-option--compact${
                  isActive ? ' app-header__theme-option--active' : ''
                }`}
                onClick={() => {
                  setHeaderTheme(option.key);
                }}
              >
                <span className="app-header__theme-option-icon" aria-hidden="true">
                  {option.icon}
                </span>
                <span className="app-header__theme-option-copy">
                  <span className="app-header__theme-option-label">{option.label}</span>
                  <span className="app-header__theme-option-description">{option.description}</span>
                </span>
                <span className="app-header__theme-option-state">{isActive ? '적용 중' : '선택'}</span>
              </button>
            );
          })}
        </div>
      </div>
      {renderHeaderHeightControls({
        label: '데스크톱 헤더 높이',
        description: '기본값은 60px이며, 상단 고정 헤더 높이를 현재 브라우저 기준으로 저장합니다.',
        value: desktopHeaderHeight,
        min: DESKTOP_HEADER_HEIGHT_MIN,
        max: DESKTOP_HEADER_HEIGHT_MAX,
        onChange: (nextValue) => setDesktopHeaderHeight(normalizeDesktopHeaderHeight(nextValue)),
      })}
      {renderHeaderHeightControls({
        label: '모바일 헤더 높이',
        description: '기본값은 36px이며, 모바일 폭에서만 적용되는 헤더 높이를 저장합니다.',
        value: mobileHeaderHeight,
        min: MOBILE_HEADER_HEIGHT_MIN,
        max: MOBILE_HEADER_HEIGHT_MAX,
        onChange: (nextValue) => setMobileHeaderHeight(normalizeMobileHeaderHeight(nextValue)),
      })}
      <Space wrap>
        <Button
          onClick={() => {
            setHeaderTheme('ocean');
            setDesktopHeaderHeight(DESKTOP_HEADER_HEIGHT_DEFAULT);
            setMobileHeaderHeight(MOBILE_HEADER_HEIGHT_DEFAULT);
          }}
        >
          추천값 불러오기
        </Button>
      </Space>
    </Space>
  );

  const settingsMenu = (
    <div className="app-header__settings-menu">
      {hasAccess ? (
        <div className="app-header__settings-group">
          <button
            type="button"
            className="app-header__settings-item"
            aria-expanded={automationGroupExpanded}
            onClick={() => {
              setAutomationGroupExpanded((current) => !current);
            }}
          >
            <span className="app-header__settings-icon">
              <ReloadOutlined />
              <span
                className={`app-header__status-dot ${
                  totalAutomationShortcutCount > 0
                    ? 'app-header__status-dot--warning'
                    : 'app-header__status-dot--inactive'
                }`}
                aria-hidden="true"
              />
            </span>
            <span className="app-header__settings-label">
              자동화 건수
              <Text type="secondary"> {totalAutomationShortcutCount}건</Text>
            </span>
            <span className="app-header__settings-group-arrow" aria-hidden="true">
              {automationGroupExpanded ? <DownOutlined /> : <RightOutlined />}
            </span>
          </button>
          {automationGroupExpanded ? (
            <div className="app-header__settings-group-children">
              <button
                type="button"
                className="app-header__settings-item app-header__settings-item--nested"
                onClick={() => {
                  setSettingsOpen(false);
                  setAutomationGroupExpanded(false);
                  onOpenPlanQuickFilter('working');
                }}
              >
                <span className="app-header__settings-icon">
                  <ProfileOutlined />
                  <span
                    className={`app-header__status-dot ${
                      planShortcutCounts.working > 0
                        ? 'app-header__status-dot--active'
                        : 'app-header__status-dot--inactive'
                    }`}
                    aria-hidden="true"
                  />
                </span>
                <span className="app-header__settings-label">
                  작업중 항목
                  <Text type="secondary"> {planShortcutCounts.working}건</Text>
                </span>
              </button>
              <button
                type="button"
                className="app-header__settings-item app-header__settings-item--nested"
                onClick={() => {
                  setSettingsOpen(false);
                  setAutomationGroupExpanded(false);
                  onOpenPlanQuickFilter('release-pending-main');
                }}
              >
                <span className="app-header__settings-icon">
                  <ProfileOutlined />
                  <span
                    className={`app-header__status-dot ${
                      planShortcutCounts.releasePendingMain > 0
                        ? 'app-header__status-dot--warning'
                        : 'app-header__status-dot--inactive'
                    }`}
                    aria-hidden="true"
                  />
                </span>
                <span className="app-header__settings-label">
                  release 상태 작업
                  <Text type="secondary"> {planShortcutCounts.releasePendingMain}건</Text>
                </span>
              </button>
              <button
                type="button"
                className="app-header__settings-item app-header__settings-item--nested"
                onClick={() => {
                  setSettingsOpen(false);
                  setAutomationGroupExpanded(false);
                  onOpenPlanQuickFilter('automation-failed');
                }}
              >
                <span className="app-header__settings-icon">
                  <ReloadOutlined />
                  <span
                    className={`app-header__status-dot ${
                      planShortcutCounts.automationFailed > 0
                        ? 'app-header__status-dot--warning'
                        : 'app-header__status-dot--inactive'
                    }`}
                    aria-hidden="true"
                  />
                </span>
                <span className="app-header__settings-label">
                  자동화 실패
                  <Text type="secondary"> {planShortcutCounts.automationFailed}건</Text>
                </span>
              </button>
            </div>
          ) : null}
        </div>
      ) : null}
      <button
        type="button"
        className="app-header__settings-item"
        disabled={!hasAccess}
        onClick={() => {
          openSettingsModal('appSettings', 'headerAppearance');
        }}
      >
        <span className="app-header__settings-icon">
          <SettingOutlined />
          <span className={`app-header__status-dot ${settingsStatusClassName}`} aria-hidden="true" />
        </span>
        <span className="app-header__settings-label">앱 설정</span>
      </button>
      <button
        type="button"
        className="app-header__settings-item"
        disabled={!hasAccess}
        onClick={() => {
          openSettingsModal('notification');
        }}
      >
        <span className="app-header__settings-icon">
          <BellOutlined />
          <span
            className={`app-header__status-dot ${notificationStatusClassName}`}
            aria-hidden="true"
          />
        </span>
        <span className="app-header__settings-label">알림</span>
      </button>
      <button
        type="button"
        className="app-header__settings-item"
        onClick={() => {
          openSettingsModal('token');
        }}
      >
        <span className="app-header__settings-icon">
          <ProfileOutlined />
          <span
            className={`app-header__status-dot ${
              hasAccess ? 'app-header__status-dot--active' : 'app-header__status-dot--inactive'
            }`}
            aria-hidden="true"
          />
        </span>
        <span className="app-header__settings-label">권한</span>
      </button>
      <button
        type="button"
        className="app-header__settings-item"
        onClick={() => {
          openSettingsModal('update');
        }}
      >
        <span className="app-header__settings-icon">
          <ReloadOutlined />
          <span className={`app-header__status-dot ${settingsStatusClassName}`} aria-hidden="true" />
        </span>
        <span className="app-header__settings-label">업데이트</span>
      </button>
      <button
        type="button"
        className="app-header__settings-item"
        disabled={!hasAccess || serverRestartReservationLoading || Boolean(serverRestartingKey)}
        onClick={() => {
          void handleToggleServerRestartReservation();
        }}
      >
        <span className="app-header__settings-icon">
          {serverRestartReservationLoading ? <ReloadOutlined spin /> : <ClockCircleOutlined />}
          <span className={`app-header__status-dot ${serverRestartReservationStatusClassName}`} aria-hidden="true" />
        </span>
        <span className="app-header__settings-copy">
          <span className="app-header__settings-label">{serverRestartReservationItemLabel}</span>
          <span className="app-header__settings-meta">{serverRestartReservationItemDescription}</span>
        </span>
      </button>
    </div>
  );

  return (
    <>
      {modalContextHolder}
      {serverRestartReservationOverlayState ? (
        <div className="app-header__restart-overlay" role="status" aria-live="assertive">
          <div className="app-header__restart-overlay-card">
            <span className="app-header__restart-overlay-eyebrow">RESTART RESERVATION</span>
            <strong className="app-header__restart-overlay-title">{serverRestartReservationOverlayState.title}</strong>
            <div className="app-header__restart-overlay-status">
              <LoadingOutlined />
              <span>{serverRestartReservationOverlayState.statusText}</span>
            </div>
            <p className="app-header__restart-overlay-detail">{serverRestartReservationOverlayState.detail}</p>
            <div className="app-header__restart-overlay-steps">
              {serverRestartReservationOverlayState.steps.map((step) => (
                <div
                  key={step.label}
                  className={`app-header__restart-overlay-step app-header__restart-overlay-step--${step.status}`}
                >
                  <span className="app-header__restart-overlay-step-dot" aria-hidden="true" />
                  <span>{step.label}</span>
                </div>
              ))}
            </div>
          </div>
        </div>
      ) : null}
      <Header className={`app-header ${appHeaderDomainClassName}`}>
        <Space size={12} className="app-header__row">
          <Space size={12} className="app-header__menu-side">
            <Button
              type="text"
              icon={sidebarCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
              onClick={onToggleSidebar}
            />
            <Segmented
              className="app-header__top-menu"
              value={headerTopMenu}
              options={headerTopMenuOptions}
              onChange={(value) => {
                onChangeTopMenu(value as 'docs' | 'plans' | 'play');
              }}
            />
          </Space>

          <Space size={4} className="app-header__actions">
            <Button
              type="text"
              aria-label={`통합 검색 열기 · ${searchShortcutLabel}`}
              title={isMobileViewport ? undefined : `통합 검색 열기 (${searchShortcutLabel})`}
              icon={<SearchOutlined />}
              onClick={onOpenSearch}
            />
            {hasAccess ? (
              <>
                <button
                  type="button"
                  className={`${connectionIndicatorClassName} app-header__connection-indicator--labelled`}
                  aria-label={chatConnectionLabel}
                  title={isMobileViewport ? undefined : chatConnectionLabel}
                  onClick={() => {
                    setIsRuntimeModalOpen(true);
                  }}
                >
                  <span className="app-header__settings-icon" aria-hidden="true">
                    <ApiOutlined />
                    <span className={`app-header__status-dot ${chatConnectionStatusClassName}`} />
                  </span>
                  <span className="app-header__connection-copy">
                    <span className="app-header__connection-title">트랜잭션</span>
                    <span className="app-header__connection-meta">
                      {hasPendingRuntimeWork ? `실행 ${runningRuntimeCount} · 대기 ${queuedRuntimeCount}` : '바로 열기'}
                    </span>
                  </span>
                  {runningRuntimeCount > 0 ? (
                    <span
                      className={connectionCountBadgeClassName}
                      aria-label={`실행 중 요청 ${runningRuntimeCount}건`}
                      title={isMobileViewport ? undefined : `실행 중 요청 ${runningRuntimeCount}건`}
                    >
                      {runningRuntimeBadgeLabel}
                    </span>
                  ) : null}
                </button>
                <HeaderMessageCenter isMobileViewport={isMobileViewport} />
                <Dropdown
                  trigger={['click']}
                  open={settingsOpen}
                  onOpenChange={setSettingsOpen}
                  placement="bottomRight"
                  popupRender={() => settingsMenu}
                >
                  {settingsTriggerButton}
                </Dropdown>
              </>
            ) : (
              tokenTriggerButton
            )}
          </Space>
        </Space>
      </Header>

      <Modal
        title="Codex 런타임"
        open={isRuntimeModalOpen}
        footer={null}
        onCancel={() => {
          setIsRuntimeModalOpen(false);
        }}
      >
        <Space direction="vertical" size={12} style={{ width: '100%' }}>
          <div className="app-header__runtime-summary">
            <div className="app-header__runtime-summary-card" title="실행 중 요청">
              <LoadingOutlined />
              <Text type="secondary">실행</Text>
              <Text strong>{runningRuntimeCount}</Text>
            </div>
            <div className="app-header__runtime-summary-card" title="대기 요청">
              <ClockCircleOutlined />
              <Text type="secondary">대기</Text>
              <Text strong>{queuedRuntimeCount}</Text>
            </div>
            <div className="app-header__runtime-summary-card" title="활성 세션">
              <ProfileOutlined />
              <Text type="secondary">활성</Text>
              <Text strong>{runtimeSessionCount}</Text>
            </div>
          </div>
          <Alert
            type={
              chatConnection.connectionState === 'connected'
                ? 'success'
                : chatConnection.connectionState === 'connecting'
                  ? 'info'
                  : 'warning'
            }
            showIcon
            message={
              chatConnection.connectionState === 'connected'
                ? '웹소켓으로 실시간 반영 중입니다.'
                : chatConnection.connectionState === 'connecting'
                  ? '웹소켓을 다시 연결하는 중입니다.'
                  : '웹소켓 연결이 끊겨 최신 상태가 지연될 수 있습니다.'
            }
            description={
              chatRuntimeSnapshot?.generatedAt
                ? `마지막 반영 ${formatRuntimeRelativeLabel(chatRuntimeSnapshot.generatedAt)}`
                : '아직 수신된 런타임 정보가 없습니다.'
            }
          />
          <div className="app-header__runtime-list">
            <Text strong>실행 중 요청</Text>
            {chatRuntimeSnapshot && chatRuntimeSnapshot.running.length > 0 ? (
              chatRuntimeSnapshot.running.map((item) => (
                <div key={item.requestId} className="app-header__runtime-list-item">
                  <div className="app-header__runtime-list-row">
                    <div className="app-header__runtime-list-copy">
                      <Text strong>{getConversationLabel(item.sessionId)}</Text>
                      <Text className="app-header__runtime-summary-text">{item.summary || '요약 없음'}</Text>
                      <div className="app-header__runtime-meta">
                        <span className="app-header__runtime-badge">
                          <LoadingOutlined />
                          실행
                        </span>
                        <span className="app-header__runtime-badge">{formatRuntimeRelativeLabel(item.startedAt)}</span>
                      </div>
                    </div>
                    <Space size={8} wrap>
                      <Button
                        size="small"
                        type="default"
                        onClick={() => {
                          void openRuntimeLog(item.requestId);
                        }}
                      >
                        로그 보기
                      </Button>
                      <Button size="small" type="default" onClick={() => navigateToConversation(item.sessionId)}>
                        이동
                      </Button>
                    </Space>
                  </div>
                </div>
              ))
            ) : (
              <Text type="secondary">현재 실행 중인 요청이 없습니다.</Text>
            )}
          </div>
          <div className="app-header__runtime-list">
            <Text strong>대기 요청</Text>
            {chatRuntimeSnapshot && chatRuntimeSnapshot.queued.length > 0 ? (
              chatRuntimeSnapshot.queued.map((item) => (
                <div key={item.requestId} className="app-header__runtime-list-item">
                  <div className="app-header__runtime-list-row">
                    <div className="app-header__runtime-list-copy">
                      <Text strong>{getConversationLabel(item.sessionId)}</Text>
                      <Text className="app-header__runtime-summary-text">{item.summary || '요약 없음'}</Text>
                      <div className="app-header__runtime-meta">
                        <span className="app-header__runtime-badge">
                          <ClockCircleOutlined />
                          대기
                        </span>
                        <span className="app-header__runtime-badge">{formatRuntimeRelativeLabel(item.enqueuedAt)}</span>
                      </div>
                    </div>
                    <Button size="small" type="default" onClick={() => navigateToConversation(item.sessionId)}>
                      이동
                    </Button>
                  </div>
                </div>
              ))
            ) : (
              <Text type="secondary">현재 대기 중인 요청이 없습니다.</Text>
            )}
          </div>
          <div className="app-header__runtime-list">
            <Text strong>최근 작업</Text>
            {chatRuntimeSnapshot && chatRuntimeSnapshot.recent.length > 0 ? (
              chatRuntimeSnapshot.recent.map((item) => (
                <div key={`recent-${item.requestId}-${item.lastUpdatedAt}`} className="app-header__runtime-list-item">
                  <div className="app-header__runtime-list-row">
                    <div className="app-header__runtime-list-copy">
                      <Text strong>{getConversationLabel(item.sessionId)}</Text>
                      <Text className="app-header__runtime-summary-text">{item.summary || '요약 없음'}</Text>
                      <div className="app-header__runtime-meta">
                        <span className="app-header__runtime-badge">{buildRuntimeTerminalLabel(item.terminalStatus)}</span>
                        <span className="app-header__runtime-badge">
                          {formatRuntimeRelativeLabel(item.lastUpdatedAt)}
                        </span>
                      </div>
                    </div>
                    <Space size={8} wrap>
                      <Button
                        size="small"
                        type="default"
                        onClick={() => {
                          void openRuntimeLog(item.requestId);
                        }}
                      >
                        로그 보기
                      </Button>
                      <Button size="small" type="default" onClick={() => navigateToConversation(item.sessionId)}>
                        이동
                      </Button>
                    </Space>
                  </div>
                </div>
              ))
            ) : (
              <Text type="secondary">최근 종료된 작업이 없습니다.</Text>
            )}
          </div>
        </Space>
      </Modal>

      <Drawer
        open={isRuntimeLogDrawerOpen}
        title="실행 로그"
        placement="right"
        width="100vw"
        onClose={() => {
          setIsRuntimeLogDrawerOpen(false);
          setRuntimeLogDetail(null);
          setRuntimeLogError('');
        }}
        styles={{
          body: {
            padding: 24,
          },
        }}
      >
        {runtimeLogLoading ? (
          <Text type="secondary">로그를 불러오는 중입니다.</Text>
        ) : runtimeLogError ? (
          <Text type="danger">{runtimeLogError}</Text>
        ) : runtimeLogDetail ? (
          <Space direction="vertical" size={12} style={{ width: '100%' }}>
            {runtimeLogDetail.item?.sessionId ? (
              <Button
                size="small"
                type="default"
                style={{ alignSelf: 'flex-start' }}
                onClick={() => {
                  navigateToConversation(runtimeLogDetail.item?.sessionId ?? '');
                  setIsRuntimeLogDrawerOpen(false);
                }}
              >
                채팅방 이동
              </Button>
            ) : null}
            <div className="app-chat-runtime__job-meta">
              <Text type="secondary">요청: {runtimeLogDetail.item?.requestId ?? '-'}</Text>
              <Text type="secondary">세션: {runtimeLogDetail.item?.sessionId ?? '-'}</Text>
              <Text type="secondary">마지막 갱신: {formatRuntimeTimestamp(runtimeLogDetail.lastUpdatedAt)}</Text>
              <Text type="secondary">
                종료 상태: {buildRuntimeTerminalLabel(runtimeLogDetail.terminalStatus)}
              </Text>
            </div>
            <Paragraph className="app-chat-runtime__job-summary">
              {runtimeLogDetail.item?.summary ?? '요약 없음'}
            </Paragraph>
            <pre className="app-chat-runtime__log-viewer">
              {runtimeLogDetail.logs.length > 0 ? runtimeLogDetail.logs.join('\n') : '아직 기록된 로그가 없습니다.'}
            </pre>
          </Space>
        ) : (
          <Text type="secondary">표시할 로그가 없습니다.</Text>
        )}
      </Drawer>

      <Modal
        title={getSettingsModalTitle(activeSettingsModal)}
        open={settingsModalOpen}
        footer={null}
        confirmLoading={notificationLoading}
        width={activeSettingsModal === 'update' && isMobileViewport ? 'calc(100vw - 16px)' : undefined}
        styles={
          activeSettingsModal === 'update'
            ? {
                body: {
                  padding: isMobileViewport ? 16 : 24,
                },
              }
            : undefined
        }
        onCancel={() => {
          setSettingsModalOpen(false);
        }}
      >
        <Space direction="vertical" size={12} style={{ width: '100%' }}>
          {activeSettingsModal === 'appSettings' ? (
            <>
              <Segmented
                block
                value={activeAppSettingsCategory}
                options={[...APP_SETTINGS_CATEGORIES]}
                onChange={(value) => {
                  setActiveAppSettingsCategory(value as AppSettingsCategoryKey);
                }}
              />
              <Select
                value={activeAppSettingsSection}
                style={{ width: '100%' }}
                options={activeAppSettingsSectionOptions}
                onChange={(value) => {
                  setActiveAppSettingsSection(value);
                }}
              />
              {activeAppSettingsSection === 'automationRuntime' ? automationRuntimePanel : null}
              {activeAppSettingsSection === 'planDefaults' ? planDefaultsPanel : null}
              {activeAppSettingsSection === 'planCost' ? planCostPanel : null}
              {activeAppSettingsSection === 'headerAppearance' ? headerAppearancePanel : null}
              {activeAppSettingsSection === 'chatSettings' ? chatSettingsPanel : null}
              {activeAppSettingsSection === 'worklogAutomation' ? worklogAutomationPanel : null}
              {activeAppSettingsSection === 'automationNotifications' ? automationNotificationPanel : null}
              {activeAppSettingsSection === 'gestureShortcuts' ? gestureShortcutsPanel : null}
            </>
          ) : null}
          {activeSettingsModal === 'notification' ? (
            <Space direction="vertical" size={12} style={{ width: '100%' }}>
              <Paragraph type="secondary" style={{ marginBottom: 0 }}>
                알림을 `On`으로 바꾸면 현재 기기의 브라우저 권한을 확인한 뒤 Web Push 구독을 생성하고 서버에 자동 등록합니다.
              </Paragraph>
              <Text type="secondary">클라이언트 알림 권한 상태: {getClientNotificationPermissionLabel(clientNotificationPermission)}</Text>
              <Text type="secondary">PWA 실행 상태: {isStandaloneMode ? '홈 화면 앱' : '브라우저 탭'}</Text>
              <Text type="secondary">서버 Web Push 설정: {webPushConfigured ? '준비됨' : '미설정'}</Text>
              {isAppleMobileDevice() && !isStandaloneMode ? (
                <Text type="warning">아이폰에서는 홈 화면에 추가한 뒤 연 PWA에서만 웹 푸시를 사용할 수 있습니다.</Text>
              ) : null}
              {clientNotificationPermission === 'denied' ? (
                <Text type="warning">브라우저 또는 기기 설정에서 이 사이트의 알림 권한을 허용한 뒤 다시 시도해 주세요.</Text>
              ) : null}
              {clientNotificationPermission === 'unsupported' ? (
                <Text type="warning">현재 환경에서는 Web Push 알림 등록을 지원하지 않습니다.</Text>
              ) : null}
              {isPreviewRuntime() ? (
                <Text type="warning">미리보기 런타임에서는 서비스워커를 유지하지 않아 알림 등록을 사용할 수 없습니다.</Text>
              ) : null}
              {!hasSecureOrigin() ? (
                <Text type="warning">알림은 HTTPS 또는 localhost 환경에서만 사용할 수 있습니다.</Text>
              ) : null}
              {renderFeedback(notificationFeedback, notificationCopyFeedback, setNotificationCopyFeedback)}
              <Checkbox
                checked={notificationEnabled}
                disabled={notificationLoading || isPreviewRuntime()}
                onChange={(event) => {
                  void syncNotificationEnabled(event.target.checked);
                }}
              >
                알림 사용
              </Checkbox>
            </Space>
          ) : null}
          {activeSettingsModal === 'token' ? (
            <Space direction="vertical" size={6} style={{ width: '100%' }}>
              <Text strong>토큰 관리</Text>
              <Text type="secondary">
                관리자에게 전달받은 권한 토큰을 등록한 사용자만 Codex 채팅과 새 메모 등록을 사용할 수 있습니다.
              </Text>
              <Text type="secondary">권한 상태: {hasAccess ? '허용됨' : '차단됨'}</Text>
              <Input.Password
                value={tokenInput}
                placeholder="권한 토큰 입력"
                onChange={(event) => {
                  setTokenInput(event.target.value);
                }}
              />
              {tokenFeedback ? <Alert showIcon type={tokenFeedback.tone} message={tokenFeedback.message} /> : null}
              <Space wrap>
                <Button type="primary" onClick={handleRegisterToken}>
                  토큰 등록
                </Button>
                <Button onClick={handleClearToken} disabled={!registeredToken}>
                  토큰 제거
                </Button>
              </Space>
            </Space>
          ) : null}
          {activeSettingsModal === 'update' ? (
            <div className="app-header__server-management">
              <div className="app-header__server-management-hero">
                <div className="app-header__server-management-hero-copy">
                  <Text strong>서버 재기동</Text>
                  <Text type="secondary">핵심 작업을 먼저 두고 상태는 한 화면에서 바로 확인하게 정리했습니다.</Text>
                </div>
                <Button
                  icon={
                    workServerStatusLoading || serverRestartReservationLoading ? <ReloadOutlined spin /> : <ReloadOutlined />
                  }
                  loading={workServerStatusLoading || serverRestartReservationLoading}
                  disabled={!canRefreshWorkServerStatus}
                  onClick={() => {
                    void refreshServerManagementStatus();
                  }}
                >
                  상태 새로고침
                </Button>
              </div>
              <div className="app-header__server-management-status-grid">
                {serverManagementStatusItems.map((statusItem) => (
                  <div key={statusItem.key} className="app-header__server-management-status-card">
                    <div className="app-header__server-management-status-head">
                      <Text strong>{statusItem.label}</Text>
                      <span
                        className={`app-header__server-version-indicator ${getServerVersionStatusClassName(statusItem.item)}`}
                        aria-label={getServerVersionStatusTitle(statusItem.item, statusItem.label)}
                        title={getServerVersionStatusTitle(statusItem.item, statusItem.label)}
                        aria-hidden="true"
                      />
                    </div>
                    <Text type="secondary">{statusItem.checkedLabel}</Text>
                    <Text type="secondary">{statusItem.detailLabel}</Text>
                    {statusItem.progressLabel ? <Text type="secondary">진행상태 {statusItem.progressLabel}</Text> : null}
                    {statusItem.hint ? <Text type="secondary">{statusItem.hint}</Text> : null}
                  </div>
                ))}
              </div>
              {workServerStatus?.buildRequired ? (
                <Alert
                  showIcon
                  type="warning"
                  message="WORK 빨간 점은 장애가 아니라 빌드 미반영 상태입니다. 워크 재기동만으로는 그대로 남을 수 있습니다."
                />
              ) : null}
              {!hasAccess ? (
                <Alert showIcon type="warning" message="서버 재기동은 권한 토큰 등록 후 사용할 수 있습니다." />
              ) : null}
              {renderFeedback(updateCheckFeedback, updateCheckCopyFeedback, setUpdateCheckCopyFeedback)}
              {renderFeedback(serverRestartFeedback, serverRestartCopyFeedback, setServerRestartCopyFeedback)}
              <div className="app-header__server-management-action-group">
                <Text strong>즉시 실행</Text>
                <div className="app-header__server-management-action-grid">
                  <Button
                    block
                    icon={serverRestartingKey === 'test' ? <ReloadOutlined spin /> : <ReloadOutlined />}
                    loading={serverRestartingKey === 'test'}
                    disabled={!canRestartServers}
                    onClick={() => {
                      void handleRestartSingleServer('test');
                    }}
                  >
                    테스트 재기동
                  </Button>
                  <Button
                    block
                    icon={serverRestartingKey === 'work-server' ? <ReloadOutlined spin /> : <ReloadOutlined />}
                    loading={serverRestartingKey === 'work-server'}
                    disabled={!canRestartServers}
                    onClick={() => {
                      void handleRestartSingleServer('work-server');
                    }}
                  >
                    워크 재기동
                  </Button>
                  <Button
                    type="primary"
                    block
                    className="app-header__server-management-action-grid-primary"
                    icon={serverRestartingKey === 'all' ? <ReloadOutlined spin /> : <ReloadOutlined />}
                    loading={serverRestartingKey === 'all' || serverRestartReservationLoading}
                    disabled={!canRestartServers || serverRestartReservationLoading}
                    onClick={() => {
                      void handleScheduleServerRestartReservation('all');
                    }}
                  >
                    전체 재기동 예약
                  </Button>
                </div>
                <Text type="secondary">전체 재기동은 TEST와 WORK 서버만 순서대로 예약 실행합니다.</Text>
              </div>
              <div className="app-header__server-management-action-group">
                <Text strong>운영 반영</Text>
                <Text type="secondary">
                  PROD 컨테이너는 전체 재기동에 포함하지 않고, 별도 확인 후 빌드와 재기동을 진행합니다.
                </Text>
                <Button
                  type="primary"
                  danger
                  block
                  icon={serverRestartingKey === 'prod' ? <ReloadOutlined spin /> : <ReloadOutlined />}
                  loading={serverRestartingKey === 'prod'}
                  disabled={!canRestartServers}
                  onClick={handleConfirmRestartProdServer}
                >
                  PROD 빌드 반영
                </Button>
              </div>
            </div>
          ) : null}
        </Space>
      </Modal>
      <Modal
        title={restartProgress ? `${restartProgress.targetLabel} 재기동 진행 중` : '서버 재기동 진행 중'}
        open={Boolean(restartProgress)}
        closable={false}
        keyboard={false}
        maskClosable={false}
        footer={[
          <Button
            key="cancel"
            onClick={requestCancelRestartProgress}
            disabled={!restartProgress?.cancellable || restartProgress.cancelPending}
          >
            {restartProgress?.cancelPending ? '취소 처리 중' : '취소'}
          </Button>,
        ]}
      >
        {restartProgress ? (
          <Space direction="vertical" size={12} style={{ width: '100%' }}>
            <Space align="center">
              <LoadingOutlined />
              <Text strong>{restartProgress.stepLabel}</Text>
            </Space>
            <Text type="secondary">{restartProgress.detail}</Text>
            <Alert
              showIcon
              type={restartProgress.cancelPending ? 'warning' : 'info'}
              message={
                restartProgress.cancelPending
                  ? '취소 요청을 처리 중입니다. 이미 접수된 서버 재기동은 계속될 수 있습니다.'
                  : '다른 조작은 막고 실제 서버 부팅 완료까지 확인한 뒤 마지막 새로고침 여부를 묻습니다.'
              }
            />
          </Space>
        ) : null}
      </Modal>
    </>
  );
}
