import {
  AppstoreOutlined,
  ArrowLeftOutlined,
  ArrowRightOutlined,
  BorderOutlined,
  CheckOutlined,
  CheckSquareOutlined,
  CloseOutlined,
  CloseCircleOutlined,
  DownOutlined,
  DownloadOutlined,
  EllipsisOutlined,
  EyeOutlined,
  FolderOpenOutlined,
  FunnelPlotOutlined,
  HeartFilled,
  HeartOutlined,
  LogoutOutlined,
  MinusSquareOutlined,
  PlayCircleFilled,
  PictureOutlined,
  ReloadOutlined,
  SearchOutlined,
  SettingOutlined,
  ShareAltOutlined,
  UnorderedListOutlined,
} from '@ant-design/icons';
import { Button, Dropdown, Empty, Form, Input, Segmented, Select, Spin, Tooltip, message } from 'antd';
import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type MouseEvent as ReactMouseEvent,
  type ReactNode,
  type SyntheticEvent,
  type TouchEvent as ReactTouchEvent,
} from 'react';
import { createPortal } from 'react-dom';
import { FullscreenPreviewModal, ZoomablePreviewSurface } from '../../../../components/previewer';
import { clearChatActionContext, publishChatActionContext } from '../../../../app/main/chatActionContextStore';
import './PhotoPrismAppView.css';
import {
  buildPhotoPrismViewerUrl,
  buildPhotoPrismDownloadUrl,
  buildPhotoPrismThumbUrl,
  createPhotoPrismSession,
  fetchPhotoPrismAlbums,
  fetchPhotoPrismPhotoDetail,
  fetchPhotoPrismPhotos,
  getPhotoDisplayDate,
  getPhotoDisplayTitle,
  getPrimaryPhotoFile,
  isPhotoPrismVideo,
  updatePhotoPrismPhotoAlbumMembership,
  updatePhotoPrismPhotoFavorite,
  type PhotoPrismAlbum,
  type PhotoPrismFile,
  type PhotoPrismPhoto,
  type PhotoPrismSearchMeta,
  type PhotoPrismStoredSession,
} from './photoPrismApi';

type PhotoPrismAppViewProps = {
  onBack: () => void;
  launchContext?: 'direct' | 'embedded';
};

type PreviewMode = 'grid' | 'masonry' | 'list';
type DensityMode = 'compact' | 'comfortable' | 'large';
type ViewerDisplayTabKey = 'photo' | 'info';
type ViewerThemePreset =
  | 'porcelain'
  | 'skyline'
  | 'sage'
  | 'sunset'
  | 'blossom'
  | 'aurora'
  | 'midnight'
  | 'graphite'
  | 'ember'
  | 'moss';
type PhotoSortOrder = 'newest' | 'oldest';
type PhotoPrismFavoriteFilter = 'all' | 'true';
type PhotoPrismMediaTypeFilter = 'all' | 'image' | 'video';
type PhotoPrismThemePreset =
  | 'amber'
  | 'ocean'
  | 'forest'
  | 'berry'
  | 'graphite'
  | 'coral'
  | 'cobalt'
  | 'mint'
  | 'sunflower'
  | 'cedar'
  | 'ruby'
  | 'sky'
  | 'indigo'
  | 'cocoa'
  | 'slate';
type MobileFilterSectionKey = 'album' | 'date' | 'tag' | 'meta' | 'theme' | 'view' | 'order' | null;
type PhotoPrismAdvancedFilters = {
  year: string;
  month: string;
  day: string;
  label: string[];
  category: string[];
  favorite: PhotoPrismFavoriteFilter;
  mediaType: PhotoPrismMediaTypeFilter;
  country: string;
  camera: string;
};
type PhotoPrismDisplaySettings = {
  previewMode?: PreviewMode;
  densityMode?: DensityMode;
};

const STORAGE_KEY = 'play-app.photoprism.session';
const DISPLAY_SETTINGS_STORAGE_KEY = 'play-app.photoprism.display-settings';
const ALL_ALBUM_ID = '__all__';
const MOBILE_MEDIA_QUERY = '(max-width: 768px)';
const LIST_THUMB_SIZE = 'fit_720';
const DETAIL_VIEW_SIZE = 'fit_1280';
const PHOTO_PAGE_SIZE = 180;
const PRELOAD_TRIGGER_MIN_REMAINING = 24;
const PRELOAD_TRIGGER_MAX_REMAINING = 60;
const LONG_PRESS_MS = 380;
const LONG_PRESS_MOVE_TOLERANCE = 16;
const DRAG_SCROLL_EDGE_PX = 96;
const DRAG_SCROLL_MIN_SPEED = 6;
const DRAG_SCROLL_MAX_SPEED = 42;
const DRAG_SCROLL_DWELL_ACCEL_MS = 520;
const DRAG_SCROLL_DOWNWARD_BIAS = 1.18;
const PLACEHOLDER_CARD_COUNT = 18;
const MOBILE_SHARE_FILE_LIMIT = 12;
const MOBILE_SHARE_TOTAL_BYTES_LIMIT = 120 * 1024 * 1024;
const VIEWER_ACTIONS_AUTO_HIDE_MS = 2200;
const THEME_STORAGE_KEY = 'play-app.photoprism.theme';
const VIEWER_THEME_STORAGE_KEY = 'play-app.photoprism.viewer-theme';
const PULL_TO_REFRESH_START_THRESHOLD_PX = 14;
const PULL_TO_REFRESH_TRIGGER_VISIBLE_PX = 86;
const PULL_TO_REFRESH_MAX_VISIBLE_PX = 96;
const PULL_TO_REFRESH_DAMPING = 0.34;
const PULL_TO_REFRESH_HORIZONTAL_CANCEL_PX = 24;
const PULL_TO_REFRESH_VERTICAL_INTENT_RATIO = 1.15;
const PULL_TO_REFRESH_TOP_TOLERANCE_PX = 2;

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 isScrollableElement(node: HTMLElement) {
  const style = window.getComputedStyle(node);
  const overflowY = style.overflowY;

  return (
    (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&
    node.scrollHeight > node.clientHeight + 1
  );
}

function resolvePullToRefreshScrollHost(target: EventTarget | null, gallery: HTMLDivElement) {
  if (!(target instanceof HTMLElement)) {
    return gallery;
  }

  let current: HTMLElement | null = target;

  while (current && current !== gallery) {
    if (isScrollableElement(current)) {
      return current;
    }

    current = current.parentElement;
  }

  return gallery;
}

function isPullToRefreshAtTop(node: HTMLElement) {
  return node.scrollTop <= PULL_TO_REFRESH_TOP_TOLERANCE_PX;
}

const PREVIEW_MODE_OPTIONS = [
  { label: <><AppstoreOutlined /> 격자</>, value: 'grid' },
  { label: <><EyeOutlined /> 모자이크</>, value: 'masonry' },
  { label: <><UnorderedListOutlined /> 목록</>, value: 'list' },
] as const;
const DENSITY_MODE_OPTIONS = [
  { label: '촘촘', value: 'compact' },
  { label: '보통', value: 'comfortable' },
  { label: '크게', value: 'large' },
] as const;
const SORT_ORDER_OPTIONS = [
  { label: '최신순', value: 'newest' },
  { label: '오래된순', value: 'oldest' },
] as const;
const FAVORITE_FILTER_OPTIONS = [
  { label: '전체', value: 'all' },
  { label: '즐겨찾기만', value: 'true' },
] as const;
const MEDIA_TYPE_FILTER_OPTIONS = [
  { label: '전체', value: 'all' },
  { label: '사진', value: 'image' },
  { label: '동영상', value: 'video' },
] as const;
const THEME_PRESET_OPTIONS: Array<{
  value: PhotoPrismThemePreset;
  label: string;
  description: string;
  accent: string;
  vars: Record<string, string>;
}> = [
  {
    value: 'amber',
    label: 'Amber',
    description: '따뜻한 필름 톤',
    accent: '#f97316',
    vars: {
      '--photoprism-accent': '#f97316',
      '--photoprism-accent-rgb': '249, 115, 22',
      '--photoprism-accent-deep': '#9a3412',
      '--photoprism-accent-contrast': '#fff7ed',
      '--photoprism-accent-soft': 'rgba(249, 115, 22, 0.12)',
      '--photoprism-panel-soft': 'rgba(255, 247, 237, 0.92)',
      '--photoprism-panel-strong': 'rgba(255, 251, 246, 0.96)',
      '--photoprism-glow-left': 'rgba(253, 186, 116, 0.4)',
      '--photoprism-glow-right': 'rgba(125, 211, 252, 0.32)',
      '--photoprism-bg-start': '#fff9f1',
      '--photoprism-bg-end': '#eef6ff',
    },
  },
  {
    value: 'ocean',
    label: 'Ocean',
    description: '청록 수면 느낌',
    accent: '#0f766e',
    vars: {
      '--photoprism-accent': '#0f766e',
      '--photoprism-accent-rgb': '15, 118, 110',
      '--photoprism-accent-deep': '#115e59',
      '--photoprism-accent-contrast': '#f0fdfa',
      '--photoprism-accent-soft': 'rgba(15, 118, 110, 0.14)',
      '--photoprism-panel-soft': 'rgba(240, 253, 250, 0.9)',
      '--photoprism-panel-strong': 'rgba(247, 254, 255, 0.96)',
      '--photoprism-glow-left': 'rgba(45, 212, 191, 0.34)',
      '--photoprism-glow-right': 'rgba(96, 165, 250, 0.28)',
      '--photoprism-bg-start': '#f0fdfa',
      '--photoprism-bg-end': '#eff6ff',
    },
  },
  {
    value: 'forest',
    label: 'Forest',
    description: '그린 하이킹 톤',
    accent: '#65a30d',
    vars: {
      '--photoprism-accent': '#65a30d',
      '--photoprism-accent-rgb': '101, 163, 13',
      '--photoprism-accent-deep': '#3f6212',
      '--photoprism-accent-contrast': '#f7fee7',
      '--photoprism-accent-soft': 'rgba(101, 163, 13, 0.14)',
      '--photoprism-panel-soft': 'rgba(247, 254, 231, 0.9)',
      '--photoprism-panel-strong': 'rgba(248, 255, 241, 0.96)',
      '--photoprism-glow-left': 'rgba(163, 230, 53, 0.28)',
      '--photoprism-glow-right': 'rgba(74, 222, 128, 0.22)',
      '--photoprism-bg-start': '#f7fee7',
      '--photoprism-bg-end': '#ecfdf5',
    },
  },
  {
    value: 'berry',
    label: 'Berry',
    description: '선명한 로제 톤',
    accent: '#e11d48',
    vars: {
      '--photoprism-accent': '#e11d48',
      '--photoprism-accent-rgb': '225, 29, 72',
      '--photoprism-accent-deep': '#9f1239',
      '--photoprism-accent-contrast': '#fff1f2',
      '--photoprism-accent-soft': 'rgba(225, 29, 72, 0.12)',
      '--photoprism-panel-soft': 'rgba(255, 241, 242, 0.9)',
      '--photoprism-panel-strong': 'rgba(255, 247, 250, 0.96)',
      '--photoprism-glow-left': 'rgba(251, 113, 133, 0.28)',
      '--photoprism-glow-right': 'rgba(244, 114, 182, 0.24)',
      '--photoprism-bg-start': '#fff1f2',
      '--photoprism-bg-end': '#fdf2f8',
    },
  },
  {
    value: 'graphite',
    label: 'Graphite',
    description: '차분한 메탈 톤',
    accent: '#475569',
    vars: {
      '--photoprism-accent': '#475569',
      '--photoprism-accent-rgb': '71, 85, 105',
      '--photoprism-accent-deep': '#1e293b',
      '--photoprism-accent-contrast': '#f8fafc',
      '--photoprism-accent-soft': 'rgba(71, 85, 105, 0.14)',
      '--photoprism-panel-soft': 'rgba(248, 250, 252, 0.9)',
      '--photoprism-panel-strong': 'rgba(255, 255, 255, 0.95)',
      '--photoprism-glow-left': 'rgba(148, 163, 184, 0.24)',
      '--photoprism-glow-right': 'rgba(96, 165, 250, 0.18)',
      '--photoprism-bg-start': '#f8fafc',
      '--photoprism-bg-end': '#e2e8f0',
    },
  },
  {
    value: 'coral',
    label: 'Coral',
    description: '산호빛 선셋 톤',
    accent: '#f05d5e',
    vars: {
      '--photoprism-accent': '#f05d5e',
      '--photoprism-accent-rgb': '240, 93, 94',
      '--photoprism-accent-deep': '#b42324',
      '--photoprism-accent-contrast': '#fff5f5',
      '--photoprism-accent-soft': 'rgba(240, 93, 94, 0.12)',
      '--photoprism-panel-soft': 'rgba(255, 244, 241, 0.92)',
      '--photoprism-panel-strong': 'rgba(255, 249, 247, 0.96)',
      '--photoprism-glow-left': 'rgba(252, 165, 165, 0.32)',
      '--photoprism-glow-right': 'rgba(253, 186, 116, 0.24)',
      '--photoprism-bg-start': '#fff7f2',
      '--photoprism-bg-end': '#fff1f2',
    },
  },
  {
    value: 'cobalt',
    label: 'Cobalt',
    description: '짙은 블루 네온',
    accent: '#2563eb',
    vars: {
      '--photoprism-accent': '#2563eb',
      '--photoprism-accent-rgb': '37, 99, 235',
      '--photoprism-accent-deep': '#1d4ed8',
      '--photoprism-accent-contrast': '#eff6ff',
      '--photoprism-accent-soft': 'rgba(37, 99, 235, 0.14)',
      '--photoprism-panel-soft': 'rgba(239, 246, 255, 0.9)',
      '--photoprism-panel-strong': 'rgba(245, 249, 255, 0.96)',
      '--photoprism-glow-left': 'rgba(96, 165, 250, 0.3)',
      '--photoprism-glow-right': 'rgba(59, 130, 246, 0.24)',
      '--photoprism-bg-start': '#eff6ff',
      '--photoprism-bg-end': '#eef2ff',
    },
  },
  {
    value: 'mint',
    label: 'Mint',
    description: '밝은 민트 스튜디오',
    accent: '#10b981',
    vars: {
      '--photoprism-accent': '#10b981',
      '--photoprism-accent-rgb': '16, 185, 129',
      '--photoprism-accent-deep': '#047857',
      '--photoprism-accent-contrast': '#ecfdf5',
      '--photoprism-accent-soft': 'rgba(16, 185, 129, 0.13)',
      '--photoprism-panel-soft': 'rgba(236, 253, 245, 0.92)',
      '--photoprism-panel-strong': 'rgba(244, 255, 250, 0.96)',
      '--photoprism-glow-left': 'rgba(110, 231, 183, 0.32)',
      '--photoprism-glow-right': 'rgba(45, 212, 191, 0.24)',
      '--photoprism-bg-start': '#ecfdf5',
      '--photoprism-bg-end': '#f0fdfa',
    },
  },
  {
    value: 'sunflower',
    label: 'Sunflower',
    description: '밝은 골든 아카이브',
    accent: '#d97706',
    vars: {
      '--photoprism-accent': '#d97706',
      '--photoprism-accent-rgb': '217, 119, 6',
      '--photoprism-accent-deep': '#92400e',
      '--photoprism-accent-contrast': '#fffbeb',
      '--photoprism-accent-soft': 'rgba(217, 119, 6, 0.14)',
      '--photoprism-panel-soft': 'rgba(255, 251, 235, 0.92)',
      '--photoprism-panel-strong': 'rgba(255, 252, 244, 0.96)',
      '--photoprism-glow-left': 'rgba(253, 224, 71, 0.32)',
      '--photoprism-glow-right': 'rgba(251, 191, 36, 0.22)',
      '--photoprism-bg-start': '#fffbeb',
      '--photoprism-bg-end': '#fff7ed',
    },
  },
  {
    value: 'cedar',
    label: 'Cedar',
    description: '우디 브라운 톤',
    accent: '#8b5e3c',
    vars: {
      '--photoprism-accent': '#8b5e3c',
      '--photoprism-accent-rgb': '139, 94, 60',
      '--photoprism-accent-deep': '#5b3a29',
      '--photoprism-accent-contrast': '#faf6f2',
      '--photoprism-accent-soft': 'rgba(139, 94, 60, 0.13)',
      '--photoprism-panel-soft': 'rgba(250, 246, 242, 0.92)',
      '--photoprism-panel-strong': 'rgba(255, 251, 247, 0.96)',
      '--photoprism-glow-left': 'rgba(191, 143, 104, 0.28)',
      '--photoprism-glow-right': 'rgba(234, 179, 111, 0.2)',
      '--photoprism-bg-start': '#faf6f2',
      '--photoprism-bg-end': '#f6efe7',
    },
  },
  {
    value: 'ruby',
    label: 'Ruby',
    description: '짙은 레드 시네마',
    accent: '#c81e4b',
    vars: {
      '--photoprism-accent': '#c81e4b',
      '--photoprism-accent-rgb': '200, 30, 75',
      '--photoprism-accent-deep': '#881337',
      '--photoprism-accent-contrast': '#fff1f2',
      '--photoprism-accent-soft': 'rgba(200, 30, 75, 0.13)',
      '--photoprism-panel-soft': 'rgba(255, 241, 242, 0.92)',
      '--photoprism-panel-strong': 'rgba(255, 247, 248, 0.96)',
      '--photoprism-glow-left': 'rgba(244, 114, 182, 0.28)',
      '--photoprism-glow-right': 'rgba(251, 113, 133, 0.22)',
      '--photoprism-bg-start': '#fff1f2',
      '--photoprism-bg-end': '#fdf2f8',
    },
  },
  {
    value: 'sky',
    label: 'Sky',
    description: '맑은 하늘 톤',
    accent: '#0284c7',
    vars: {
      '--photoprism-accent': '#0284c7',
      '--photoprism-accent-rgb': '2, 132, 199',
      '--photoprism-accent-deep': '#0c4a6e',
      '--photoprism-accent-contrast': '#f0f9ff',
      '--photoprism-accent-soft': 'rgba(2, 132, 199, 0.14)',
      '--photoprism-panel-soft': 'rgba(240, 249, 255, 0.92)',
      '--photoprism-panel-strong': 'rgba(247, 252, 255, 0.96)',
      '--photoprism-glow-left': 'rgba(125, 211, 252, 0.32)',
      '--photoprism-glow-right': 'rgba(56, 189, 248, 0.22)',
      '--photoprism-bg-start': '#f0f9ff',
      '--photoprism-bg-end': '#eef6ff',
    },
  },
  {
    value: 'indigo',
    label: 'Indigo',
    description: '딥 블루 아카이브',
    accent: '#4f46e5',
    vars: {
      '--photoprism-accent': '#4f46e5',
      '--photoprism-accent-rgb': '79, 70, 229',
      '--photoprism-accent-deep': '#312e81',
      '--photoprism-accent-contrast': '#eef2ff',
      '--photoprism-accent-soft': 'rgba(79, 70, 229, 0.14)',
      '--photoprism-panel-soft': 'rgba(238, 242, 255, 0.92)',
      '--photoprism-panel-strong': 'rgba(245, 247, 255, 0.96)',
      '--photoprism-glow-left': 'rgba(129, 140, 248, 0.28)',
      '--photoprism-glow-right': 'rgba(96, 165, 250, 0.2)',
      '--photoprism-bg-start': '#eef2ff',
      '--photoprism-bg-end': '#eff6ff',
    },
  },
  {
    value: 'cocoa',
    label: 'Cocoa',
    description: '다크 코코아 톤',
    accent: '#7c3f00',
    vars: {
      '--photoprism-accent': '#7c3f00',
      '--photoprism-accent-rgb': '124, 63, 0',
      '--photoprism-accent-deep': '#4a2511',
      '--photoprism-accent-contrast': '#fdf6f0',
      '--photoprism-accent-soft': 'rgba(124, 63, 0, 0.14)',
      '--photoprism-panel-soft': 'rgba(253, 246, 240, 0.92)',
      '--photoprism-panel-strong': 'rgba(255, 250, 245, 0.96)',
      '--photoprism-glow-left': 'rgba(180, 83, 9, 0.28)',
      '--photoprism-glow-right': 'rgba(251, 146, 60, 0.18)',
      '--photoprism-bg-start': '#fdf6f0',
      '--photoprism-bg-end': '#fff7ed',
    },
  },
  {
    value: 'slate',
    label: 'Slate',
    description: '차가운 스톤 블루',
    accent: '#334155',
    vars: {
      '--photoprism-accent': '#334155',
      '--photoprism-accent-rgb': '51, 65, 85',
      '--photoprism-accent-deep': '#0f172a',
      '--photoprism-accent-contrast': '#f8fafc',
      '--photoprism-accent-soft': 'rgba(51, 65, 85, 0.14)',
      '--photoprism-panel-soft': 'rgba(248, 250, 252, 0.92)',
      '--photoprism-panel-strong': 'rgba(255, 255, 255, 0.96)',
      '--photoprism-glow-left': 'rgba(148, 163, 184, 0.26)',
      '--photoprism-glow-right': 'rgba(125, 211, 252, 0.18)',
      '--photoprism-bg-start': '#f8fafc',
      '--photoprism-bg-end': '#e2e8f0',
    },
  },
];
const VIEWER_THEME_OPTIONS: Array<{
  value: ViewerThemePreset;
  label: string;
  description: string;
  swatch: string;
  vars: Record<string, string>;
}> = [
  {
    value: 'porcelain',
    label: 'Porcelain',
    description: '맑은 화이트 아카이브',
    swatch: 'linear-gradient(135deg, #ffffff 0%, #dbeafe 100%)',
    vars: {
      '--photoprism-viewer-shell-top': '#f7fbff',
      '--photoprism-viewer-shell-mid': '#edf4fb',
      '--photoprism-viewer-shell-bottom': '#e1ebf5',
      '--photoprism-viewer-surface': '#ffffff',
      '--photoprism-viewer-surface-strong': '#f5f9fd',
      '--photoprism-viewer-surface-soft': '#e8f1fa',
      '--photoprism-viewer-text': '#18324a',
      '--photoprism-viewer-text-muted': '#49657f',
      '--photoprism-viewer-text-subtle': '#69849c',
      '--photoprism-viewer-line': '#cad7e5',
      '--photoprism-viewer-line-strong': '#8fb7dc',
      '--photoprism-viewer-accent': '#dcecff',
      '--photoprism-viewer-accent-deep': '#b8d7f5',
      '--photoprism-viewer-control-surface': '#ffffff',
      '--photoprism-viewer-control-surface-hover': '#eef6fd',
      '--photoprism-viewer-control-surface-active': '#e2eefb',
      '--photoprism-viewer-control-text': '#17324c',
      '--photoprism-viewer-stage-top': '#dbe7f2',
      '--photoprism-viewer-stage-bottom': '#c4d3df',
      '--photoprism-viewer-media-surface': '#f8fbff',
      '--photoprism-viewer-rail-surface': '#edf4fb',
      '--photoprism-viewer-shadow-rgb': '125, 163, 199',
    },
  },
  {
    value: 'skyline',
    label: 'Skyline',
    description: '차가운 스카이 블루',
    swatch: 'linear-gradient(135deg, #eff6ff 0%, #bfdbfe 100%)',
    vars: {
      '--photoprism-viewer-shell-top': '#f3f8ff',
      '--photoprism-viewer-shell-mid': '#e6f0fb',
      '--photoprism-viewer-shell-bottom': '#d9e7f5',
      '--photoprism-viewer-surface': '#fefeff',
      '--photoprism-viewer-surface-strong': '#eef5fd',
      '--photoprism-viewer-surface-soft': '#e0ebf8',
      '--photoprism-viewer-text': '#17304a',
      '--photoprism-viewer-text-muted': '#4d6580',
      '--photoprism-viewer-text-subtle': '#6e84a0',
      '--photoprism-viewer-line': '#bfd0e3',
      '--photoprism-viewer-line-strong': '#87add6',
      '--photoprism-viewer-accent': '#d8e8ff',
      '--photoprism-viewer-accent-deep': '#b7d0f0',
      '--photoprism-viewer-control-surface': '#ffffff',
      '--photoprism-viewer-control-surface-hover': '#edf5ff',
      '--photoprism-viewer-control-surface-active': '#ddeafb',
      '--photoprism-viewer-control-text': '#17304a',
      '--photoprism-viewer-stage-top': '#d3e2f1',
      '--photoprism-viewer-stage-bottom': '#b8cadc',
      '--photoprism-viewer-media-surface': '#f5f9fe',
      '--photoprism-viewer-rail-surface': '#e6f0fb',
      '--photoprism-viewer-shadow-rgb': '120, 155, 194',
    },
  },
  {
    value: 'sage',
    label: 'Sage',
    description: '부드러운 민트 세이지',
    swatch: 'linear-gradient(135deg, #f0fdf4 0%, #bbf7d0 100%)',
    vars: {
      '--photoprism-viewer-shell-top': '#f5fcf7',
      '--photoprism-viewer-shell-mid': '#ebf8ef',
      '--photoprism-viewer-shell-bottom': '#dceee2',
      '--photoprism-viewer-surface': '#ffffff',
      '--photoprism-viewer-surface-strong': '#f3fbf5',
      '--photoprism-viewer-surface-soft': '#e2f2e7',
      '--photoprism-viewer-text': '#1a342c',
      '--photoprism-viewer-text-muted': '#4f6f63',
      '--photoprism-viewer-text-subtle': '#6e8c81',
      '--photoprism-viewer-line': '#c6ddd2',
      '--photoprism-viewer-line-strong': '#92bfae',
      '--photoprism-viewer-accent': '#dff3e8',
      '--photoprism-viewer-accent-deep': '#bedfcf',
      '--photoprism-viewer-control-surface': '#ffffff',
      '--photoprism-viewer-control-surface-hover': '#eef8f1',
      '--photoprism-viewer-control-surface-active': '#dfefe4',
      '--photoprism-viewer-control-text': '#173127',
      '--photoprism-viewer-stage-top': '#d8e8dd',
      '--photoprism-viewer-stage-bottom': '#bfd3c7',
      '--photoprism-viewer-media-surface': '#f8fdf9',
      '--photoprism-viewer-rail-surface': '#ebf8ef',
      '--photoprism-viewer-shadow-rgb': '108, 146, 128',
    },
  },
  {
    value: 'sunset',
    label: 'Sunset',
    description: '피치 샌드 선셋',
    swatch: 'linear-gradient(135deg, #fff7ed 0%, #fdba74 100%)',
    vars: {
      '--photoprism-viewer-shell-top': '#fff8f1',
      '--photoprism-viewer-shell-mid': '#fdf0e4',
      '--photoprism-viewer-shell-bottom': '#f7dfcf',
      '--photoprism-viewer-surface': '#fffdfb',
      '--photoprism-viewer-surface-strong': '#fff4eb',
      '--photoprism-viewer-surface-soft': '#fae6d9',
      '--photoprism-viewer-text': '#4a2616',
      '--photoprism-viewer-text-muted': '#7b5848',
      '--photoprism-viewer-text-subtle': '#9a7564',
      '--photoprism-viewer-line': '#e0c8b6',
      '--photoprism-viewer-line-strong': '#d3a387',
      '--photoprism-viewer-accent': '#ffe4cf',
      '--photoprism-viewer-accent-deep': '#f4c9a8',
      '--photoprism-viewer-control-surface': '#fffdfb',
      '--photoprism-viewer-control-surface-hover': '#fff1e6',
      '--photoprism-viewer-control-surface-active': '#fde4d2',
      '--photoprism-viewer-control-text': '#4a2616',
      '--photoprism-viewer-stage-top': '#eed5c2',
      '--photoprism-viewer-stage-bottom': '#dcbba4',
      '--photoprism-viewer-media-surface': '#fff8f2',
      '--photoprism-viewer-rail-surface': '#fdf0e4',
      '--photoprism-viewer-shadow-rgb': '171, 116, 76',
    },
  },
  {
    value: 'blossom',
    label: 'Blossom',
    description: '로즈 핑크 아틀리에',
    swatch: 'linear-gradient(135deg, #fff1f2 0%, #f9a8d4 100%)',
    vars: {
      '--photoprism-viewer-shell-top': '#fff5f7',
      '--photoprism-viewer-shell-mid': '#fdebf0',
      '--photoprism-viewer-shell-bottom': '#f6dce5',
      '--photoprism-viewer-surface': '#fffdfd',
      '--photoprism-viewer-surface-strong': '#fff1f5',
      '--photoprism-viewer-surface-soft': '#fae3ea',
      '--photoprism-viewer-text': '#4f2034',
      '--photoprism-viewer-text-muted': '#80556a',
      '--photoprism-viewer-text-subtle': '#9d7487',
      '--photoprism-viewer-line': '#e1c4d3',
      '--photoprism-viewer-line-strong': '#d9a1bf',
      '--photoprism-viewer-accent': '#ffe0ec',
      '--photoprism-viewer-accent-deep': '#f6bdd3',
      '--photoprism-viewer-control-surface': '#fffdfd',
      '--photoprism-viewer-control-surface-hover': '#fff0f5',
      '--photoprism-viewer-control-surface-active': '#fce2eb',
      '--photoprism-viewer-control-text': '#4f2034',
      '--photoprism-viewer-stage-top': '#edd3dd',
      '--photoprism-viewer-stage-bottom': '#d8bac8',
      '--photoprism-viewer-media-surface': '#fff8fb',
      '--photoprism-viewer-rail-surface': '#fdebf0',
      '--photoprism-viewer-shadow-rgb': '171, 111, 140',
    },
  },
  {
    value: 'aurora',
    label: 'Aurora',
    description: '민트와 라일락 오로라',
    swatch: 'linear-gradient(135deg, #ecfeff 0%, #ddd6fe 100%)',
    vars: {
      '--photoprism-viewer-shell-top': '#f4fbff',
      '--photoprism-viewer-shell-mid': '#edf3ff',
      '--photoprism-viewer-shell-bottom': '#e1e5f7',
      '--photoprism-viewer-surface': '#ffffff',
      '--photoprism-viewer-surface-strong': '#f5f6ff',
      '--photoprism-viewer-surface-soft': '#e8ecfb',
      '--photoprism-viewer-text': '#232b4f',
      '--photoprism-viewer-text-muted': '#5d6690',
      '--photoprism-viewer-text-subtle': '#7f88af',
      '--photoprism-viewer-line': '#ccd4ef',
      '--photoprism-viewer-line-strong': '#a3b1e7',
      '--photoprism-viewer-accent': '#e4e8ff',
      '--photoprism-viewer-accent-deep': '#ccd6fb',
      '--photoprism-viewer-control-surface': '#ffffff',
      '--photoprism-viewer-control-surface-hover': '#f1f4ff',
      '--photoprism-viewer-control-surface-active': '#e4e9fb',
      '--photoprism-viewer-control-text': '#232b4f',
      '--photoprism-viewer-stage-top': '#dce2f4',
      '--photoprism-viewer-stage-bottom': '#c4cde8',
      '--photoprism-viewer-media-surface': '#f8f9ff',
      '--photoprism-viewer-rail-surface': '#edf3ff',
      '--photoprism-viewer-shadow-rgb': '130, 137, 191',
    },
  },
  {
    value: 'midnight',
    label: 'Midnight',
    description: '딥 네이비 스튜디오',
    swatch: 'linear-gradient(135deg, #0f172a 0%, #2563eb 100%)',
    vars: {
      '--photoprism-viewer-shell-top': '#111821',
      '--photoprism-viewer-shell-mid': '#0b1118',
      '--photoprism-viewer-shell-bottom': '#070c13',
      '--photoprism-viewer-surface': '#18222e',
      '--photoprism-viewer-surface-strong': '#1e2936',
      '--photoprism-viewer-surface-soft': '#111926',
      '--photoprism-viewer-text': '#f8fafc',
      '--photoprism-viewer-text-muted': '#d5dfeb',
      '--photoprism-viewer-text-subtle': '#99abbd',
      '--photoprism-viewer-line': '#334155',
      '--photoprism-viewer-line-strong': '#5b7085',
      '--photoprism-viewer-accent': '#334d67',
      '--photoprism-viewer-accent-deep': '#4c6a88',
      '--photoprism-viewer-control-surface': '#edf3f9',
      '--photoprism-viewer-control-surface-hover': '#f6f9fc',
      '--photoprism-viewer-control-surface-active': '#dde7f0',
      '--photoprism-viewer-control-text': '#122a40',
      '--photoprism-viewer-stage-top': '#0c1219',
      '--photoprism-viewer-stage-bottom': '#05090f',
      '--photoprism-viewer-media-surface': '#0f1620',
      '--photoprism-viewer-rail-surface': '#111926',
      '--photoprism-viewer-shadow-rgb': '2, 6, 23',
    },
  },
  {
    value: 'graphite',
    label: 'Graphite',
    description: '쿨 그레이 메탈',
    swatch: 'linear-gradient(135deg, #1f2937 0%, #94a3b8 100%)',
    vars: {
      '--photoprism-viewer-shell-top': '#1d2530',
      '--photoprism-viewer-shell-mid': '#141b24',
      '--photoprism-viewer-shell-bottom': '#0e141b',
      '--photoprism-viewer-surface': '#253140',
      '--photoprism-viewer-surface-strong': '#2b394a',
      '--photoprism-viewer-surface-soft': '#1b2430',
      '--photoprism-viewer-text': '#f8fafc',
      '--photoprism-viewer-text-muted': '#d8e1ea',
      '--photoprism-viewer-text-subtle': '#9cafc1',
      '--photoprism-viewer-line': '#415064',
      '--photoprism-viewer-line-strong': '#72849a',
      '--photoprism-viewer-accent': '#3e5165',
      '--photoprism-viewer-accent-deep': '#566d85',
      '--photoprism-viewer-control-surface': '#f5f8fb',
      '--photoprism-viewer-control-surface-hover': '#ffffff',
      '--photoprism-viewer-control-surface-active': '#dfe7ef',
      '--photoprism-viewer-control-text': '#162334',
      '--photoprism-viewer-stage-top': '#151d27',
      '--photoprism-viewer-stage-bottom': '#0a0f16',
      '--photoprism-viewer-media-surface': '#121a25',
      '--photoprism-viewer-rail-surface': '#1b2430',
      '--photoprism-viewer-shadow-rgb': '3, 7, 18',
    },
  },
  {
    value: 'ember',
    label: 'Ember',
    description: '브론즈 앰버 다크',
    swatch: 'linear-gradient(135deg, #2b160f 0%, #f97316 100%)',
    vars: {
      '--photoprism-viewer-shell-top': '#241612',
      '--photoprism-viewer-shell-mid': '#1a0f0c',
      '--photoprism-viewer-shell-bottom': '#120908',
      '--photoprism-viewer-surface': '#31211c',
      '--photoprism-viewer-surface-strong': '#382620',
      '--photoprism-viewer-surface-soft': '#211511',
      '--photoprism-viewer-text': '#fff8f3',
      '--photoprism-viewer-text-muted': '#f4d9cb',
      '--photoprism-viewer-text-subtle': '#d7b4a3',
      '--photoprism-viewer-line': '#5b4034',
      '--photoprism-viewer-line-strong': '#8e624e',
      '--photoprism-viewer-accent': '#5c3b2f',
      '--photoprism-viewer-accent-deep': '#7c503f',
      '--photoprism-viewer-control-surface': '#fff3ea',
      '--photoprism-viewer-control-surface-hover': '#fffaf5',
      '--photoprism-viewer-control-surface-active': '#f8dfd0',
      '--photoprism-viewer-control-text': '#4b2817',
      '--photoprism-viewer-stage-top': '#170d0b',
      '--photoprism-viewer-stage-bottom': '#0d0605',
      '--photoprism-viewer-media-surface': '#19100d',
      '--photoprism-viewer-rail-surface': '#211511',
      '--photoprism-viewer-shadow-rgb': '18, 7, 4',
    },
  },
  {
    value: 'moss',
    label: 'Moss',
    description: '딥 올리브 스튜디오',
    swatch: 'linear-gradient(135deg, #172018 0%, #84cc16 100%)',
    vars: {
      '--photoprism-viewer-shell-top': '#172019',
      '--photoprism-viewer-shell-mid': '#101611',
      '--photoprism-viewer-shell-bottom': '#0b0f0c',
      '--photoprism-viewer-surface': '#222d25',
      '--photoprism-viewer-surface-strong': '#29352c',
      '--photoprism-viewer-surface-soft': '#161d18',
      '--photoprism-viewer-text': '#f8fcef',
      '--photoprism-viewer-text-muted': '#dce8cc',
      '--photoprism-viewer-text-subtle': '#a8bc97',
      '--photoprism-viewer-line': '#435245',
      '--photoprism-viewer-line-strong': '#6a806c',
      '--photoprism-viewer-accent': '#3f5344',
      '--photoprism-viewer-accent-deep': '#5b745f',
      '--photoprism-viewer-control-surface': '#f6fbe9',
      '--photoprism-viewer-control-surface-hover': '#ffffff',
      '--photoprism-viewer-control-surface-active': '#e1ecd0',
      '--photoprism-viewer-control-text': '#203123',
      '--photoprism-viewer-stage-top': '#0f1611',
      '--photoprism-viewer-stage-bottom': '#070b08',
      '--photoprism-viewer-media-surface': '#111912',
      '--photoprism-viewer-rail-surface': '#161d18',
      '--photoprism-viewer-shadow-rgb': '7, 10, 7',
    },
  },
];
const INITIAL_ADVANCED_FILTERS: PhotoPrismAdvancedFilters = {
  year: '',
  month: '',
  day: '',
  label: [],
  category: [],
  favorite: 'all',
  mediaType: 'all',
  country: '',
  camera: '',
};

function normalizePhotoSearchMeta(
  meta: PhotoPrismSearchMeta,
  input: {
    requestedOffset: number;
    receivedCount: number;
    requestedCount: number;
  },
): PhotoPrismSearchMeta {
  const requestedOffset =
    Number.isFinite(input.requestedOffset) && input.requestedOffset >= 0 ? Math.max(0, Math.round(input.requestedOffset)) : 0;
  const receivedCount =
    Number.isFinite(input.receivedCount) && input.receivedCount >= 0 ? Math.max(0, Math.round(input.receivedCount)) : 0;
  const requestedCount =
    Number.isFinite(input.requestedCount) && input.requestedCount > 0 ? Math.max(1, Math.round(input.requestedCount)) : PHOTO_PAGE_SIZE;
  const nextOffsetFromMeta =
    typeof meta.nextOffset === 'number' && Number.isFinite(meta.nextOffset) ? Math.max(0, Math.round(meta.nextOffset)) : null;
  const fallbackNextOffset = requestedOffset + receivedCount;
  const nextOffset =
    nextOffsetFromMeta !== null && nextOffsetFromMeta > requestedOffset ? nextOffsetFromMeta : fallbackNextOffset;
  const hasMoreFromMeta = meta.hasMore === true;
  const hasMoreFromPageFill = receivedCount >= requestedCount && nextOffset > requestedOffset;
  const hasMore = hasMoreFromMeta || hasMoreFromPageFill;

  return {
    ...meta,
    offset: requestedOffset,
    hasMore,
    nextOffset: hasMore ? nextOffset : requestedOffset,
  };
}

function formatFileSize(bytes?: number) {
  if (typeof bytes !== 'number' || !Number.isFinite(bytes) || bytes <= 0) {
    return '';
  }

  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
  let size = bytes;
  let unitIndex = 0;

  while (size >= 1024 && unitIndex < units.length - 1) {
    size /= 1024;
    unitIndex += 1;
  }

  const precision = size >= 100 || unitIndex === 0 ? 0 : size >= 10 ? 1 : 2;
  return `${size.toFixed(precision)} ${units[unitIndex]}`;
}

function formatPlainNumber(value?: number) {
  if (typeof value !== 'number' || !Number.isFinite(value)) {
    return '';
  }

  return value.toLocaleString('ko-KR');
}

function inferVideoMimeFromName(fileName?: string) {
  const normalized = fileName?.trim().toLowerCase() ?? '';

  if (!normalized) {
    return '';
  }

  if (normalized.endsWith('.mov')) {
    return 'video/quicktime';
  }

  if (normalized.endsWith('.m4v')) {
    return 'video/x-m4v';
  }

  if (normalized.endsWith('.webm')) {
    return 'video/webm';
  }

  if (normalized.endsWith('.avi')) {
    return 'video/x-msvideo';
  }

  if (normalized.endsWith('.mkv')) {
    return 'video/x-matroska';
  }

  if (normalized.endsWith('.3gp')) {
    return 'video/3gpp';
  }

  if (normalized.endsWith('.mts') || normalized.endsWith('.m2ts')) {
    return 'video/mp2t';
  }

  if (normalized.endsWith('.mp4')) {
    return 'video/mp4';
  }

  return '';
}

function isLikelyPosterFile(file: PhotoPrismFile | null | undefined, photo: PhotoPrismPhoto | null | undefined) {
  const mime = file?.Mime?.trim().toLowerCase() ?? '';
  const fileName = (file?.Name || photo?.OriginalName || photo?.FileName || '').trim().toLowerCase();

  if (mime.startsWith('image/')) {
    return true;
  }

  return /\.(mp4|mov|m4v|webm|avi|mkv|3gp|mts|m2ts)\.(jpg|jpeg|png|webp|avif)$/i.test(fileName);
}

function resolveViewerVideoMime(file: PhotoPrismFile | null | undefined, photo: PhotoPrismPhoto | null | undefined) {
  const mime = file?.Mime?.trim() || '';

  if (mime.toLowerCase().startsWith('video/')) {
    return mime;
  }

  return inferVideoMimeFromName(file?.Name || photo?.OriginalName || photo?.FileName);
}

function readStoredSession(): PhotoPrismStoredSession | null {
  if (typeof window === 'undefined') {
    return null;
  }

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

    if (!raw) {
      return null;
    }

    const parsed = JSON.parse(raw) as Partial<PhotoPrismStoredSession>;

    if (!parsed.baseUrl || !parsed.username || !parsed.token) {
      return null;
    }

    return {
      baseUrl: parsed.baseUrl,
      username: parsed.username,
      password: parsed.rememberPassword ? parsed.password ?? '' : '',
      rememberPassword: parsed.rememberPassword === true,
      token: parsed.token,
    };
  } catch {
    return null;
  }
}

function writeStoredSession(session: PhotoPrismStoredSession | null) {
  if (typeof window === 'undefined') {
    return;
  }

  if (!session) {
    window.localStorage.removeItem(STORAGE_KEY);
    return;
  }

  const payload = session.rememberPassword
    ? session
    : {
        ...session,
        password: '',
      };
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
}

function readStoredThemePreset(): PhotoPrismThemePreset {
  if (typeof window === 'undefined') {
    return 'amber';
  }

  const stored = window.localStorage.getItem(THEME_STORAGE_KEY)?.trim() as PhotoPrismThemePreset | undefined;
  return THEME_PRESET_OPTIONS.some((option) => option.value === stored) ? stored : 'amber';
}

function readStoredViewerTheme(): ViewerThemePreset {
  if (typeof window === 'undefined') {
    return 'porcelain';
  }

  const stored = window.localStorage.getItem(VIEWER_THEME_STORAGE_KEY)?.trim();
  const migratedStoredValue =
    stored === 'light' ? 'porcelain' : stored === 'dark' ? 'midnight' : stored;
  return VIEWER_THEME_OPTIONS.some((option) => option.value === migratedStoredValue)
    ? (migratedStoredValue as ViewerThemePreset)
    : 'porcelain';
}

function isPreviewMode(value: unknown): value is PreviewMode {
  return value === 'grid' || value === 'masonry' || value === 'list';
}

function isDensityMode(value: unknown): value is DensityMode {
  return value === 'compact' || value === 'comfortable' || value === 'large';
}

function readStoredDisplaySettings(): PhotoPrismDisplaySettings | null {
  if (typeof window === 'undefined') {
    return null;
  }

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

    if (!raw) {
      return null;
    }

    const parsed = JSON.parse(raw) as Partial<PhotoPrismDisplaySettings>;
    const previewMode = isPreviewMode(parsed.previewMode) ? parsed.previewMode : undefined;
    const densityMode = isDensityMode(parsed.densityMode) ? parsed.densityMode : undefined;

    if (!previewMode && !densityMode) {
      return null;
    }

    return {
      previewMode,
      densityMode,
    };
  } catch {
    return null;
  }
}

function writeStoredDisplaySettings(settings: PhotoPrismDisplaySettings) {
  if (typeof window === 'undefined') {
    return;
  }

  window.localStorage.setItem(DISPLAY_SETTINGS_STORAGE_KEY, JSON.stringify(settings));
}

function normalizeAlbums(items: PhotoPrismAlbum[]) {
  return [
    {
      UID: ALL_ALBUM_ID,
      Title: '전체 앨범',
      Name: '전체 앨범',
    },
    ...items.filter((item) => (item.Type?.trim().toLowerCase() || 'album') === 'album'),
  ];
}

function normalizePhotoMembershipAlbums(photo: PhotoPrismPhoto | null | undefined) {
  if (!photo?.Albums?.length) {
    return [] as PhotoPrismAlbum[];
  }

  const seen = new Set<string>();

  return photo.Albums.filter((album) => {
    const uid = album.UID?.trim() ?? '';
    const type = album.Type?.trim().toLowerCase() || 'album';

    if (!uid || uid === ALL_ALBUM_ID || type !== 'album' || seen.has(uid)) {
      return false;
    }

    seen.add(uid);
    return true;
  });
}

function normalizeFilterTokenValue(value: string) {
  return value.trim().replace(/^#+/u, '');
}

function normalizeFilterValues(values: string[]) {
  return Array.from(
    new Set(
      values.map((entry) => normalizeFilterTokenValue(entry)).filter(Boolean),
    ),
  );
}

function quotePhotoPrismQueryValue(value: string) {
  const normalized = normalizeFilterTokenValue(value);

  if (!normalized) {
    return '';
  }

  return /[\s"]/u.test(normalized) ? `"${normalized.replace(/(["\\])/gu, '\\$1')}"` : normalized;
}

function buildPhotoPrismSearchQuery(keyword: string, filters: PhotoPrismAdvancedFilters) {
  const queryTokens: string[] = [];
  const normalizedKeyword = keyword.trim();

  if (normalizedKeyword) {
    queryTokens.push(normalizedKeyword);
  }

  const normalizedYear = filters.year.trim();
  if (normalizedYear) {
    queryTokens.push(`year:${normalizedYear}`);
  }

  const normalizedMonth = filters.month.trim();
  if (normalizedMonth) {
    queryTokens.push(`month:${normalizedMonth}`);
  }

  const normalizedDay = filters.day.trim();
  if (normalizedDay) {
    queryTokens.push(`day:${normalizedDay}`);
  }

  normalizeFilterValues(filters.label).forEach((value) => {
    const token = quotePhotoPrismQueryValue(value);
    if (token) {
      queryTokens.push(`label:${token}`);
    }
  });

  normalizeFilterValues(filters.category).forEach((value) => {
    const token = quotePhotoPrismQueryValue(value);
    if (token) {
      queryTokens.push(`category:${token}`);
    }
  });

  if (filters.favorite === 'true') {
    queryTokens.push('favorite:true');
  }

  if (filters.mediaType !== 'all') {
    queryTokens.push(`type:${filters.mediaType}`);
  }

  const normalizedCountry = quotePhotoPrismQueryValue(filters.country);
  if (normalizedCountry) {
    queryTokens.push(`country:${normalizedCountry}`);
  }

  const normalizedCamera = quotePhotoPrismQueryValue(filters.camera);
  if (normalizedCamera) {
    queryTokens.push(`camera:${normalizedCamera}`);
  }

  return queryTokens.join(' ').trim();
}

function buildShareFileName(photo: PhotoPrismPhoto) {
  const primaryFile = getPrimaryPhotoFile(photo);
  const originalName = (primaryFile?.Name || photo.OriginalName || photo.FileName || '').trim();

  if (originalName) {
    return originalName.replace(/[\\/:*?"<>|]+/g, ' ').trim() || `${photo.UID}`;
  }

  const title = getPhotoDisplayTitle(photo).replace(/[\\/:*?"<>|]+/g, ' ').trim();
  const mime = (primaryFile?.Mime || photo.Type || '').trim().toLowerCase();
  const extension =
    mime === 'video/quicktime'
      ? 'mov'
      : mime.includes('/')
        ? mime.split('/')[1]?.replace(/[^a-z0-9]+/g, '') || ''
        : '';

  return `${title || photo.UID}${extension ? `.${extension}` : ''}`;
}

function getPhotoSectionLabel(photo: PhotoPrismPhoto) {
  if (!photo.TakenAt) {
    return '날짜 없음';
  }

  const takenAt = new Date(photo.TakenAt);

  if (Number.isNaN(takenAt.getTime())) {
    return '날짜 없음';
  }

  try {
    return new Intl.DateTimeFormat('ko-KR', {
      year: 'numeric',
      month: 'long',
    }).format(takenAt);
  } catch {
    return `${takenAt.getFullYear()}년 ${takenAt.getMonth() + 1}월`;
  }
}

function getPhotoDateParts(photo: PhotoPrismPhoto) {
  if (!photo.TakenAt) {
    return null;
  }

  const takenAt = new Date(photo.TakenAt);
  if (Number.isNaN(takenAt.getTime())) {
    return null;
  }

  return {
    year: String(takenAt.getFullYear()),
    month: String(takenAt.getMonth() + 1),
    day: String(takenAt.getDate()),
  };
}

function normalizePhotoMetaValue(value: unknown) {
  if (typeof value !== 'string') {
    return '';
  }

  return value.trim().replace(/^#+/u, '');
}

function extractTokenNames(input: unknown): string[] {
  if (Array.isArray(input)) {
    return Array.from(new Set(input.flatMap((item) => extractTokenNames(item))));
  }

  if (typeof input === 'string') {
    const normalized = normalizePhotoMetaValue(input);
    return normalized ? [normalized] : [];
  }

  if (!input || typeof input !== 'object') {
    return [];
  }

  const record = input as Record<string, unknown>;
  const candidates = [record.Name, record.Title, record.Label, record.Keyword, record.Slug];

  return Array.from(
    new Set(candidates.map((candidate) => normalizePhotoMetaValue(candidate)).filter(Boolean)),
  );
}

function collectPhotoRecordValues(photo: PhotoPrismPhoto, keys: string[]) {
  const record = photo as unknown as Record<string, unknown>;
  return Array.from(new Set(keys.flatMap((key) => extractTokenNames(record[key]))));
}

function createNumericSelectOptions(values: string[]) {
  return values.map((value) => ({ label: value, value }));
}

function getFilterSelectPopupContainer(triggerNode: HTMLElement) {
  return triggerNode.parentElement ?? document.body;
}

type DragSelectionMode = 'select' | 'deselect';

type RectBounds = {
  left: number;
  top: number;
  right: number;
  bottom: number;
};

type DragSelectionState = {
  pointerId: number;
  startUid: string;
  currentUid: string;
  startX: number;
  startY: number;
  initialSelectionMode: boolean;
  initialSelected: boolean;
  longPressActivated: boolean;
  movedBeforeLongPress: boolean;
  mode: DragSelectionMode | null;
  baselineSelectedUids: Set<string>;
  latestX: number;
  latestY: number;
  startScrollTop: number;
  photoOrder: string[];
  photoRects: Map<string, RectBounds>;
  target: HTMLElement | null;
  rafId: number | null;
  timerId: number | null;
  autoScrollDirection: 'up' | 'down' | null;
  autoScrollSince: number | null;
};

export function PhotoPrismAppView({ onBack, launchContext = 'direct' }: PhotoPrismAppViewProps) {
  const [messageApi, contextHolder] = message.useMessage();
  const [form] = Form.useForm<{
    baseUrl: string;
    username: string;
    password: string;
    rememberPassword: boolean;
  }>();
  const appRootRef = useRef<HTMLElement | null>(null);
  const [isMounted, setIsMounted] = useState(false);
  const [isBusy, setIsBusy] = useState(false);
  const [session, setSession] = useState<PhotoPrismStoredSession | null>(null);
  const [albums, setAlbums] = useState<PhotoPrismAlbum[]>([]);
  const [photos, setPhotos] = useState<PhotoPrismPhoto[]>([]);
  const [isLoadingMore, setIsLoadingMore] = useState(false);
  const [hasMorePhotos, setHasMorePhotos] = useState(false);
  const [activeAlbumUid, setActiveAlbumUid] = useState(ALL_ALBUM_ID);
  const [selectedPhotoUid, setSelectedPhotoUid] = useState('');
  const [selectedPhotoUids, setSelectedPhotoUids] = useState<string[]>([]);
  const [searchKeyword, setSearchKeyword] = useState('');
  const [advancedFilters, setAdvancedFilters] = useState<PhotoPrismAdvancedFilters>(INITIAL_ADVANCED_FILTERS);
  const [themePreset, setThemePreset] = useState<PhotoPrismThemePreset>('amber');
  const [viewerThemePreset, setViewerThemePreset] = useState<ViewerThemePreset>('porcelain');
  const [isViewerThemeMenuOpen, setIsViewerThemeMenuOpen] = useState(false);
  const [isCompactViewport, setIsCompactViewport] = useState(false);
  const [isAlbumPanelOpen, setIsAlbumPanelOpen] = useState(false);
  const [mobileFilterSection, setMobileFilterSection] = useState<MobileFilterSectionKey>(null);
  const [isMobileControlsOpen, setIsMobileControlsOpen] = useState(false);
  const [hasHydratedDisplaySettings, setHasHydratedDisplaySettings] = useState(false);
  const [previewMode, setPreviewMode] = useState<PreviewMode>('grid');
  const [densityMode, setDensityMode] = useState<DensityMode>('comfortable');
  const [sortOrder, setSortOrder] = useState<PhotoSortOrder>('newest');
  const [viewerPhotoUid, setViewerPhotoUid] = useState('');
  const [viewerDetailPhoto, setViewerDetailPhoto] = useState<PhotoPrismPhoto | null>(null);
  const [isViewerDetailLoading, setIsViewerDetailLoading] = useState(false);
  const [viewerDisplayTab, setViewerDisplayTab] = useState<ViewerDisplayTabKey>('photo');
  const [viewerMediaState, setViewerMediaState] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle');
  const [viewerVideoError, setViewerVideoError] = useState('');
  const [viewerVideoState, setViewerVideoState] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle');
  const [viewerZoomScale, setViewerZoomScale] = useState(1);
  const [viewerAlbumInputUid, setViewerAlbumInputUid] = useState<string | undefined>(undefined);
  const [viewerAlbumMutationKey, setViewerAlbumMutationKey] = useState('');
  const [refreshNonce, setRefreshNonce] = useState(0);
  const [pullToRefreshOffset, setPullToRefreshOffset] = useState(0);
  const [isPullToRefreshArmed, setIsPullToRefreshArmed] = useState(false);
  const [isPullRefreshing, setIsPullRefreshing] = useState(false);
  const [favoritePendingUids, setFavoritePendingUids] = useState<string[]>([]);
  const [meta, setMeta] = useState<PhotoPrismSearchMeta>({
    count: 0,
    limit: 0,
    offset: 0,
    previewToken: '',
    downloadToken: '',
  });
  const viewerTouchStartRef = useRef<{ x: number; y: number } | null>(null);
  const viewerRailRef = useRef<HTMLDivElement | null>(null);
  const viewerVideoRef = useRef<HTMLVideoElement | null>(null);
  const viewerDetailCacheRef = useRef<Record<string, PhotoPrismPhoto>>({});
  const viewerActionsHideTimerRef = useRef<number | null>(null);
  const dragSelectionRef = useRef<DragSelectionState | null>(null);
  const galleryRef = useRef<HTMLDivElement | null>(null);
  const galleryPreloadTriggerRef = useRef<HTMLElement | null>(null);
  const dragAutoScrollFrameRef = useRef<number | null>(null);
  const lastCardActionRef = useRef<{ uid: string; action: 'open' | 'toggle'; at: number } | null>(null);
  const isLoadingMoreRef = useRef(false);
  const pullToRefreshStateRef = useRef<{
    startX: number;
    startY: number;
    active: boolean;
    cancelled: boolean;
    scrollHost: HTMLElement;
  } | null>(null);
  const pullRefreshPhaseRef = useRef<'idle' | 'scheduled' | 'loading'>('idle');
  const previewModeTouchedRef = useRef(false);
  const densityModeTouchedRef = useRef(false);
  const [isRangeSelecting, setIsRangeSelecting] = useState(false);
  const [rangeTargetUids, setRangeTargetUids] = useState<string[]>([]);
  const [isViewerActionsVisible, setIsViewerActionsVisible] = useState(true);
  const isEmbeddedLaunch = launchContext === 'embedded';
  const isStandaloneLaunch = isStandaloneDisplayMode();
  const canExitToParentApp = isEmbeddedLaunch && !isStandaloneLaunch;

  const isOverlayActionTarget = (target: EventTarget | null) =>
    target instanceof HTMLElement && Boolean(target.closest('[data-photoprism-overlay-action="true"]'));

  const clearViewerActionsHideTimer = useCallback(() => {
    if (viewerActionsHideTimerRef.current && typeof window !== 'undefined') {
      window.clearTimeout(viewerActionsHideTimerRef.current);
      viewerActionsHideTimerRef.current = null;
    }
  }, []);

  const scheduleViewerActionsAutoHide = useCallback(() => {
    clearViewerActionsHideTimer();

    if (typeof window === 'undefined' || !isCompactViewport || !viewerPhotoUid || viewerDisplayTab !== 'photo') {
      return;
    }

    viewerActionsHideTimerRef.current = window.setTimeout(() => {
      setIsViewerActionsVisible(false);
    }, VIEWER_ACTIONS_AUTO_HIDE_MS);
  }, [clearViewerActionsHideTimer, isCompactViewport, viewerDisplayTab, viewerPhotoUid]);

  const revealViewerActions = useCallback(() => {
    setIsViewerActionsVisible(true);
    scheduleViewerActionsAutoHide();
  }, [scheduleViewerActionsAutoHide]);

  useEffect(() => {
    setIsMounted(true);

    const storedSession = readStoredSession();
    const storedDisplaySettings = readStoredDisplaySettings();
    setThemePreset(readStoredThemePreset());
    setViewerThemePreset(readStoredViewerTheme());

    if (storedDisplaySettings?.previewMode) {
      previewModeTouchedRef.current = true;
      setPreviewMode(storedDisplaySettings.previewMode);
    }

    if (storedDisplaySettings?.densityMode) {
      densityModeTouchedRef.current = true;
      setDensityMode(storedDisplaySettings.densityMode);
    }

    setHasHydratedDisplaySettings(true);

    if (storedSession) {
      setSession(storedSession);
      form.setFieldsValue({
        baseUrl: storedSession.baseUrl,
        username: storedSession.username,
        password: storedSession.password,
        rememberPassword: storedSession.rememberPassword,
      });
    }
  }, [form]);

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

    window.localStorage.setItem(THEME_STORAGE_KEY, themePreset);
  }, [themePreset]);

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

    window.localStorage.setItem(VIEWER_THEME_STORAGE_KEY, viewerThemePreset);
  }, [viewerThemePreset]);

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

    writeStoredDisplaySettings({
      previewMode,
      densityMode,
    });
  }, [densityMode, hasHydratedDisplaySettings, previewMode]);

  useEffect(() => {
    if (!viewerPhotoUid) {
      setIsViewerThemeMenuOpen(false);
    }
  }, [viewerPhotoUid]);

  useEffect(() => {
    setViewerAlbumInputUid(undefined);
    setViewerAlbumMutationKey('');
  }, [viewerPhotoUid]);

  useEffect(() => {
    if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
      return;
    }

    const mediaQuery = window.matchMedia(MOBILE_MEDIA_QUERY);
    const applyViewportState = (matches: boolean) => {
      setIsCompactViewport(matches);
    };

    applyViewportState(mediaQuery.matches);

    const handleChange = (event: MediaQueryListEvent) => {
      applyViewportState(event.matches);
    };

    if (typeof mediaQuery.addEventListener === 'function') {
      mediaQuery.addEventListener('change', handleChange);
      return () => mediaQuery.removeEventListener('change', handleChange);
    }

    mediaQuery.addListener(handleChange);
    return () => mediaQuery.removeListener(handleChange);
  }, []);

  useEffect(() => {
    if (isCompactViewport) {
      setIsAlbumPanelOpen(false);
      setIsMobileControlsOpen(false);

      if (!previewModeTouchedRef.current) {
        setPreviewMode('masonry');
      }

      if (!densityModeTouchedRef.current) {
        setDensityMode('compact');
      }

      return;
    }

    setIsAlbumPanelOpen(true);
    setIsMobileControlsOpen(false);

    if (!previewModeTouchedRef.current) {
      setPreviewMode('grid');
    }

    if (!densityModeTouchedRef.current) {
      setDensityMode('comfortable');
    }
  }, [isCompactViewport]);

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

    let cancelled = false;
    const queryText = buildPhotoPrismSearchQuery(searchKeyword, advancedFilters);

    const loadData = async () => {
      setIsBusy(true);
      isLoadingMoreRef.current = false;
      setIsLoadingMore(false);
      setHasMorePhotos(false);

      try {
        const [albumResponse, photoResponse] = await Promise.all([
          fetchPhotoPrismAlbums({
            baseUrl: session.baseUrl,
            token: session.token,
          }),
          fetchPhotoPrismPhotos({
            baseUrl: session.baseUrl,
            token: session.token,
            albumUid: activeAlbumUid === ALL_ALBUM_ID ? undefined : activeAlbumUid,
            order: sortOrder,
            q: queryText,
            count: PHOTO_PAGE_SIZE,
          }),
        ]);

        if (cancelled) {
          return;
        }

        const normalizedPhotoMeta = normalizePhotoSearchMeta(photoResponse.meta, {
          requestedOffset: 0,
          receivedCount: photoResponse.items.length,
          requestedCount: PHOTO_PAGE_SIZE,
        });
        const normalizedAlbumItems = normalizeAlbums(albumResponse.items);

        setAlbums(normalizedAlbumItems);
        setPhotos(photoResponse.items);
        setMeta(normalizedPhotoMeta);
        setHasMorePhotos(normalizedPhotoMeta.hasMore === true);
        setActiveAlbumUid((current) => normalizedAlbumItems.some((album) => album.UID === current) ? current : ALL_ALBUM_ID);
        const nextSelectedPhotoUid = photoResponse.items[0]?.UID ?? '';
        setSelectedPhotoUid((current) => (photoResponse.items.some((photo) => photo.UID === current) ? current : nextSelectedPhotoUid));
        setSelectedPhotoUids((current) => current.filter((uid) => photoResponse.items.some((photo) => photo.UID === uid)));
      } catch (error) {
        const messageText = error instanceof Error ? error.message : 'PhotoPrism 데이터를 불러오지 못했습니다.';

        if (!cancelled) {
          messageApi.error(messageText);

          if (/401|403|token|인증/u.test(messageText)) {
            setSession(null);
            writeStoredSession(null);
          }
        }
      } finally {
        if (!cancelled) {
          setIsBusy(false);
        }
      }
    };

    void loadData();

    return () => {
      cancelled = true;
    };
  }, [activeAlbumUid, advancedFilters, messageApi, refreshNonce, searchKeyword, session, sortOrder]);

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

    if (isBusy && pullRefreshPhaseRef.current === 'scheduled') {
      pullRefreshPhaseRef.current = 'loading';
      return;
    }

    if (!isBusy && pullRefreshPhaseRef.current === 'loading') {
      pullRefreshPhaseRef.current = 'idle';
      setIsPullRefreshing(false);
    }
  }, [isBusy, isPullRefreshing]);

  useEffect(() => {
    isLoadingMoreRef.current = isLoadingMore;
  }, [isLoadingMore]);

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

    const html = document.documentElement;
    const body = document.body;
    const previousHtmlOverflow = html.style.overflow;
    const previousBodyOverflow = body.style.overflow;

    html.style.overflow = 'hidden';
    body.style.overflow = 'hidden';

    return () => {
      html.style.overflow = previousHtmlOverflow;
      body.style.overflow = previousBodyOverflow;
    };
  }, []);

  useEffect(() => {
    if (!isRangeSelecting || typeof document === 'undefined') {
      return;
    }

    const preventTouchMove = (event: TouchEvent) => {
      event.preventDefault();
    };

    document.addEventListener('touchmove', preventTouchMove, { passive: false });

    return () => {
      document.removeEventListener('touchmove', preventTouchMove);
    };
  }, [isRangeSelecting]);

  useEffect(() => {
    if (!viewerPhotoUid) {
      clearViewerActionsHideTimer();
      setIsViewerActionsVisible(true);
      return;
    }

    if (!isCompactViewport || viewerDisplayTab !== 'photo') {
      clearViewerActionsHideTimer();
      setIsViewerActionsVisible(true);
      return;
    }

    setIsViewerActionsVisible(true);
    scheduleViewerActionsAutoHide();

    return () => {
      clearViewerActionsHideTimer();
    };
  }, [clearViewerActionsHideTimer, isCompactViewport, scheduleViewerActionsAutoHide, viewerDisplayTab, viewerPhotoUid]);

  const refreshGallery = useCallback(
    (reason: 'manual' | 'pull' = 'manual') => {
      if (!session || isBusy || isLoadingMoreRef.current) {
        return false;
      }

      if (reason === 'pull') {
        pullRefreshPhaseRef.current = 'scheduled';
        setIsPullRefreshing(true);
      }

      setRefreshNonce((current) => current + 1);
      return true;
    },
    [isBusy, session],
  );

  const selectedPhoto = useMemo(
    () => photos.find((photo) => photo.UID === selectedPhotoUid) ?? photos[0] ?? null,
    [photos, selectedPhotoUid],
  );
  const selectedPhotos = useMemo(() => {
    if (!selectedPhotoUids.length) {
      return [] as PhotoPrismPhoto[];
    }

    const selectedUidSet = new Set(selectedPhotoUids);
    return photos.filter((photo) => selectedUidSet.has(photo.UID));
  }, [photos, selectedPhotoUids]);
  const selectedPhotoUidSet = useMemo(() => new Set(selectedPhotoUids), [selectedPhotoUids]);
  const selectedCount = selectedPhotos.length;
  const isSelectionMode = selectedCount > 0;
  const allPhotosSelected = photos.length > 0 && selectedCount === photos.length;

  useEffect(() => {
    const selectionSummary =
      selectedCount > 0
        ? `PhotoPrism에서 사진 ${selectedCount}장을 선택했습니다.`
        : viewerPhotoUid
          ? `PhotoPrism에서 현재 보고 있는 사진 UID는 ${viewerPhotoUid}입니다.`
          : null;
    const selectionIds = selectedCount > 0 ? selectedPhotoUids : viewerPhotoUid ? [viewerPhotoUid] : [];

    publishChatActionContext({
      sourceAppId: 'photoprism',
      featureTitle: isSelectionMode ? '선택 사진 작업' : 'PhotoPrism 탐색',
      selectionSummary,
      selectionIds,
    });

    return () => {
      clearChatActionContext('photoprism');
    };
  }, [isSelectionMode, selectedCount, selectedPhotoUids, viewerPhotoUid]);

  const photoThumbUrls = useMemo(() => {
    if (!session || !meta.previewToken) {
      return {} as Record<string, string>;
    }

    return photos.reduce<Record<string, string>>((accumulator, photo) => {
      const primaryFile = getPrimaryPhotoFile(photo);
      const hash = primaryFile?.Hash ?? photo.Hash;

      accumulator[photo.UID] = hash
        ? buildPhotoPrismThumbUrl({
            baseUrl: session.baseUrl,
            hash,
            previewToken: meta.previewToken,
            token: session.token,
            size: LIST_THUMB_SIZE,
          })
        : '';

      return accumulator;
    }, {});
  }, [meta.previewToken, photos, session]);

  const viewerPhotos = photos;
  const viewerPhoto = useMemo(
    () => viewerPhotos.find((photo) => photo.UID === viewerPhotoUid) ?? null,
    [viewerPhotoUid, viewerPhotos],
  );
  const viewerDetail = useMemo(() => {
    if (viewerDetailPhoto?.UID === viewerPhotoUid) {
      return viewerDetailPhoto;
    }

    return viewerPhoto;
  }, [viewerDetailPhoto, viewerPhoto, viewerPhotoUid]);
  const viewerPhotoThumbUrl = viewerPhoto ? photoThumbUrls[viewerPhoto.UID] ?? '' : '';
  const viewerDisplayPhoto = viewerDetail ?? viewerPhoto;
  const isViewerVideo = useMemo(() => (viewerDisplayPhoto ? isPhotoPrismVideo(viewerDisplayPhoto) : false), [viewerDisplayPhoto]);
  const viewerPrimaryFile = useMemo(
    () => (viewerDisplayPhoto ? getPrimaryPhotoFile(viewerDisplayPhoto) : null),
    [viewerDisplayPhoto],
  );
  const viewerVideoMime = useMemo(
    () => resolveViewerVideoMime(viewerPrimaryFile, viewerDisplayPhoto),
    [viewerDisplayPhoto, viewerPrimaryFile],
  );
  const viewerVideoSourceWarning = useMemo(() => {
    if (!isViewerVideo) {
      return '';
    }

    if (isLikelyPosterFile(viewerPrimaryFile, viewerDisplayPhoto)) {
      return '현재 대표 파일 메타가 썸네일 또는 포스터 이미지로 보여 실제 동영상 파일 재선택이 필요할 수 있습니다.';
    }

    if (!viewerVideoMime) {
      return '현재 응답에서 재생 가능한 동영상 MIME 정보를 확인하지 못했습니다.';
    }

    return '';
  }, [isViewerVideo, viewerDisplayPhoto, viewerPrimaryFile, viewerVideoMime]);
  const viewerPhotoIndex = useMemo(
    () => (viewerPhoto ? viewerPhotos.findIndex((photo) => photo.UID === viewerPhoto.UID) : -1),
    [viewerPhoto, viewerPhotos],
  );
  const canViewPreviousPhoto = viewerPhotoIndex > 0;
  const canViewNextPhoto = viewerPhotoIndex >= 0 && viewerPhotoIndex < viewerPhotos.length - 1;
  const viewerPhotoUrl = useMemo(() => {
    if (!session || !viewerDisplayPhoto) {
      return '';
    }

    const hash = viewerPrimaryFile?.Hash ?? viewerDisplayPhoto.Hash;

    if (isViewerVideo && meta.downloadToken) {
      return buildPhotoPrismDownloadUrl({
        baseUrl: session.baseUrl,
        token: session.token,
        uid: viewerDisplayPhoto.UID,
        hash,
        downloadToken: meta.downloadToken,
      });
    }

    return buildPhotoPrismViewerUrl({
      baseUrl: session.baseUrl,
      token: session.token,
      uid: viewerDisplayPhoto.UID,
      hash,
      previewToken: meta.previewToken,
      downloadToken: meta.downloadToken,
      size: DETAIL_VIEW_SIZE,
    });
  }, [isViewerVideo, meta.downloadToken, meta.previewToken, session, viewerDisplayPhoto, viewerPrimaryFile]);
  const actionTargetPhotos = useMemo(() => {
    if (selectedPhotos.length > 0) {
      return selectedPhotos;
    }

    return viewerDisplayPhoto ? [viewerDisplayPhoto] : [];
  }, [selectedPhotos, viewerDisplayPhoto]);
  const actionTargetCount = actionTargetPhotos.length;
  const viewerStageStyle = useMemo(() => {
    if (!viewerPhotoThumbUrl) {
      return undefined;
    }

    return {
      '--photoprism-viewer-placeholder-image': `url("${viewerPhotoThumbUrl}")`,
    } as CSSProperties;
  }, [viewerPhotoThumbUrl]);
  const shouldShowViewerPlaceholder = viewerMediaState !== 'ready' && viewerMediaState !== 'error';

  const albumTitle = useMemo(() => {
    return albums.find((album) => album.UID === activeAlbumUid)?.Title ?? albums.find((album) => album.UID === activeAlbumUid)?.Name ?? '전체 앨범';
  }, [activeAlbumUid, albums]);
  const normalizedSearchKeyword = searchKeyword.trim();
  const hasKeywordFilter = normalizedSearchKeyword.length > 0;
  const hasAlbumFilter = activeAlbumUid !== ALL_ALBUM_ID;
  const viewerTabSummaryParts = useMemo(() => {
    const parts: string[] = [];
    if (viewerPhotoIndex >= 0) {
      parts.push(`${viewerPhotoIndex + 1} / ${photos.length}`);
    }
    if (hasAlbumFilter && albumTitle) {
      parts.push(albumTitle);
    }
    if (actionTargetCount > 0) {
      parts.push(`작업 ${actionTargetCount}장`);
    }
    return parts;
  }, [actionTargetCount, albumTitle, hasAlbumFilter, photos.length, viewerPhotoIndex]);
  const activeSearchQuery = useMemo(
    () => buildPhotoPrismSearchQuery(searchKeyword, advancedFilters),
    [advancedFilters, searchKeyword],
  );
  const normalizedLabelFilters = useMemo(() => normalizeFilterValues(advancedFilters.label), [advancedFilters.label]);
  const normalizedCategoryFilters = useMemo(() => normalizeFilterValues(advancedFilters.category), [advancedFilters.category]);
  const selectedThemeOption = useMemo(
    () => THEME_PRESET_OPTIONS.find((option) => option.value === themePreset) ?? THEME_PRESET_OPTIONS[0],
    [themePreset],
  );
  const themeStyle = useMemo(() => selectedThemeOption.vars as CSSProperties, [selectedThemeOption.vars]);
  const selectedViewerThemeOption = useMemo(
    () => VIEWER_THEME_OPTIONS.find((option) => option.value === viewerThemePreset) ?? VIEWER_THEME_OPTIONS[0],
    [viewerThemePreset],
  );
  const viewerThemeStyle = useMemo(
    () =>
      ({
        ...selectedThemeOption.vars,
        ...selectedViewerThemeOption.vars,
      }) as CSSProperties,
    [selectedThemeOption.vars, selectedViewerThemeOption.vars],
  );
  const yearOptions = useMemo(() => {
    const fromPhotos = photos.map((photo) => getPhotoDateParts(photo)?.year ?? '').filter(Boolean);
    const fallbackYears = Array.from({ length: 16 }, (_, index) => String(new Date().getFullYear() - index));
    const values = Array.from(new Set([advancedFilters.year, ...fromPhotos, ...fallbackYears].filter(Boolean))).sort(
      (left, right) => Number(right) - Number(left),
    );
    return createNumericSelectOptions(values);
  }, [advancedFilters.year, photos]);
  const monthOptions = useMemo(() => {
    const filteredPhotos = photos.filter((photo) => {
      if (!advancedFilters.year) {
        return true;
      }

      return getPhotoDateParts(photo)?.year === advancedFilters.year;
    });
    const fromPhotos = filteredPhotos.map((photo) => getPhotoDateParts(photo)?.month ?? '').filter(Boolean);
    const fallbackMonths = Array.from({ length: 12 }, (_, index) => String(index + 1));
    const values = Array.from(new Set([advancedFilters.month, ...fromPhotos, ...fallbackMonths].filter(Boolean))).sort(
      (left, right) => Number(left) - Number(right),
    );
    return createNumericSelectOptions(values);
  }, [advancedFilters.month, advancedFilters.year, photos]);
  const dayOptions = useMemo(() => {
    const filteredPhotos = photos.filter((photo) => {
      const parts = getPhotoDateParts(photo);
      if (!parts) {
        return false;
      }
      if (advancedFilters.year && parts.year !== advancedFilters.year) {
        return false;
      }
      if (advancedFilters.month && parts.month !== advancedFilters.month) {
        return false;
      }
      return true;
    });
    const fromPhotos = filteredPhotos.map((photo) => getPhotoDateParts(photo)?.day ?? '').filter(Boolean);
    const fallbackDays = Array.from({ length: 31 }, (_, index) => String(index + 1));
    const values = Array.from(new Set([advancedFilters.day, ...fromPhotos, ...fallbackDays].filter(Boolean))).sort(
      (left, right) => Number(left) - Number(right),
    );
    return createNumericSelectOptions(values);
  }, [advancedFilters.day, advancedFilters.month, advancedFilters.year, photos]);
  const labelOptions = useMemo(() => {
    const values = Array.from(
      new Set([
        ...advancedFilters.label,
        ...photos.flatMap((photo) => collectPhotoRecordValues(photo, ['Labels', 'Label', 'Keywords', 'Keyword'])),
      ]),
    ).sort((left, right) => left.localeCompare(right, 'ko-KR'));
    return values.map((value) => ({ label: `#${value}`, value }));
  }, [advancedFilters.label, photos]);
  const categoryOptions = useMemo(() => {
    const values = Array.from(
      new Set([
        ...advancedFilters.category,
        ...photos.flatMap((photo) => collectPhotoRecordValues(photo, ['Categories', 'Category'])),
      ]),
    ).sort((left, right) => left.localeCompare(right, 'ko-KR'));
    return values.map((value) => ({ label: value, value }));
  }, [advancedFilters.category, photos]);
  const activeFilterCount =
    Number(hasKeywordFilter) +
    Number(hasAlbumFilter) +
    Number(Boolean(advancedFilters.year.trim())) +
    Number(Boolean(advancedFilters.month.trim())) +
    Number(Boolean(advancedFilters.day.trim())) +
    Number(normalizedLabelFilters.length > 0) +
    Number(normalizedCategoryFilters.length > 0) +
    Number(advancedFilters.favorite === 'true') +
    Number(advancedFilters.mediaType !== 'all') +
    Number(Boolean(advancedFilters.country.trim())) +
    Number(Boolean(advancedFilters.camera.trim()));
  const shouldShowAlbumPanel = !isCompactViewport || isAlbumPanelOpen;
  const previewModeLabel = useMemo(() => {
    if (previewMode === 'masonry') {
      return '모자이크';
    }

    if (previewMode === 'list') {
      return '목록';
    }

    return '격자';
  }, [previewMode]);
  const sortOrderLabel = sortOrder === 'oldest' ? '오래된순' : '최신순';
  const favoriteFilterLabel = advancedFilters.favorite === 'true' ? '즐겨찾기만' : '전체';
  const mediaTypeFilterLabel =
    advancedFilters.mediaType === 'image' ? '사진만' : advancedFilters.mediaType === 'video' ? '동영상만' : '전체';
  const dateFilterSummary = useMemo(() => {
    if (!advancedFilters.year && !advancedFilters.month && !advancedFilters.day) {
      return '전체';
    }

    return [advancedFilters.year || '전체', advancedFilters.month || '전체', advancedFilters.day || '전체'].join(' / ');
  }, [advancedFilters.day, advancedFilters.month, advancedFilters.year]);
  const activeFilterChips = useMemo(() => {
    const chips: string[] = [];

    if (hasAlbumFilter) {
      chips.push(`앨범 · ${albumTitle}`);
    }

    if (hasKeywordFilter) {
      chips.push(`검색 · ${normalizedSearchKeyword}`);
    }

    if (advancedFilters.year.trim()) {
      chips.push(`년도 · ${advancedFilters.year.trim()}`);
    }

    if (advancedFilters.month.trim()) {
      chips.push(`월 · ${advancedFilters.month.trim()}`);
    }

    if (advancedFilters.day.trim()) {
      chips.push(`일 · ${advancedFilters.day.trim()}`);
    }

    normalizedLabelFilters.forEach((value) => chips.push(`해시태그 · ${value}`));
    normalizedCategoryFilters.forEach((value) => chips.push(`카테고리 · ${value}`));

    if (advancedFilters.favorite === 'true') {
      chips.push('즐겨찾기만');
    }

    if (advancedFilters.mediaType !== 'all') {
      chips.push(`미디어 · ${mediaTypeFilterLabel}`);
    }

    if (advancedFilters.country.trim()) {
      chips.push(`국가 · ${advancedFilters.country.trim()}`);
    }

    if (advancedFilters.camera.trim()) {
      chips.push(`카메라 · ${advancedFilters.camera.trim()}`);
    }

    return chips;
  }, [
    advancedFilters.camera,
    advancedFilters.country,
    advancedFilters.day,
    advancedFilters.favorite,
    advancedFilters.mediaType,
    advancedFilters.month,
    advancedFilters.year,
    albumTitle,
    hasAlbumFilter,
    hasKeywordFilter,
    mediaTypeFilterLabel,
    normalizedCategoryFilters,
    normalizedLabelFilters,
    normalizedSearchKeyword,
  ]);

  const viewerResolutionText = useMemo(() => {
    if (!viewerDetail || typeof viewerDetail.Width !== 'number' || typeof viewerDetail.Height !== 'number') {
      return '';
    }

    return `${viewerDetail.Width.toLocaleString('ko-KR')} × ${viewerDetail.Height.toLocaleString('ko-KR')}`;
  }, [viewerDetail]);
  const viewerDetailPrimaryFile = useMemo(() => (viewerDetail ? getPrimaryPhotoFile(viewerDetail) : null), [viewerDetail]);
  const viewerFileTypeText = viewerDetailPrimaryFile?.Mime || viewerDetail?.Type || '';
  const viewerFileSizeText = formatFileSize(viewerDetail?.Filesize);
  const viewerDetailRows = useMemo(() => {
    if (!viewerDetail) {
      return [] as Array<{ key: string; label: string; value: string; tone?: 'muted' }>;
    }

    return [
      { key: 'shot-at', label: '촬영일', value: getPhotoDisplayDate(viewerDetail) || '-' },
      { key: 'place', label: '장소', value: viewerDetail.PlaceLabel || [viewerDetail.City, viewerDetail.Country].filter(Boolean).join(', ') || '-' },
      { key: 'favorite', label: '즐겨찾기', value: viewerDetail.Favorite ? '예' : '아니오' },
      { key: 'live-photo', label: '라이브 포토', value: viewerDetail.LivePhoto ? '예' : '아니오' },
      { key: 'panorama', label: '파노라마', value: viewerDetail.Panorama ? '예' : '아니오' },
      { key: 'file-name', label: '파일명', value: viewerDetail.OriginalName || viewerDetail.FileName || viewerDetailPrimaryFile?.Name || viewerDetail.UID },
      { key: 'format', label: '포맷', value: viewerFileTypeText || '-' },
      { key: 'resolution', label: '해상도', value: viewerResolutionText || '-' },
      { key: 'file-size', label: '용량', value: viewerFileSizeText || '-' },
      { key: 'camera', label: '카메라', value: viewerDetail.CameraModel || '-' },
      { key: 'lens', label: '렌즈', value: [viewerDetail.LensMake, viewerDetail.LensModel].filter(Boolean).join(' ') || '-' },
      { key: 'focal-length', label: '초점거리', value: formatPlainNumber(viewerDetail.FocalLength) ? `${formatPlainNumber(viewerDetail.FocalLength)} mm` : '-' },
      { key: 'aperture', label: '조리개', value: formatPlainNumber(viewerDetail.FNumber) ? `f/${formatPlainNumber(viewerDetail.FNumber)}` : '-' },
      { key: 'exposure', label: '노출', value: viewerDetail.Exposure || '-' },
      { key: 'iso', label: 'ISO', value: formatPlainNumber(viewerDetail.Iso) || '-' },
      { key: 'quality', label: '품질', value: formatPlainNumber(viewerDetail.Quality) || '-' },
      { key: 'hash', label: '해시', value: viewerDetailPrimaryFile?.Hash || viewerDetail.Hash || '-', tone: 'muted' },
      { key: 'file-uid', label: '파일 UID', value: viewerDetailPrimaryFile?.UID || '-', tone: 'muted' },
    ];
  }, [viewerDetail, viewerDetailPrimaryFile, viewerFileSizeText, viewerFileTypeText, viewerResolutionText]);
  const viewerMembershipAlbums = useMemo(() => normalizePhotoMembershipAlbums(viewerDetail), [viewerDetail]);
  const viewerMembershipAlbumUidSet = useMemo(
    () => new Set(viewerMembershipAlbums.map((album) => album.UID?.trim() ?? '').filter(Boolean)),
    [viewerMembershipAlbums],
  );
  const assignableAlbums = useMemo(
    () => albums.filter((album) => album.UID && album.UID !== ALL_ALBUM_ID && !viewerMembershipAlbumUidSet.has(album.UID)),
    [albums, viewerMembershipAlbumUidSet],
  );
  const assignableAlbumOptions = useMemo(
    () =>
      assignableAlbums.map((album) => ({
        label: album.Title ?? album.Name ?? 'Album',
        value: album.UID ?? '',
      })),
    [assignableAlbums],
  );
  const viewerAlbumMutationPending = viewerAlbumMutationKey.length > 0;
  const viewerAlbumMutationDisabled = !session || !viewerDisplayPhoto || isViewerDetailLoading || viewerAlbumMutationPending;

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

    if (assignableAlbumOptions.some((option) => option.value === viewerAlbumInputUid)) {
      return;
    }

    setViewerAlbumInputUid(undefined);
  }, [assignableAlbumOptions, viewerAlbumInputUid]);

  const galleryPlaceholderItems = useMemo(
    () => Array.from({ length: PLACEHOLDER_CARD_COUNT }, (_, index) => `placeholder-${index}`),
    [],
  );
  const isPullRefreshContentLoading = isPullRefreshing && isBusy;
  const pullToRefreshVisualHeight = isPullRefreshing ? 0 : pullToRefreshOffset;
  const pullToRefreshProgress = Math.max(
    0,
    Math.min(1, pullToRefreshOffset / PULL_TO_REFRESH_TRIGGER_VISIBLE_PX),
  );
  const favoritePendingUidSet = useMemo(() => new Set(favoritePendingUids), [favoritePendingUids]);
  const rangeTargetUidSet = useMemo(() => new Set(rangeTargetUids), [rangeTargetUids]);
  const photoSections = useMemo(() => {
    const sections: Array<{ key: string; label: string; photos: PhotoPrismPhoto[] }> = [];

    photos.forEach((photo) => {
      const label = getPhotoSectionLabel(photo);
      const key = label === '날짜 없음' ? 'section-undated' : `${label}-${sections.length}`;
      const lastSection = sections.at(-1);

      if (!lastSection || lastSection.label !== label) {
        sections.push({
          key,
          label,
          photos: [photo],
        });
        return;
      }

      lastSection.photos.push(photo);
    });

    return sections;
  }, [photos]);
  const preloadTriggerPhotoUid = useMemo(() => {
    if (!hasMorePhotos || photos.length === 0) {
      return '';
    }

    const leadCount = Math.min(
      PRELOAD_TRIGGER_MAX_REMAINING,
      Math.max(PRELOAD_TRIGGER_MIN_REMAINING, Math.round(PHOTO_PAGE_SIZE * 0.35)),
    );
    const triggerIndex = Math.max(0, photos.length - Math.min(leadCount, photos.length));

    return photos[triggerIndex]?.UID ?? '';
  }, [hasMorePhotos, photos]);

  const updateSectionSelection = (sectionPhotos: PhotoPrismPhoto[], mode: 'select' | 'clear') => {
    if (!sectionPhotos.length) {
      return;
    }

    if (mode === 'select') {
      setSelectedPhotoUid(sectionPhotos[0].UID);
    }

    setSelectedPhotoUids((current) => {
      const nextSelection = new Set(current);

      sectionPhotos.forEach((photo) => {
        if (mode === 'select') {
          nextSelection.add(photo.UID);
        } else {
          nextSelection.delete(photo.UID);
        }
      });

      return Array.from(nextSelection);
    });
  };

  const resetFilterState = useCallback(() => {
    setSearchKeyword('');
    setAdvancedFilters(INITIAL_ADVANCED_FILTERS);
    setActiveAlbumUid(ALL_ALBUM_ID);
    setSelectedPhotoUids([]);
    closeViewer();
  }, []);

  const handleMobileFilterResetClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
    event.preventDefault();
    event.stopPropagation();
    resetFilterState();
    setIsAlbumPanelOpen(true);
  }, [resetFilterState]);

  const updateAdvancedFilter = useCallback(
    <Key extends keyof PhotoPrismAdvancedFilters>(key: Key, value: PhotoPrismAdvancedFilters[Key]) => {
      setAdvancedFilters((current) => ({
        ...current,
        [key]: value,
      }));
      setSelectedPhotoUids([]);
      closeViewer();
    },
    [],
  );

  const loadMorePhotos = useCallback(async () => {
    if (!session || isBusy || isLoadingMoreRef.current || !hasMorePhotos) {
      return;
    }

    isLoadingMoreRef.current = true;
    setIsLoadingMore(true);
    const requestedOffset =
      typeof meta.nextOffset === 'number' && Number.isFinite(meta.nextOffset) ? Math.max(0, Math.round(meta.nextOffset)) : photos.length;

    try {
      const response = await fetchPhotoPrismPhotos({
        baseUrl: session.baseUrl,
        token: session.token,
        albumUid: activeAlbumUid === ALL_ALBUM_ID ? undefined : activeAlbumUid,
        order: sortOrder,
        q: activeSearchQuery,
        count: PHOTO_PAGE_SIZE,
        offset: requestedOffset,
      });
      const normalizedMeta = normalizePhotoSearchMeta(response.meta, {
        requestedOffset,
        receivedCount: response.items.length,
        requestedCount: PHOTO_PAGE_SIZE,
      });

      setPhotos((current) => {
        if (current.length === 0) {
          return response.items;
        }

        const seen = new Set(current.map((photo) => photo.UID));
        const appended = response.items.filter((photo) => !seen.has(photo.UID));
        return appended.length > 0 ? [...current, ...appended] : current;
      });
      setMeta(normalizedMeta);
      setHasMorePhotos(normalizedMeta.hasMore === true);
    } catch (error) {
      const messageText = error instanceof Error ? error.message : '추가 사진을 불러오지 못했습니다.';
      messageApi.warning(messageText);
    } finally {
      isLoadingMoreRef.current = false;
      setIsLoadingMore(false);
    }
  }, [activeAlbumUid, activeSearchQuery, hasMorePhotos, isBusy, messageApi, meta.nextOffset, photos.length, session, sortOrder]);

  useEffect(() => {
    const gallery = galleryRef.current;
    const preloadTrigger = galleryPreloadTriggerRef.current;

    if (!gallery || !preloadTrigger || !session || isBusy || !hasMorePhotos) {
      return;
    }

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries.some((entry) => entry.isIntersecting)) {
          void loadMorePhotos();
        }
      },
      {
        root: gallery,
        rootMargin: '120px 0px 240px 0px',
      },
    );

    observer.observe(preloadTrigger);

    return () => {
      observer.disconnect();
    };
  }, [hasMorePhotos, isBusy, loadMorePhotos, preloadTriggerPhotoUid, session]);

  useEffect(() => {
    const gallery = galleryRef.current;

    if (!gallery || !session || isBusy || !hasMorePhotos || isLoadingMore) {
      return;
    }

    if (gallery.scrollHeight <= gallery.clientHeight + 48) {
      void loadMorePhotos();
    }
  }, [hasMorePhotos, isBusy, isLoadingMore, loadMorePhotos, photos, previewMode, densityMode, session]);

  const openViewer = (uid: string) => {
    const targetPhoto = photos.find((photo) => photo.UID === uid);

    if (!targetPhoto) {
      return;
    }

    setSelectedPhotoUid(uid);

    setViewerDisplayTab('photo');
    setIsViewerActionsVisible(true);
    setViewerPhotoUid(uid);
  };

  const navigateViewer = (direction: 'previous' | 'next') => {
    if (viewerPhotoIndex < 0) {
      return;
    }

    const nextIndex = direction === 'previous' ? viewerPhotoIndex - 1 : viewerPhotoIndex + 1;
    const nextPhoto = viewerPhotos[nextIndex];

    if (!nextPhoto) {
      return;
    }

    openViewer(nextPhoto.UID);
  };

  const closeViewer = () => {
    clearViewerActionsHideTimer();
    setViewerPhotoUid('');
    setViewerDetailPhoto(null);
    setIsViewerDetailLoading(false);
    setIsViewerActionsVisible(true);
    setViewerDisplayTab('photo');
    setViewerVideoError('');
    setViewerVideoState('idle');
    setViewerZoomScale(1);
    viewerTouchStartRef.current = null;
  };

  const markCardActionHandled = (uid: string, action: 'open' | 'toggle') => {
    lastCardActionRef.current = {
      uid,
      action,
      at: Date.now(),
    };
  };

  const shouldIgnoreViewerSwipe = (target: EventTarget | null) => {
    if (!(target instanceof Element)) {
      return false;
    }

    return target.closest('.photoprism-app__viewer-rail') !== null;
  };

  const handleViewerTouchStart = (event: ReactTouchEvent<HTMLDivElement>) => {
    if (viewerZoomScale > 1.01) {
      viewerTouchStartRef.current = null;
      return;
    }

    if (shouldIgnoreViewerSwipe(event.target)) {
      viewerTouchStartRef.current = null;
      return;
    }

    if (event.touches.length !== 1) {
      viewerTouchStartRef.current = null;
      return;
    }

    const touch = event.touches[0];
    viewerTouchStartRef.current = { x: touch.clientX, y: touch.clientY };
  };

  const handleViewerTouchEnd = (event: ReactTouchEvent<HTMLDivElement>) => {
    if (viewerZoomScale > 1.01) {
      viewerTouchStartRef.current = null;
      return;
    }

    if (shouldIgnoreViewerSwipe(event.target)) {
      viewerTouchStartRef.current = null;
      return;
    }

    const start = viewerTouchStartRef.current;
    viewerTouchStartRef.current = null;

    if (!start || event.changedTouches.length !== 1) {
      return;
    }

    const touch = event.changedTouches[0];
    const deltaX = touch.clientX - start.x;
    const deltaY = touch.clientY - start.y;

    if (Math.abs(deltaX) < 56 || Math.abs(deltaX) < Math.abs(deltaY) * 1.25) {
      return;
    }

    navigateViewer(deltaX > 0 ? 'previous' : 'next');
  };

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

    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        closeViewer();
        return;
      }

      if (event.key === 'ArrowLeft') {
        event.preventDefault();
        navigateViewer('previous');
        return;
      }

      if (event.key === 'ArrowRight') {
        event.preventDefault();
        navigateViewer('next');
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [viewerPhotoUid, viewerPhotoIndex, viewerPhotos]);

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

    const rail = viewerRailRef.current;

    if (!rail) {
      return;
    }

    const activeItem = rail.querySelector<HTMLElement>(`[data-photo-uid="${viewerPhotoUid}"]`);

    if (!activeItem) {
      return;
    }

    activeItem.scrollIntoView({
      block: 'nearest',
      inline: 'nearest',
    });
  }, [viewerPhotoUid]);

  useEffect(() => {
    if (!viewerPhotoUid || !session) {
      setViewerDetailPhoto(null);
      setIsViewerDetailLoading(false);
      return;
    }

    const cachedDetail = viewerDetailCacheRef.current[viewerPhotoUid];
    setViewerDetailPhoto(cachedDetail ?? viewerPhoto ?? null);

    if (viewerDisplayTab !== 'info' || cachedDetail) {
      setIsViewerDetailLoading(false);
      return;
    }

    setIsViewerDetailLoading(true);
    let cancelled = false;

    const loadViewerDetail = async () => {
      try {
        const response = await fetchPhotoPrismPhotoDetail({
          baseUrl: session.baseUrl,
          token: session.token,
          uid: viewerPhotoUid,
        });

        if (!cancelled) {
          viewerDetailCacheRef.current[viewerPhotoUid] = response.item;
          setViewerDetailPhoto(response.item);
        }
      } catch (error) {
        if (!cancelled) {
          const messageText = error instanceof Error ? error.message : '사진 상세 정보를 불러오지 못했습니다.';
          messageApi.warning(messageText);
        }
      } finally {
        if (!cancelled) {
          setIsViewerDetailLoading(false);
        }
      }
    };

    void loadViewerDetail();

    return () => {
      cancelled = true;
    };
  }, [messageApi, session, viewerDisplayTab, viewerPhoto, viewerPhotoUid]);

  useEffect(() => {
    if (!viewerPhotoUid) {
      setViewerMediaState('idle');
      return;
    }

    if (viewerPhotoUrl || viewerPhotoThumbUrl) {
      setViewerMediaState('loading');
      return;
    }

    setViewerMediaState('idle');
  }, [viewerPhotoThumbUrl, viewerPhotoUid, viewerPhotoUrl]);

  useEffect(() => {
    if (isViewerVideo && viewerPhotoUrl) {
      setViewerVideoState('loading');
      setViewerVideoError('');
      return;
    }

    setViewerVideoState('idle');
    setViewerVideoError('');
  }, [isViewerVideo, viewerPhotoUid, viewerPhotoUrl]);

  const handleViewerVideoReady = () => {
    setViewerMediaState('ready');
    setViewerVideoState('ready');
    setViewerVideoError('');
  };

  const handleViewerImageReady = () => {
    setViewerMediaState('ready');
  };

  const handleViewerImageError = () => {
    setViewerMediaState('error');
  };

  const handleViewerVideoError = (event?: SyntheticEvent<HTMLVideoElement>) => {
    const mediaElement = event?.currentTarget ?? viewerVideoRef.current;
    const mediaError = mediaElement?.error;
    const mime = viewerVideoMime || '동영상';
    const mediaErrorCode = mediaError?.code;
    const errorSuffix =
      mediaErrorCode === 4
        ? '브라우저가 이 스트림 형식 또는 코덱을 지원하지 않을 수 있습니다.'
        : mediaErrorCode === 2
          ? '스트림 응답이 중간에 끊겼습니다.'
        : mediaErrorCode === 3
            ? '브라우저가 스트림을 해석하지 못했습니다.'
            : '원본 스트림 또는 브라우저 코덱 지원을 확인해 주세요.';
    setViewerMediaState('error');
    setViewerVideoState('error');
    setViewerVideoError(`${mime} 재생을 시작하지 못했습니다. ${errorSuffix}${viewerVideoSourceWarning ? ` ${viewerVideoSourceWarning}` : ''}`);
  };

  const handleLogin = async () => {
    try {
      const values = await form.validateFields();
      setIsBusy(true);
      const response = await createPhotoPrismSession({
        baseUrl: values.baseUrl,
        username: values.username,
        password: values.password,
      });
      const nextSession: PhotoPrismStoredSession = {
        baseUrl: values.baseUrl.trim().replace(/\/+$/, ''),
        username: values.username.trim(),
        password: values.password,
        rememberPassword: values.rememberPassword === true,
        token: response.token,
      };

      writeStoredSession(nextSession);
      setSession(nextSession);
      setActiveAlbumUid(ALL_ALBUM_ID);
      setSearchKeyword('');
      messageApi.success('연결되었습니다.');
    } catch (error) {
      const messageText = error instanceof Error ? error.message : 'PhotoPrism 로그인에 실패했습니다.';
      messageApi.error(messageText);
    } finally {
      setIsBusy(false);
    }
  };

  const handleLogout = () => {
    writeStoredSession(null);
    viewerDetailCacheRef.current = {};
    setSession(null);
    setAlbums([]);
    setPhotos([]);
    setSelectedPhotoUid('');
    setSelectedPhotoUids([]);
  };

  const applyPhotoFavoriteState = (uid: string, favorite: boolean, nextDetailPhoto?: PhotoPrismPhoto | null) => {
    setPhotos((current) => current.map((photo) => (photo.UID === uid ? { ...photo, Favorite: favorite } : photo)));
    const cachedPhoto = viewerDetailCacheRef.current[uid];
    if (cachedPhoto) {
      viewerDetailCacheRef.current[uid] = { ...cachedPhoto, Favorite: favorite };
    }
    if (nextDetailPhoto?.UID === uid) {
      viewerDetailCacheRef.current[uid] = nextDetailPhoto;
    }
    setViewerDetailPhoto((current) => {
      if (nextDetailPhoto && nextDetailPhoto.UID === uid) {
        return nextDetailPhoto;
      }

      return current?.UID === uid ? { ...current, Favorite: favorite } : current;
    });
  };

  const applyViewerAlbumMembershipState = useCallback(
    (nextDetailPhoto: PhotoPrismPhoto, options?: { removedAlbumUid?: string }) => {
      viewerDetailCacheRef.current[nextDetailPhoto.UID] = nextDetailPhoto;
      setViewerDetailPhoto((current) => (current?.UID === nextDetailPhoto.UID ? nextDetailPhoto : current));

      const shouldRemoveFromCurrentAlbum =
        Boolean(options?.removedAlbumUid) && activeAlbumUid !== ALL_ALBUM_ID && options?.removedAlbumUid === activeAlbumUid;

      if (shouldRemoveFromCurrentAlbum) {
        setPhotos((current) => current.filter((photo) => photo.UID !== nextDetailPhoto.UID));
        setSelectedPhotoUids((current) => current.filter((uid) => uid !== nextDetailPhoto.UID));
        setSelectedPhotoUid((current) => (current === nextDetailPhoto.UID ? '' : current));
        closeViewer();
        return;
      }

      setPhotos((current) =>
        current.map((photo) => (photo.UID === nextDetailPhoto.UID ? { ...photo, Albums: nextDetailPhoto.Albums } : photo)),
      );
    },
    [activeAlbumUid, closeViewer],
  );

  const handleAddViewerPhotoToAlbum = async () => {
    if (!session || !viewerDisplayPhoto || !viewerAlbumInputUid || viewerAlbumMutationPending) {
      return;
    }

    const album = albums.find((item) => item.UID === viewerAlbumInputUid);
    const albumName = album?.Title ?? album?.Name ?? '선택한 앨범';
    setViewerAlbumMutationKey(`add:${viewerAlbumInputUid}`);

    try {
      const response = await updatePhotoPrismPhotoAlbumMembership({
        baseUrl: session.baseUrl,
        token: session.token,
        uid: viewerDisplayPhoto.UID,
        albumUid: viewerAlbumInputUid,
        action: 'add',
      });

      applyViewerAlbumMembershipState(response.item);
      setViewerAlbumInputUid(undefined);
      messageApi.success(`${albumName}에 등록했습니다.`);
    } catch (error) {
      messageApi.warning(error instanceof Error ? error.message : '앨범 등록을 완료하지 못했습니다.');
    } finally {
      setViewerAlbumMutationKey('');
    }
  };

  const handleRemoveViewerPhotoFromAlbum = async (album: PhotoPrismAlbum) => {
    const albumUid = album.UID?.trim() ?? '';

    if (!session || !viewerDisplayPhoto || !albumUid || viewerAlbumMutationPending) {
      return;
    }

    const albumName = album.Title ?? album.Name ?? '선택한 앨범';
    setViewerAlbumMutationKey(`remove:${albumUid}`);

    try {
      const response = await updatePhotoPrismPhotoAlbumMembership({
        baseUrl: session.baseUrl,
        token: session.token,
        uid: viewerDisplayPhoto.UID,
        albumUid,
        action: 'remove',
      });

      applyViewerAlbumMembershipState(response.item, { removedAlbumUid: albumUid });
      messageApi.success(`${albumName}에서 해제했습니다.`);
    } catch (error) {
      messageApi.warning(error instanceof Error ? error.message : '앨범 해제를 완료하지 못했습니다.');
    } finally {
      setViewerAlbumMutationKey('');
    }
  };

  const handleToggleFavorite = async (photo: PhotoPrismPhoto) => {
    if (!session || favoritePendingUidSet.has(photo.UID)) {
      return;
    }

    const nextFavorite = photo.Favorite !== true;
    setFavoritePendingUids((current) => [...current, photo.UID]);
    applyPhotoFavoriteState(photo.UID, nextFavorite);

    try {
      const response = await updatePhotoPrismPhotoFavorite({
        baseUrl: session.baseUrl,
        token: session.token,
        uid: photo.UID,
        favorite: nextFavorite,
      });

      applyPhotoFavoriteState(photo.UID, response.item?.Favorite === true, response.item ?? null);
    } catch (error) {
      applyPhotoFavoriteState(photo.UID, photo.Favorite === true, photo);
      messageApi.warning(error instanceof Error ? error.message : '즐겨찾기 상태를 변경하지 못했습니다.');
    } finally {
      setFavoritePendingUids((current) => current.filter((uid) => uid !== photo.UID));
    }
  };

  const viewerRail = viewerPhotos.length > 1 ? (
    <div ref={viewerRailRef} className="photoprism-app__viewer-rail" role="tablist" aria-label="미디어 슬라이더">
      {viewerPhotos.map((photo) => {
        const thumbUrl = photoThumbUrls[photo.UID] ?? '';
        const isActive = photo.UID === viewerPhotoUid;
        const isVideo = isPhotoPrismVideo(photo);

        return (
          <button
            key={photo.UID}
            data-photo-uid={photo.UID}
            type="button"
            role="tab"
            aria-selected={isActive}
            className={`photoprism-app__viewer-rail-item${isActive ? ' photoprism-app__viewer-rail-item--active' : ''}`}
            onClick={() => openViewer(photo.UID)}
          >
            {thumbUrl ? <img src={thumbUrl} alt={getPhotoDisplayTitle(photo)} loading="lazy" decoding="async" /> : null}
            <span className="photoprism-app__viewer-rail-media-badge" aria-hidden="true">
              {isVideo ? <PlayCircleFilled /> : <PictureOutlined />}
            </span>
          </button>
        );
      })}
    </div>
  ) : null;
  const togglePhotoSelection = (uid: string) => {
    setSelectedPhotoUid(uid);
    setSelectedPhotoUids((current) =>
      current.includes(uid) ? current.filter((item) => item !== uid) : [...current, uid],
    );
  };

  const clearDragSelectionTimer = () => {
    if (dragSelectionRef.current?.timerId) {
      window.clearTimeout(dragSelectionRef.current.timerId);
      dragSelectionRef.current.timerId = null;
    }
  };

  const clearDragSelectionFrame = () => {
    if (dragSelectionRef.current?.rafId) {
      window.cancelAnimationFrame(dragSelectionRef.current.rafId);
      dragSelectionRef.current.rafId = null;
    }
  };

  const clearDragAutoScrollFrame = () => {
    if (dragAutoScrollFrameRef.current) {
      window.cancelAnimationFrame(dragAutoScrollFrameRef.current);
      dragAutoScrollFrameRef.current = null;
    }
  };

  const finishDragSelection = () => {
    clearDragSelectionTimer();
    clearDragSelectionFrame();
    clearDragAutoScrollFrame();
    dragSelectionRef.current = null;
    setIsRangeSelecting(false);
    setRangeTargetUids([]);
  };

  const resolvePhotoUidFromPoint = (clientX: number, clientY: number) => {
    if (typeof document !== 'undefined') {
      const matchedElement = document
        .elementsFromPoint(clientX, clientY)
        .find(
          (element): element is HTMLElement =>
            element instanceof HTMLElement && Boolean(element.closest<HTMLElement>('[data-photoprism-photo-uid]')),
        );
      const matchedCard = matchedElement?.closest<HTMLElement>('[data-photoprism-photo-uid]');
      const matchedUid = matchedCard?.dataset.photoprismPhotoUid;

      if (matchedUid) {
        return matchedUid;
      }
    }

    const state = dragSelectionRef.current;

    if (!state) {
      return null;
    }

    const gallery = galleryRef.current;
    const scrollOffsetY = (gallery?.scrollTop ?? state.startScrollTop) - state.startScrollTop;
    let nearestUid = state.currentUid || state.startUid;
    let nearestScore = Number.POSITIVE_INFINITY;

    state.photoRects.forEach((rect, uid) => {
      const adjustedTop = rect.top - scrollOffsetY;
      const adjustedBottom = rect.bottom - scrollOffsetY;

      if (clientX >= rect.left && clientX <= rect.right && clientY >= adjustedTop && clientY <= adjustedBottom) {
        nearestUid = uid;
        nearestScore = 0;
        return;
      }

      if (nearestScore === 0) {
        return;
      }

      const deltaX = clientX < rect.left ? rect.left - clientX : clientX > rect.right ? clientX - rect.right : 0;
      const deltaY = clientY < adjustedTop ? adjustedTop - clientY : clientY > adjustedBottom ? clientY - adjustedBottom : 0;
      const score = deltaX * deltaX + deltaY * deltaY;

      if (score < nearestScore) {
        nearestScore = score;
        nearestUid = uid;
      }
    });

    return nearestUid;
  };

  const recalculateDragSelection = () => {
    const state = dragSelectionRef.current;

    if (!state || !state.longPressActivated || !state.mode) {
      return;
    }

    state.rafId = null;
    const nextCurrentUid = resolvePhotoUidFromPoint(state.latestX, state.latestY) ?? state.currentUid ?? state.startUid;
    const anchorIndex = state.photoOrder.indexOf(state.startUid);
    const currentIndex = state.photoOrder.indexOf(nextCurrentUid);

    if (anchorIndex < 0 || currentIndex < 0) {
      return;
    }

    state.currentUid = nextCurrentUid;
    const rangeStart = Math.min(anchorIndex, currentIndex);
    const rangeEnd = Math.max(anchorIndex, currentIndex);
    const nextRangeTargetUids = state.photoOrder.slice(rangeStart, rangeEnd + 1);

    const nextSelectionSet = new Set(state.baselineSelectedUids);

    nextRangeTargetUids.forEach((uid) => {
      if (state.mode === 'select') {
        nextSelectionSet.add(uid);
      } else {
        nextSelectionSet.delete(uid);
      }
    });

    setRangeTargetUids(nextRangeTargetUids);
    setSelectedPhotoUid(nextCurrentUid);
    setSelectedPhotoUids(Array.from(nextSelectionSet));
  };

  const getAutoScrollDelta = (pointerY: number, gallery: HTMLDivElement) => {
    const state = dragSelectionRef.current;
    const rect = gallery.getBoundingClientRect();
    const buildDelta = (direction: 'up' | 'down', ratio: number) => {
      const clampedRatio = Math.max(0, Math.min(1, ratio));
      const edgeProgress = Math.max(clampedRatio, Math.pow(clampedRatio, 3));
      const dwellElapsed = state?.autoScrollSince ? performance.now() - state.autoScrollSince : 0;
      const dwellBoost = 1 + Math.min(0.85, dwellElapsed / DRAG_SCROLL_DWELL_ACCEL_MS);
      const directionalBias = direction === 'down' ? DRAG_SCROLL_DOWNWARD_BIAS : 1;
      const speed = Math.min(
        DRAG_SCROLL_MAX_SPEED,
        Math.max(DRAG_SCROLL_MIN_SPEED, edgeProgress * DRAG_SCROLL_MAX_SPEED * directionalBias * dwellBoost),
      );

      return direction === 'up' ? -speed : speed;
    };

    if (pointerY < rect.top + DRAG_SCROLL_EDGE_PX) {
      const ratio = Math.max(0, Math.min(1, (rect.top + DRAG_SCROLL_EDGE_PX - pointerY) / DRAG_SCROLL_EDGE_PX));
      return buildDelta('up', ratio);
    }

    if (pointerY > rect.bottom - DRAG_SCROLL_EDGE_PX) {
      const ratio = Math.max(0, Math.min(1, (pointerY - (rect.bottom - DRAG_SCROLL_EDGE_PX)) / DRAG_SCROLL_EDGE_PX));
      return buildDelta('down', ratio);
    }

    return 0;
  };

  const scheduleDragSelectionRecalc = () => {
    const state = dragSelectionRef.current;

    if (!state || state.rafId) {
      return;
    }

    state.rafId = window.requestAnimationFrame(() => {
      recalculateDragSelection();
    });
  };

  const startDragAutoScroll = () => {
    if (dragAutoScrollFrameRef.current) {
      return;
    }

    const tick = () => {
      const state = dragSelectionRef.current;
      const gallery = galleryRef.current;

      if (!state || !gallery || !state.longPressActivated) {
        dragAutoScrollFrameRef.current = null;
        return;
      }

      const delta = getAutoScrollDelta(state.latestY, gallery);

      if (delta !== 0) {
        const nextDirection = delta < 0 ? 'up' : 'down';

        if (state.autoScrollDirection !== nextDirection) {
          state.autoScrollDirection = nextDirection;
          state.autoScrollSince = performance.now();
        }

        const maxScrollTop = Math.max(0, gallery.scrollHeight - gallery.clientHeight);
        const nextScrollTop = Math.max(0, Math.min(maxScrollTop, gallery.scrollTop + delta));

        if (nextScrollTop !== gallery.scrollTop) {
          gallery.scrollTop = nextScrollTop;
          scheduleDragSelectionRecalc();
        }
      } else if (state.autoScrollDirection) {
        state.autoScrollDirection = null;
        state.autoScrollSince = null;
      }

      dragAutoScrollFrameRef.current = window.requestAnimationFrame(tick);
    };

    dragAutoScrollFrameRef.current = window.requestAnimationFrame(tick);
  };

  const activateDragSelection = () => {
    const state = dragSelectionRef.current;
    const gallery = galleryRef.current;

    if (!state || !gallery || state.longPressActivated || state.movedBeforeLongPress) {
      return;
    }

    state.longPressActivated = true;
    state.mode = state.initialSelectionMode && state.initialSelected ? 'deselect' : 'select';
    state.target?.setPointerCapture?.(state.pointerId);
    setIsRangeSelecting(true);
    state.photoRects = new Map(
      Array.from(gallery.querySelectorAll<HTMLElement>('[data-photoprism-photo-uid]')).map((element) => {
        const uid = element.dataset.photoprismPhotoUid ?? '';
        const rect = element.getBoundingClientRect();

        return [
          uid,
          {
            left: rect.left,
            top: rect.top,
            right: rect.right,
            bottom: rect.bottom,
          },
        ] as const;
      }),
    );
    state.startScrollTop = gallery.scrollTop;
    recalculateDragSelection();
    startDragAutoScroll();
  };

  const handleCardPointerDown = (event: React.PointerEvent<HTMLElement>, uid: string) => {
    if (isOverlayActionTarget(event.target)) {
      return;
    }

    if (event.pointerType === 'mouse' && event.button !== 0) {
      return;
    }

    const initialSelected = selectedPhotoUids.includes(uid);
    finishDragSelection();
    dragSelectionRef.current = {
      pointerId: event.pointerId,
      startUid: uid,
      currentUid: uid,
      startX: event.clientX,
      startY: event.clientY,
      initialSelectionMode: isSelectionMode,
      initialSelected,
      longPressActivated: false,
      movedBeforeLongPress: false,
      mode: null,
      baselineSelectedUids: new Set(selectedPhotoUids),
      latestX: event.clientX,
      latestY: event.clientY,
      startScrollTop: galleryRef.current?.scrollTop ?? 0,
      photoOrder: photos.map((photo) => photo.UID),
      photoRects: new Map<string, RectBounds>(),
      target: event.currentTarget,
      rafId: null,
      autoScrollDirection: null,
      autoScrollSince: null,
      timerId: window.setTimeout(() => {
        activateDragSelection();
      }, LONG_PRESS_MS),
    };
  };

  const handleCardPointerMove = (event: React.PointerEvent<HTMLElement>) => {
    const state = dragSelectionRef.current;

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

    if (!state.longPressActivated) {
      state.latestX = event.clientX;
      state.latestY = event.clientY;
      const deltaX = Math.abs(event.clientX - state.startX);
      const deltaY = Math.abs(event.clientY - state.startY);

      if (deltaX > LONG_PRESS_MOVE_TOLERANCE || deltaY > LONG_PRESS_MOVE_TOLERANCE) {
        state.movedBeforeLongPress = true;
        clearDragSelectionTimer();
      }

      return;
    }

    event.preventDefault();
    state.latestX = event.clientX;
    state.latestY = event.clientY;
    scheduleDragSelectionRecalc();
    startDragAutoScroll();
  };

  const handleCardPointerUp = (event: React.PointerEvent<HTMLElement>, uid: string) => {
    if (isOverlayActionTarget(event.target)) {
      return;
    }

    const state = dragSelectionRef.current;

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

    const wasLongPress = state.longPressActivated;
    const movedBeforeLongPress = state.movedBeforeLongPress;
    state.target?.releasePointerCapture?.(event.pointerId);
    finishDragSelection();

    if (wasLongPress || movedBeforeLongPress) {
      return;
    }

    if (state.initialSelectionMode) {
      markCardActionHandled(uid, 'toggle');
      togglePhotoSelection(uid);
      return;
    }

    markCardActionHandled(uid, 'open');
    openViewer(uid);
  };

  const handleCardPointerCancel = (event: React.PointerEvent<HTMLElement>) => {
    if (dragSelectionRef.current?.pointerId !== event.pointerId) {
      return;
    }

    dragSelectionRef.current.target?.releasePointerCapture?.(event.pointerId);
    finishDragSelection();
  };

  const handleCardKeyDown = (event: React.KeyboardEvent<HTMLElement>, uid: string) => {
    if (event.key !== 'Enter' && event.key !== ' ') {
      return;
    }

    event.preventDefault();

    if (isSelectionMode) {
      markCardActionHandled(uid, 'toggle');
      togglePhotoSelection(uid);
      return;
    }

    markCardActionHandled(uid, 'open');
    openViewer(uid);
  };

  const handleCardClick = (event: ReactMouseEvent<HTMLElement>, uid: string) => {
    if (isOverlayActionTarget(event.target) || isRangeSelecting) {
      return;
    }

    const lastAction = lastCardActionRef.current;

    if (lastAction && lastAction.uid === uid && Date.now() - lastAction.at < 400) {
      return;
    }

    if (isSelectionMode) {
      markCardActionHandled(uid, 'toggle');
      togglePhotoSelection(uid);
      return;
    }

    markCardActionHandled(uid, 'open');
    openViewer(uid);
  };

  const resetPullToRefreshState = useCallback(() => {
    pullToRefreshStateRef.current = null;
    setPullToRefreshOffset(0);
    setIsPullToRefreshArmed(false);
  }, []);

  const handleGalleryTouchStart = useCallback(
    (event: ReactTouchEvent<HTMLDivElement>) => {
      if (!session || isRangeSelecting || isBusy || isLoadingMoreRef.current || event.touches.length !== 1) {
        resetPullToRefreshState();
        return;
      }

      const gallery = galleryRef.current;
      const touch = event.touches[0];
      const scrollHost = gallery ? resolvePullToRefreshScrollHost(event.target, gallery) : null;

      if (!gallery || !touch || !scrollHost || !isPullToRefreshAtTop(scrollHost)) {
        resetPullToRefreshState();
        return;
      }

      pullToRefreshStateRef.current = {
        startX: touch.clientX,
        startY: touch.clientY,
        active: true,
        cancelled: false,
        scrollHost,
      };
    },
    [isBusy, isRangeSelecting, resetPullToRefreshState, session],
  );

  const handleGalleryTouchMove = useCallback(
    (event: ReactTouchEvent<HTMLDivElement>) => {
      const state = pullToRefreshStateRef.current;
      const gallery = galleryRef.current;
      const touch = event.touches[0];
      const scrollHost = state?.scrollHost ?? gallery;

      if (!state || !state.active || state.cancelled || !gallery || !touch || !scrollHost) {
        return;
      }

      if (event.touches.length !== 1 || isPullRefreshing) {
        state.cancelled = true;
        resetPullToRefreshState();
        return;
      }

      if (!isPullToRefreshAtTop(scrollHost)) {
        resetPullToRefreshState();
        return;
      }

      const deltaX = touch.clientX - state.startX;
      const deltaY = touch.clientY - state.startY;

      if (deltaY <= 0) {
        setPullToRefreshOffset(0);
        setIsPullToRefreshArmed(false);
        return;
      }

      if (
        Math.abs(deltaX) > PULL_TO_REFRESH_HORIZONTAL_CANCEL_PX &&
        Math.abs(deltaX) > deltaY / PULL_TO_REFRESH_VERTICAL_INTENT_RATIO
      ) {
        state.cancelled = true;
        resetPullToRefreshState();
        return;
      }

      if (deltaY <= PULL_TO_REFRESH_START_THRESHOLD_PX) {
        setPullToRefreshOffset(0);
        setIsPullToRefreshArmed(false);
        return;
      }

      if (event.cancelable) {
        event.preventDefault();
      }

      const adjustedDeltaY = deltaY - PULL_TO_REFRESH_START_THRESHOLD_PX;
      const nextOffset = Math.min(PULL_TO_REFRESH_MAX_VISIBLE_PX, adjustedDeltaY * PULL_TO_REFRESH_DAMPING);
      setPullToRefreshOffset(nextOffset);
      setIsPullToRefreshArmed(nextOffset >= PULL_TO_REFRESH_TRIGGER_VISIBLE_PX);
    },
    [isPullRefreshing, resetPullToRefreshState],
  );

  const handleGalleryTouchEnd = useCallback(() => {
    const shouldRefresh = isPullToRefreshArmed && !isPullRefreshing;
    resetPullToRefreshState();

    if (shouldRefresh) {
      refreshGallery('pull');
    }
  }, [isPullRefreshing, isPullToRefreshArmed, refreshGallery, resetPullToRefreshState]);

  const stopOverlayActionEvent = (
    event:
      | React.PointerEvent<HTMLElement>
      | ReactMouseEvent<HTMLElement>
      | React.KeyboardEvent<HTMLElement>,
  ) => {
    event.preventDefault();
    event.stopPropagation();
  };

  const handlePhotoDetailAction = (
    event:
      | React.PointerEvent<HTMLButtonElement>
      | ReactMouseEvent<HTMLButtonElement>
      | React.KeyboardEvent<HTMLButtonElement>,
    uid: string,
  ) => {
    stopOverlayActionEvent(event);
    markCardActionHandled(uid, 'open');
    openViewer(uid);
  };

  const handleDownloadSelected = () => {
    if (!session || !meta.downloadToken) {
      messageApi.warning('다운로드 토큰이 아직 준비되지 않았습니다.');
      return;
    }

    if (!actionTargetPhotos.length) {
      messageApi.info('다운로드할 사진이 없습니다.');
      return;
    }

    actionTargetPhotos.forEach((photo, index) => {
      const primaryFile = getPrimaryPhotoFile(photo);
      const url = buildPhotoPrismDownloadUrl({
        baseUrl: session.baseUrl,
        token: session.token,
        downloadToken: meta.downloadToken,
        uid: photo.UID,
        hash: primaryFile?.Hash ?? photo.Hash,
      });

      window.setTimeout(() => {
        const link = document.createElement('a');
        link.href = url;
        link.download = buildShareFileName(photo);
        link.rel = 'noreferrer';
        document.body.append(link);
        link.click();
        link.remove();
      }, index * 120);
    });

    messageApi.success(
      selectedPhotos.length > 0 ? `선택한 사진 ${actionTargetPhotos.length}장을 다운로드합니다.` : '현재 사진 다운로드를 시작합니다.',
    );
  };

  const handleShareSelected = async () => {
    if (!session || !meta.downloadToken) {
      return;
    }

    if (!actionTargetPhotos.length) {
      messageApi.info('공유할 사진이 없습니다.');
      return;
    }

    const shareTargets = actionTargetPhotos.map((photo) => {
      const primaryFile = getPrimaryPhotoFile(photo);
      const url = buildPhotoPrismDownloadUrl({
        baseUrl: session.baseUrl,
        token: session.token,
        downloadToken: meta.downloadToken,
        uid: photo.UID,
        hash: primaryFile?.Hash ?? photo.Hash,
      });

      return {
        photo,
        url,
        fileName: buildShareFileName(photo),
      };
    });
    const shareTitle =
      shareTargets.length > 1
        ? `${getPhotoDisplayTitle(shareTargets[0].photo)} 외 ${shareTargets.length - 1}장`
        : getPhotoDisplayTitle(shareTargets[0].photo);
    const shareText = shareTargets.map((target) => target.url).join('\n');
    const estimatedShareBytes = shareTargets.reduce((total, target) => total + (target.photo.Filesize ?? 0), 0);

    const copyShareLinks = async (noticeMessage: string) => {
      if (!navigator.clipboard?.writeText) {
        throw new Error('이 브라우저에서는 링크 복사를 지원하지 않습니다.');
      }

      await navigator.clipboard.writeText(shareText);
      messageApi.success(noticeMessage);
    };

    const fallbackToLinkShare = async (noticeMessage: string) => {
      await copyShareLinks(noticeMessage);
    };

    try {
      if (!navigator.share) {
        await copyShareLinks(`공유용 다운로드 링크 ${shareTargets.length}건을 클립보드에 복사했습니다.`);
        return;
      }

      if (shareTargets.length > MOBILE_SHARE_FILE_LIMIT || estimatedShareBytes > MOBILE_SHARE_TOTAL_BYTES_LIMIT) {
        await fallbackToLinkShare(
          `선택한 사진이 많아 파일 공유 대신 다운로드 링크 ${shareTargets.length}건을 복사했습니다.`,
        );
        return;
      }

      const files = await Promise.all(
        shareTargets.map(async (target) => {
          const response = await fetch(target.url);

          if (!response.ok) {
            throw new Error('원본 파일을 가져오지 못했습니다.');
          }

          const blob = await response.blob();
          return new File([blob], target.fileName, {
            type: blob.type || 'application/octet-stream',
          });
        }),
      );

      const canShareFiles = typeof navigator.canShare !== 'function' || navigator.canShare({ files });

      if (canShareFiles) {
        try {
          await navigator.share({
            files,
            title: shareTitle,
          });
          return;
        } catch (error) {
          if (error instanceof DOMException && error.name === 'AbortError') {
            return;
          }
        }
      }

      await fallbackToLinkShare(
        `이 브라우저에서는 파일 공유를 지원하지 않아 다운로드 링크 ${shareTargets.length}건을 복사했습니다.`,
      );
    } catch (error) {
      if (error instanceof DOMException && error.name === 'AbortError') {
        return;
      }

      messageApi.error(error instanceof Error ? error.message : '공유를 완료하지 못했습니다.');
    }
  };

  const viewerQuickActions =
    viewerPhoto && viewerDisplayTab === 'photo' ? (
      <div className="photoprism-app__viewer-stage-sidecar" aria-label="상세보기 빠른 작업">
        <div
          className={`photoprism-app__viewer-stage-actions${
            isViewerActionsVisible ? '' : ' photoprism-app__viewer-stage-actions--hidden'
          }`}
          role="group"
          aria-label="상세보기 작업"
        >
          <Button
            type="text"
            className={`photoprism-app__viewer-stage-action${
              selectedPhotoUids.includes(viewerPhoto.UID) ? ' photoprism-app__viewer-stage-action--selected' : ''
            }`}
            aria-label={selectedPhotoUids.includes(viewerPhoto.UID) ? '현재 사진 선택 해제' : '현재 사진 선택'}
            icon={selectedPhotoUids.includes(viewerPhoto.UID) ? <CheckSquareOutlined /> : <BorderOutlined />}
            onClick={() => {
              revealViewerActions();
              togglePhotoSelection(viewerPhoto.UID);
            }}
          >
            {selectedPhotoUids.includes(viewerPhoto.UID) ? '선택됨' : '선택'}
          </Button>
          <Button
            type="text"
            className={`photoprism-app__viewer-stage-action${viewerDetail?.Favorite ? ' photoprism-app__viewer-stage-action--favorite' : ''}`}
            aria-label={viewerDetail?.Favorite ? '현재 사진 즐겨찾기 해제' : '현재 사진 즐겨찾기 추가'}
            aria-pressed={viewerDetail?.Favorite === true}
            disabled={favoritePendingUidSet.has(viewerPhoto.UID)}
            icon={viewerDetail?.Favorite ? <HeartFilled /> : <HeartOutlined />}
            onClick={() => {
              revealViewerActions();
              void handleToggleFavorite(viewerDetail ?? viewerPhoto);
            }}
          >
            즐겨찾기
          </Button>
          <Button
            type="text"
            className="photoprism-app__viewer-stage-action"
            aria-label={selectedPhotos.length > 0 ? `선택한 사진 ${actionTargetCount}장 다운로드` : '현재 사진 다운로드'}
            icon={<DownloadOutlined />}
            onClick={() => {
              revealViewerActions();
              handleDownloadSelected();
            }}
          >
            저장
          </Button>
          <Button
            type="text"
            className="photoprism-app__viewer-stage-action"
            aria-label={selectedPhotos.length > 0 ? `선택한 사진 ${actionTargetCount}장 공유` : '현재 사진 공유'}
            icon={<ShareAltOutlined />}
            onClick={() => {
              revealViewerActions();
              void handleShareSelected();
            }}
          >
            공유
          </Button>
        </div>
      </div>
    ) : null;

  const overlay = (
    <section
      ref={appRootRef}
      className={`photoprism-app${isEmbeddedLaunch ? ' photoprism-app--embedded' : ''}`}
      data-testid="photoprism-app"
      style={themeStyle}
    >
      {contextHolder}
      <header className="photoprism-app__topbar">
        <div className="photoprism-app__brand">
            {canExitToParentApp ? (
            <Button
              type="text"
              size="large"
              className="photoprism-app__back-button"
              aria-label="앱 닫기"
              icon={<ArrowLeftOutlined />}
              onClick={onBack}
            />
            ) : null}
            <div>
              <strong>PhotoPrism</strong>
              <span>{session ? albumTitle : 'Connect'}</span>
            </div>
          </div>
          <div className="photoprism-app__actions">
            {canExitToParentApp && session ? (
              <Tooltip title="부모 앱 종료">
                <Button
                  type="text"
                  className="photoprism-app__exit-button"
                  icon={<CloseOutlined />}
                  aria-label="부모 앱으로 종료"
                  onClick={onBack}
                >
                  종료
                </Button>
              </Tooltip>
            ) : null}
            {session ? (
              isCompactViewport ? (
                <Tooltip title="도구">
                  <Button
                    type="text"
                    icon={<EllipsisOutlined />}
                    onClick={() => {
                      setIsMobileControlsOpen((current) => !current);
                      setIsAlbumPanelOpen(false);
                    }}
                    aria-label="모바일 도구 패널 토글"
                  />
                </Tooltip>
              ) : (
                <>
                  <Tooltip title="새로고침">
                    <Button type="text" icon={<ReloadOutlined />} onClick={() => refreshGallery()} />
                  </Tooltip>
                  <Tooltip title="원본">
                    <Button
                      type="text"
                      icon={<DownloadOutlined />}
                      onClick={handleDownloadSelected}
                      disabled={actionTargetCount === 0}
                      aria-label={selectedPhotos.length > 0 ? `선택한 사진 ${actionTargetCount}장 다운로드` : '현재 사진 다운로드'}
                    />
                  </Tooltip>
                  <Tooltip title="공유">
                    <Button
                      type="text"
                      icon={<ShareAltOutlined />}
                      onClick={() => void handleShareSelected()}
                      disabled={actionTargetCount === 0}
                      aria-label={selectedPhotos.length > 0 ? `선택한 사진 ${actionTargetCount}장 공유` : '현재 사진 공유'}
                    />
                  </Tooltip>
                  <Tooltip title="종료">
                    <Button type="text" icon={<LogoutOutlined />} onClick={handleLogout} />
                  </Tooltip>
                </>
              )
            ) : null}
          </div>
        </header>

      {!session ? (
        <div className="photoprism-app__login-shell">
          <div className="photoprism-app__login-card">
            <Form
              form={form}
              layout="vertical"
              initialValues={{
                baseUrl: '',
                username: '',
                password: '',
                rememberPassword: false,
              }}
            >
              <Form.Item name="baseUrl" rules={[{ required: true, message: '주소' }]}>
                <Input size="large" placeholder="https://photos.example.com" autoComplete="url" />
              </Form.Item>
              <Form.Item name="username" rules={[{ required: true, message: '아이디' }]}>
                <Input size="large" placeholder="ID" autoComplete="username" />
              </Form.Item>
              <Form.Item name="password" rules={[{ required: true, message: '비밀번호' }]}>
                <Input.Password size="large" placeholder="Password" autoComplete="current-password" />
              </Form.Item>
              <Form.Item name="rememberPassword" valuePropName="checked" className="photoprism-app__login-memory">
                <label className="photoprism-app__memory-toggle">
                  <input
                    type="checkbox"
                    checked={form.getFieldValue('rememberPassword') === true}
                    onChange={(event) => {
                      form.setFieldValue('rememberPassword', event.target.checked);
                    }}
                  />
                  <span>기억</span>
                </label>
              </Form.Item>
              <Button type="primary" block size="large" loading={isBusy} onClick={() => void handleLogin()}>
                Connect
              </Button>
            </Form>
          </div>
        </div>
      ) : (
        <div className="photoprism-app__workspace">
          {isCompactViewport && isAlbumPanelOpen ? (
            <button
              type="button"
              className="photoprism-app__sidebar-backdrop"
              aria-label="필터 패널 닫기"
              onClick={() => setIsAlbumPanelOpen(false)}
            />
          ) : null}
          {shouldShowAlbumPanel ? (
            <aside className={`photoprism-app__sidebar${isCompactViewport ? ' photoprism-app__sidebar--sheet' : ''}`}>
              {isCompactViewport ? (
                <>
                  <div className="photoprism-app__sidebar-sheet-head">
                    <div className="photoprism-app__sidebar-sheet-copy">
                      <strong>필터</strong>
                      <span>{activeFilterCount > 0 ? `${activeFilterCount}개 적용됨` : '모든 사진 표시 중'}</span>
                    </div>
                    <div className="photoprism-app__sidebar-sheet-actions">
                      {activeFilterCount > 0 ? (
                        <Button
                          type="text"
                          size="small"
                          className="photoprism-app__filter-reset"
                          onClick={handleMobileFilterResetClick}
                        >
                          초기화
                        </Button>
                      ) : null}
                      <Button
                        type="text"
                        size="small"
                        icon={<CloseCircleOutlined />}
                        onClick={() => setIsAlbumPanelOpen(false)}
                        aria-label="필터 패널 닫기"
                      />
                    </div>
                  </div>
                  <div className="photoprism-app__filter-sheet-mobile">
                    <div className="photoprism-app__filter-sheet-current">
                      <span>검색</span>
                      <strong>{activeSearchQuery || '전체 사진'}</strong>
                    </div>
                    {activeFilterChips.length > 0 ? (
                      <div className="photoprism-app__filter-chip-list">
                        {activeFilterChips.map((chip) => (
                          <span key={chip} className="photoprism-app__filter-chip photoprism-app__filter-chip--active">
                            {chip}
                          </span>
                        ))}
                      </div>
                    ) : null}
                    <section className="photoprism-app__filter-group photoprism-app__filter-group--static">
                      <div className="photoprism-app__filter-group-heading">
                        <span className="photoprism-app__filter-group-label">년/월/일</span>
                        <span className="photoprism-app__filter-group-heading-value">{dateFilterSummary}</span>
                      </div>
                      <div className="photoprism-app__filter-group-body photoprism-app__filter-group-body--fields">
                        <span className="photoprism-app__filter-field-label">년도</span>
                        <Select
                          allowClear
                          size="large"
                          value={advancedFilters.year || undefined}
                          placeholder="년도 선택"
                          options={yearOptions}
                          getPopupContainer={getFilterSelectPopupContainer}
                          onChange={(value) => updateAdvancedFilter('year', value ?? '')}
                        />
                        <span className="photoprism-app__filter-field-label">월</span>
                        <Select
                          allowClear
                          size="large"
                          value={advancedFilters.month || undefined}
                          placeholder="월 선택"
                          options={monthOptions}
                          getPopupContainer={getFilterSelectPopupContainer}
                          onChange={(value) => updateAdvancedFilter('month', value ?? '')}
                        />
                        <span className="photoprism-app__filter-field-label">일</span>
                        <Select
                          allowClear
                          size="large"
                          value={advancedFilters.day || undefined}
                          placeholder="일 선택"
                          options={dayOptions}
                          getPopupContainer={getFilterSelectPopupContainer}
                          onChange={(value) => updateAdvancedFilter('day', value ?? '')}
                        />
                      </div>
                    </section>
                    <section className="photoprism-app__filter-group photoprism-app__filter-group--static">
                      <div className="photoprism-app__filter-group-heading">
                        <span className="photoprism-app__filter-group-label">해시태그/카테고리</span>
                        <span className="photoprism-app__filter-group-heading-value">
                          {normalizedLabelFilters.length > 0 || normalizedCategoryFilters.length > 0
                            ? `태그 ${normalizedLabelFilters.length} · 카테고리 ${normalizedCategoryFilters.length}`
                            : '미설정'}
                        </span>
                      </div>
                      <div className="photoprism-app__filter-group-body photoprism-app__filter-group-body--fields">
                        <span className="photoprism-app__filter-field-label">해시태그</span>
                        <Select
                          mode="multiple"
                          allowClear
                          size="large"
                          value={advancedFilters.label}
                          placeholder="해시태그 선택"
                          options={labelOptions}
                          notFoundContent="표시할 해시태그 없음"
                          getPopupContainer={getFilterSelectPopupContainer}
                          onChange={(value) => updateAdvancedFilter('label', value)}
                        />
                        <span className="photoprism-app__filter-field-label">카테고리</span>
                        <Select
                          mode="multiple"
                          allowClear
                          size="large"
                          value={advancedFilters.category}
                          placeholder="카테고리 선택"
                          options={categoryOptions}
                          notFoundContent="표시할 카테고리 없음"
                          getPopupContainer={getFilterSelectPopupContainer}
                          onChange={(value) => updateAdvancedFilter('category', value)}
                        />
                      </div>
                    </section>
                    <section className={`photoprism-app__filter-group${mobileFilterSection === 'meta' ? ' photoprism-app__filter-group--open' : ''}`}>
                      <button
                        type="button"
                        className="photoprism-app__filter-group-trigger"
                        onClick={() => setMobileFilterSection((current) => (current === 'meta' ? null : 'meta'))}
                      >
                        <span className="photoprism-app__filter-group-label">추가 필터</span>
                        <span className="photoprism-app__filter-group-value">
                          <strong>{favoriteFilterLabel} · {mediaTypeFilterLabel}</strong>
                          <DownOutlined />
                        </span>
                      </button>
                      {mobileFilterSection === 'meta' ? (
                        <div className="photoprism-app__filter-group-body photoprism-app__filter-group-body--fields">
                          <Segmented<PhotoPrismFavoriteFilter>
                            size="middle"
                            value={advancedFilters.favorite}
                            options={FAVORITE_FILTER_OPTIONS as unknown as Array<{ label: ReactNode; value: PhotoPrismFavoriteFilter }>}
                            onChange={(value) => updateAdvancedFilter('favorite', value)}
                          />
                          <Segmented<PhotoPrismMediaTypeFilter>
                            size="middle"
                            value={advancedFilters.mediaType}
                            options={MEDIA_TYPE_FILTER_OPTIONS as unknown as Array<{ label: ReactNode; value: PhotoPrismMediaTypeFilter }>}
                            onChange={(value) => updateAdvancedFilter('mediaType', value)}
                          />
                          <Input
                            value={advancedFilters.country}
                            placeholder="국가 예: kr"
                            onChange={(event) => updateAdvancedFilter('country', event.target.value)}
                          />
                          <Input
                            value={advancedFilters.camera}
                            placeholder="카메라 예: iphone"
                            onChange={(event) => updateAdvancedFilter('camera', event.target.value)}
                          />
                        </div>
                      ) : null}
                    </section>
                    <section className={`photoprism-app__filter-group${mobileFilterSection === 'theme' ? ' photoprism-app__filter-group--open' : ''}`}>
                      <button
                        type="button"
                        className="photoprism-app__filter-group-trigger"
                        onClick={() => setMobileFilterSection((current) => (current === 'theme' ? null : 'theme'))}
                      >
                        <span className="photoprism-app__filter-group-label">테마</span>
                        <span className="photoprism-app__filter-group-value">
                          <strong>{selectedThemeOption.label}</strong>
                          <DownOutlined />
                        </span>
                      </button>
                      {mobileFilterSection === 'theme' ? (
                        <div className="photoprism-app__filter-group-body photoprism-app__filter-group-body--theme">
                          <div className="photoprism-app__theme-preview-card">
                            <div className="photoprism-app__theme-preview-copy">
                              <span>현재 테마</span>
                              <strong>{selectedThemeOption.label}</strong>
                              <p>{selectedThemeOption.description}</p>
                            </div>
                            <span
                              className="photoprism-app__theme-preview-swatch"
                              style={{
                                background: `linear-gradient(135deg, ${selectedThemeOption.accent}, var(--photoprism-accent-deep))`,
                              }}
                            />
                          </div>
                          {THEME_PRESET_OPTIONS.map((option) => (
                            <button
                              key={option.value}
                              type="button"
                              className={`photoprism-app__theme-chip${themePreset === option.value ? ' photoprism-app__theme-chip--active' : ''}`}
                              onClick={() => setThemePreset(option.value)}
                            >
                              <span className="photoprism-app__theme-chip-swatch" style={{ background: option.accent }} />
                              <span className="photoprism-app__theme-chip-copy">
                                <strong>{option.label}</strong>
                                <small>{option.description}</small>
                              </span>
                            </button>
                          ))}
                        </div>
                      ) : null}
                    </section>
                    <section className={`photoprism-app__filter-group${mobileFilterSection === 'album' ? ' photoprism-app__filter-group--open' : ''}`}>
                      <button
                        type="button"
                        className="photoprism-app__filter-group-trigger"
                        onClick={() => setMobileFilterSection((current) => (current === 'album' ? null : 'album'))}
                      >
                        <span className="photoprism-app__filter-group-label">앨범</span>
                        <span className="photoprism-app__filter-group-value">
                          <strong>{hasAlbumFilter ? albumTitle : '전체 앨범'}</strong>
                          <DownOutlined />
                        </span>
                      </button>
                      {mobileFilterSection === 'album' ? (
                        <div className="photoprism-app__filter-group-body photoprism-app__filter-group-body--albums">
                          <div className="photoprism-app__album-list">
                            {albums.map((album) => (
                              <button
                                key={album.UID ?? album.Title ?? album.Name}
                                type="button"
                                className={`photoprism-app__album-item${album.UID === activeAlbumUid ? ' photoprism-app__album-item--active' : ''}`}
                                onClick={() => {
                                  setActiveAlbumUid(album.UID ?? ALL_ALBUM_ID);
                                  setSelectedPhotoUids([]);
                                  closeViewer();
                                }}
                              >
                                <FolderOpenOutlined />
                                <span>{album.Title ?? album.Name ?? 'Album'}</span>
                              </button>
                            ))}
                          </div>
                        </div>
                      ) : null}
                    </section>
                    <section className={`photoprism-app__filter-group${mobileFilterSection === 'view' ? ' photoprism-app__filter-group--open' : ''}`}>
                      <button
                        type="button"
                        className="photoprism-app__filter-group-trigger"
                        onClick={() => setMobileFilterSection((current) => (current === 'view' ? null : 'view'))}
                      >
                        <span className="photoprism-app__filter-group-label">보기</span>
                        <span className="photoprism-app__filter-group-value">
                          <strong>{previewModeLabel}</strong>
                          <DownOutlined />
                        </span>
                      </button>
                      {mobileFilterSection === 'view' ? (
                        <div className="photoprism-app__filter-group-body">
                          <Segmented<PreviewMode>
                            size="middle"
                            value={previewMode}
                            options={PREVIEW_MODE_OPTIONS as unknown as Array<{ label: ReactNode; value: PreviewMode }>}
                            onChange={(value) => {
                              previewModeTouchedRef.current = true;
                              setPreviewMode(value);
                            }}
                          />
                        </div>
                      ) : null}
                    </section>
                    <section className={`photoprism-app__filter-group${mobileFilterSection === 'order' ? ' photoprism-app__filter-group--open' : ''}`}>
                      <button
                        type="button"
                        className="photoprism-app__filter-group-trigger"
                        onClick={() => setMobileFilterSection((current) => (current === 'order' ? null : 'order'))}
                      >
                        <span className="photoprism-app__filter-group-label">정렬</span>
                        <span className="photoprism-app__filter-group-value">
                          <strong>{sortOrderLabel}</strong>
                          <DownOutlined />
                        </span>
                      </button>
                      {mobileFilterSection === 'order' ? (
                        <div className="photoprism-app__filter-group-body photoprism-app__filter-group-body--options">
                          {SORT_ORDER_OPTIONS.map((option) => (
                            <button
                              key={option.value}
                              type="button"
                              className={`photoprism-app__filter-option${sortOrder === option.value ? ' photoprism-app__filter-option--active' : ''}`}
                              onClick={() => setSortOrder(option.value)}
                            >
                              {option.label}
                            </button>
                          ))}
                        </div>
                      ) : null}
                    </section>
                    <div className="photoprism-app__filter-sheet-footer">
                      <Button
                        size="large"
                        className="photoprism-app__filter-sheet-apply"
                        type="primary"
                        onClick={() => setIsAlbumPanelOpen(false)}
                      >
                        적용
                      </Button>
                    </div>
                  </div>
                </>
              ) : (
                <>
                  <div className="photoprism-app__search">
                    <Input
                      allowClear
                      size="large"
                      value={searchKeyword}
                      prefix={<SearchOutlined />}
                      placeholder="검색어"
                      onChange={(event) => {
                        setSearchKeyword(event.target.value);
                        setSelectedPhotoUids([]);
                        closeViewer();
                      }}
                    />
                  </div>
                  <div className="photoprism-app__filter-sheet-summary">
                    <div className="photoprism-app__filter-sheet-summary-copy">
                      <strong>{activeFilterCount > 0 ? `${activeFilterCount}개 필터 적용 중` : '사용자 등록 앨범만 표시'}</strong>
                      <span>{activeSearchQuery || '전체 사진'}</span>
                    </div>
                    {activeFilterChips.length > 0 ? (
                      <div className="photoprism-app__filter-chip-list">
                        {activeFilterChips.map((chip) => (
                          <span key={chip} className="photoprism-app__filter-chip photoprism-app__filter-chip--active">
                            {chip}
                          </span>
                        ))}
                      </div>
                    ) : null}
                    {activeFilterCount > 0 ? (
                      <Button type="text" size="small" className="photoprism-app__filter-reset" onClick={resetFilterState}>
                        초기화
                      </Button>
                    ) : null}
                  </div>
                  <div className="photoprism-app__filter-group">
                    <div className="photoprism-app__filter-group-body photoprism-app__filter-group-body--fields">
                      <span className="photoprism-app__filter-field-label">년도</span>
                      <Select
                        allowClear
                        size="large"
                        value={advancedFilters.year || undefined}
                        placeholder="년도 선택"
                        options={yearOptions}
                        getPopupContainer={getFilterSelectPopupContainer}
                        onChange={(value) => updateAdvancedFilter('year', value ?? '')}
                      />
                      <span className="photoprism-app__filter-field-label">월</span>
                      <Select
                        allowClear
                        size="large"
                        value={advancedFilters.month || undefined}
                        placeholder="월 선택"
                        options={monthOptions}
                        getPopupContainer={getFilterSelectPopupContainer}
                        onChange={(value) => updateAdvancedFilter('month', value ?? '')}
                      />
                      <span className="photoprism-app__filter-field-label">일</span>
                      <Select
                        allowClear
                        size="large"
                        value={advancedFilters.day || undefined}
                        placeholder="일 선택"
                        options={dayOptions}
                        getPopupContainer={getFilterSelectPopupContainer}
                        onChange={(value) => updateAdvancedFilter('day', value ?? '')}
                      />
                      <span className="photoprism-app__filter-field-label">해시태그</span>
                      <Select
                        mode="multiple"
                        allowClear
                        size="large"
                        value={advancedFilters.label}
                        placeholder="해시태그 선택"
                        options={labelOptions}
                        notFoundContent="표시할 해시태그 없음"
                        getPopupContainer={getFilterSelectPopupContainer}
                        onChange={(value) => updateAdvancedFilter('label', value)}
                      />
                      <span className="photoprism-app__filter-field-label">카테고리</span>
                      <Select
                        mode="multiple"
                        allowClear
                        size="large"
                        value={advancedFilters.category}
                        placeholder="카테고리 선택"
                        options={categoryOptions}
                        notFoundContent="표시할 카테고리 없음"
                        getPopupContainer={getFilterSelectPopupContainer}
                        onChange={(value) => updateAdvancedFilter('category', value)}
                      />
                      <span className="photoprism-app__filter-field-label">국가</span>
                      <Input
                        value={advancedFilters.country}
                        placeholder="kr"
                        onChange={(event) => updateAdvancedFilter('country', event.target.value)}
                      />
                      <span className="photoprism-app__filter-field-label">카메라</span>
                      <Input
                        value={advancedFilters.camera}
                        placeholder="iphone"
                        onChange={(event) => updateAdvancedFilter('camera', event.target.value)}
                      />
                      <span className="photoprism-app__filter-field-label">즐겨찾기</span>
                      <Segmented<PhotoPrismFavoriteFilter>
                        size="middle"
                        value={advancedFilters.favorite}
                        options={FAVORITE_FILTER_OPTIONS as unknown as Array<{ label: ReactNode; value: PhotoPrismFavoriteFilter }>}
                        onChange={(value) => updateAdvancedFilter('favorite', value)}
                      />
                      <span className="photoprism-app__filter-field-label">미디어</span>
                      <Segmented<PhotoPrismMediaTypeFilter>
                        size="middle"
                        value={advancedFilters.mediaType}
                        options={MEDIA_TYPE_FILTER_OPTIONS as unknown as Array<{ label: ReactNode; value: PhotoPrismMediaTypeFilter }>}
                        onChange={(value) => updateAdvancedFilter('mediaType', value)}
                      />
                      <span className="photoprism-app__filter-field-label">테마</span>
                      <div className="photoprism-app__theme-chip-list">
                        {THEME_PRESET_OPTIONS.map((option) => (
                          <button
                            key={option.value}
                            type="button"
                            className={`photoprism-app__theme-chip${themePreset === option.value ? ' photoprism-app__theme-chip--active' : ''}`}
                            onClick={() => setThemePreset(option.value)}
                          >
                            <span className="photoprism-app__theme-chip-swatch" style={{ background: option.accent }} />
                            <span className="photoprism-app__theme-chip-copy">
                              <strong>{option.label}</strong>
                              <small>{option.description}</small>
                            </span>
                          </button>
                        ))}
                      </div>
                    </div>
                  </div>
                </>
              )}
              {!isCompactViewport ? (
                <div className="photoprism-app__album-list">
                  {albums.map((album) => (
                    <button
                      key={album.UID ?? album.Title ?? album.Name}
                      type="button"
                      className={`photoprism-app__album-item${album.UID === activeAlbumUid ? ' photoprism-app__album-item--active' : ''}`}
                      onClick={() => {
                        setActiveAlbumUid(album.UID ?? ALL_ALBUM_ID);
                        setSelectedPhotoUids([]);
                        closeViewer();
                      }}
                    >
                      <FolderOpenOutlined />
                      <span>{album.Title ?? album.Name ?? 'Album'}</span>
                    </button>
                  ))}
                </div>
              ) : null}
            </aside>
          ) : null}

          <main className="photoprism-app__main">
            <div className="photoprism-app__toolbar">
              {isCompactViewport ? (
                <div className="photoprism-app__toolbar-search">
                  <Button
                    type="text"
                    className={`photoprism-app__toolbar-toggle photoprism-app__toolbar-toggle--filter${isAlbumPanelOpen ? ' photoprism-app__toolbar-toggle--filter-active' : ''}${activeFilterCount > 0 ? ' photoprism-app__toolbar-toggle--filter-has-active' : ''}`}
                    onClick={() => {
                      setIsAlbumPanelOpen((current) => {
                        const next = !current;
                        if (next) {
                          setMobileFilterSection('date');
                        }
                        return next;
                      });
                      setIsMobileControlsOpen(false);
                    }}
                    aria-label={activeFilterCount > 0 ? `필터 패널 토글, 적용 ${activeFilterCount}개` : '필터 패널 토글'}
                  >
                    <FunnelPlotOutlined />
                    {activeFilterCount > 0 ? <span className="photoprism-app__toolbar-filter-badge">{activeFilterCount}</span> : null}
                  </Button>
                  <div className="photoprism-app__search photoprism-app__search--toolbar">
                    <Input
                      allowClear
                      size="large"
                      value={searchKeyword}
                      prefix={<SearchOutlined />}
                      placeholder="검색어"
                      onChange={(event) => {
                        setSearchKeyword(event.target.value);
                        setSelectedPhotoUids([]);
                        closeViewer();
                      }}
                    />
                  </div>
                  <div className="photoprism-app__toolbar-preview-switch" aria-label="보기 모드">
                    <Segmented<PreviewMode>
                      size="middle"
                      value={previewMode}
                      options={PREVIEW_MODE_OPTIONS as unknown as Array<{ label: ReactNode; value: PreviewMode }>}
                      onChange={(value) => {
                        previewModeTouchedRef.current = true;
                        setPreviewMode(value);
                      }}
                    />
                  </div>
                </div>
              ) : null}
              {isCompactViewport ? (
                <div
                  className={`photoprism-app__mobile-utility-sheet${isMobileControlsOpen ? ' photoprism-app__mobile-utility-sheet--open' : ''}`}
                  aria-hidden={!isMobileControlsOpen}
                >
                  <div className="photoprism-app__mobile-utility-head">
                    <span>{photos.length}장</span>
                    <span className={`photoprism-app__selection-count${isSelectionMode ? ' photoprism-app__selection-count--active' : ''}`} aria-live="polite">
                      선택 {selectedCount}장
                    </span>
                  </div>
                  <div className="photoprism-app__mobile-utility-group">
                    <Segmented<DensityMode>
                      size="middle"
                      value={densityMode}
                      options={DENSITY_MODE_OPTIONS as unknown as Array<{ label: ReactNode; value: DensityMode }>}
                      onChange={(value) => {
                        densityModeTouchedRef.current = true;
                        setDensityMode(value);
                      }}
                    />
                  </div>
                  <div className="photoprism-app__mobile-utility-actions" role="group" aria-label="모바일 도구">
                    <Button
                      type="text"
                      icon={<CheckSquareOutlined />}
                      className="photoprism-app__toolbar-toggle photoprism-app__toolbar-toggle--select"
                      disabled={photos.length === 0 || allPhotosSelected}
                      onClick={() => setSelectedPhotoUids(photos.map((photo) => photo.UID))}
                    >
                      전체선택
                    </Button>
                    <Button
                      type="text"
                      icon={<CloseCircleOutlined />}
                      className="photoprism-app__toolbar-toggle photoprism-app__toolbar-toggle--clear"
                      disabled={selectedCount === 0}
                      onClick={() => setSelectedPhotoUids([])}
                    >
                      선택해제
                    </Button>
                    <Button type="text" icon={<ReloadOutlined />} className="photoprism-app__toolbar-toggle" onClick={() => refreshGallery()}>
                      새로고침
                    </Button>
                    <Button
                      type="text"
                      icon={<DownloadOutlined />}
                      className="photoprism-app__toolbar-toggle"
                      onClick={handleDownloadSelected}
                      disabled={actionTargetCount === 0}
                    >
                      원본
                    </Button>
                    <Button
                      type="text"
                      icon={<ShareAltOutlined />}
                      className="photoprism-app__toolbar-toggle"
                      onClick={() => void handleShareSelected()}
                      disabled={actionTargetCount === 0}
                    >
                      공유
                    </Button>
                    <Button type="text" icon={<LogoutOutlined />} className="photoprism-app__toolbar-toggle" onClick={handleLogout}>
                      종료
                    </Button>
                  </div>
                </div>
              ) : (
                <>
                  <div className="photoprism-app__toolbar-row photoprism-app__toolbar-row--primary">
                    <div className="photoprism-app__toolbar-left photoprism-app__toolbar-left--summary">
                      <span>{photos.length}장</span>
                      <span className="photoprism-app__mode-badge">
                        사진
                      </span>
                    </div>
                    <div className="photoprism-app__toolbar-right photoprism-app__toolbar-right--density">
                      <Segmented<DensityMode>
                        size="middle"
                        value={densityMode}
                        options={DENSITY_MODE_OPTIONS as unknown as Array<{ label: ReactNode; value: DensityMode }>}
                        onChange={(value) => {
                          densityModeTouchedRef.current = true;
                          setDensityMode(value);
                        }}
                      />
                    </div>
                  </div>
                  <div className="photoprism-app__toolbar-row photoprism-app__toolbar-row--secondary">
                    <div className="photoprism-app__toolbar-left photoprism-app__toolbar-left--actions">
                      <span
                        className={`photoprism-app__selection-count${isSelectionMode ? ' photoprism-app__selection-count--active' : ''}`}
                        aria-live="polite"
                      >
                        선택 {selectedCount}장
                      </span>
                      <div className="photoprism-app__selection-actions" role="group" aria-label="선택 작업">
                        <Button
                          type="text"
                          icon={<CheckSquareOutlined />}
                          className="photoprism-app__toolbar-toggle photoprism-app__toolbar-toggle--select"
                          disabled={photos.length === 0 || allPhotosSelected}
                          onClick={() => setSelectedPhotoUids(photos.map((photo) => photo.UID))}
                          aria-label="전체 선택"
                        >
                          전체선택
                        </Button>
                        <Button
                          type="text"
                          icon={<CloseCircleOutlined />}
                          className="photoprism-app__toolbar-toggle photoprism-app__toolbar-toggle--clear"
                          disabled={selectedCount === 0}
                          onClick={() => setSelectedPhotoUids([])}
                          aria-label="전체 선택 해제"
                        >
                          선택해제
                        </Button>
                      </div>
                    </div>
                    <div className="photoprism-app__toolbar-right photoprism-app__toolbar-right--preview">
                      <Segmented<PreviewMode>
                        size="middle"
                        value={previewMode}
                        options={PREVIEW_MODE_OPTIONS as unknown as Array<{ label: ReactNode; value: PreviewMode }>}
                        onChange={(value) => {
                          previewModeTouchedRef.current = true;
                          setPreviewMode(value);
                        }}
                      />
                    </div>
                  </div>
                </>
              )}
            </div>

            {!isBusy && photos.length === 0 ? (
              <div className="photoprism-app__empty">
                <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={false} />
              </div>
            ) : (
              <div className={`photoprism-app__content photoprism-app__content--${previewMode}${isBusy ? ' photoprism-app__content--loading' : ''}`}>
                <div
                  ref={galleryRef}
                  className={`photoprism-app__gallery photoprism-app__gallery--${previewMode} photoprism-app__gallery--density-${densityMode}${isRangeSelecting ? ' photoprism-app__gallery--selection-active' : ''}`}
                  data-pull-state={isPullRefreshContentLoading ? 'loading' : isPullToRefreshArmed ? 'armed' : pullToRefreshOffset > 0 ? 'pulling' : 'idle'}
                  onTouchStart={handleGalleryTouchStart}
                  onTouchMove={handleGalleryTouchMove}
                  onTouchEnd={handleGalleryTouchEnd}
                  onTouchCancel={handleGalleryTouchEnd}
                >
                  <div
                    className={`photoprism-app__pull-refresh${pullToRefreshOffset > 0 ? ' photoprism-app__pull-refresh--visible' : ''}${isPullToRefreshArmed ? ' photoprism-app__pull-refresh--armed' : ''}`}
                    style={{ height: pullToRefreshVisualHeight > 0 ? `${pullToRefreshVisualHeight}px` : undefined }}
                    aria-hidden="true"
                  >
                    <span
                      className="photoprism-app__pull-refresh-indicator"
                      style={
                        {
                          '--pull-refresh-progress': `${pullToRefreshProgress}`,
                        } as CSSProperties
                      }
                    >
                      <span className="photoprism-app__pull-refresh-ring" />
                      <span className="photoprism-app__pull-refresh-icon">
                        <ReloadOutlined />
                      </span>
                    </span>
                  </div>
                  {isPullRefreshContentLoading ? (
                    <div className="photoprism-app__gallery-refresh-blank" aria-live="polite" aria-busy="true">
                      <Spin size="large" />
                    </div>
                  ) : isBusy && photos.length === 0
                    ? (
                        <div
                          className={`photoprism-app__gallery-section-body photoprism-app__gallery-section-body--${previewMode} photoprism-app__gallery-section-body--density-${densityMode}`}
                        >
                          {galleryPlaceholderItems.map((photoOrKey) => (
                            <article
                              key={photoOrKey}
                              className={`photoprism-app__photo-card photoprism-app__photo-card--placeholder photoprism-app__photo-card--${previewMode}`}
                              aria-hidden="true"
                            >
                              <span className="photoprism-app__photo-thumb-shell">
                                <span className="photoprism-app__photo-thumb-skeleton" />
                              </span>
                              {previewMode === 'list' ? (
                                <span className="photoprism-app__photo-meta photoprism-app__photo-meta--placeholder">
                                  <span className="photoprism-app__photo-line-skeleton photoprism-app__photo-line-skeleton--title" />
                                  <span className="photoprism-app__photo-line-skeleton" />
                                </span>
                              ) : null}
                            </article>
                          ))}
                        </div>
                      )
                    : null}
                  {!isPullRefreshContentLoading && (!isBusy || photos.length > 0)
                    ? photoSections.map((section) => (
                        <section key={section.key} className="photoprism-app__gallery-section" aria-label={section.label}>
                          {(() => {
                            const sectionSelectedCount = section.photos.reduce(
                              (count, photo) => count + (selectedPhotoUidSet.has(photo.UID) ? 1 : 0),
                              0,
                            );
                            const hasSectionSelection = sectionSelectedCount > 0;
                            const isSectionFullySelected = section.photos.length > 0 && sectionSelectedCount === section.photos.length;
                            const shouldShowSectionActions = !isCompactViewport || isSelectionMode;
                            const sectionSelectionState = isSectionFullySelected ? 'all' : hasSectionSelection ? 'partial' : 'none';
                            const sectionToggleLabel = isSectionFullySelected ? '전체 선택 해제' : '전체 선택';

                            return (
                          <header className={`photoprism-app__gallery-section-header${isCompactViewport ? ' photoprism-app__gallery-section-header--compact' : ''}`}>
                                <button
                                  type="button"
                                  className={`photoprism-app__gallery-section-toggle photoprism-app__gallery-section-toggle--${sectionSelectionState}`}
                                  onClick={() => updateSectionSelection(section.photos, isSectionFullySelected ? 'clear' : 'select')}
                                  aria-label={`${section.label} ${sectionToggleLabel}`}
                                  aria-pressed={hasSectionSelection}
                                >
                                  <span
                                    className={`photoprism-app__gallery-section-toggle-check photoprism-app__gallery-section-toggle-check--${sectionSelectionState}`}
                                    aria-hidden="true"
                                  >
                                    {isSectionFullySelected ? (
                                      <CheckSquareOutlined />
                                    ) : hasSectionSelection ? (
                                      <MinusSquareOutlined />
                                    ) : (
                                      <BorderOutlined />
                                    )}
                                  </span>
                                  <span className="photoprism-app__gallery-section-heading">
                                    <strong>{section.label}</strong>
                                    {!isCompactViewport || isSelectionMode ? (
                                      <span>
                                        {section.photos.length}개
                                        {hasSectionSelection ? ` · 선택 ${sectionSelectedCount}개` : ''}
                                      </span>
                                    ) : null}
                                  </span>
                                </button>
                                {shouldShowSectionActions ? (
                                  <div className="photoprism-app__gallery-section-actions">
                                    <button
                                      type="button"
                                      className={`photoprism-app__gallery-section-action${isSectionFullySelected ? ' is-active' : ''}`}
                                      onClick={() => updateSectionSelection(section.photos, 'select')}
                                      disabled={isSectionFullySelected}
                                    >
                                      전체선택
                                    </button>
                                    <button
                                      type="button"
                                      className={`photoprism-app__gallery-section-action photoprism-app__gallery-section-action--clear${hasSectionSelection ? ' is-active' : ''}`}
                                      onClick={() => updateSectionSelection(section.photos, 'clear')}
                                      disabled={!hasSectionSelection}
                                    >
                                      해제
                                    </button>
                                  </div>
                                ) : null}
                          </header>
                            );
                          })()}
                          <div
                            className={`photoprism-app__gallery-section-body photoprism-app__gallery-section-body--${previewMode} photoprism-app__gallery-section-body--density-${densityMode}`}
                          >
                            {section.photos.map((photo) => {
                              const thumbUrl = photoThumbUrls[photo.UID] ?? '';
                              const aspectRatio =
                                typeof photo.Width === 'number' && typeof photo.Height === 'number' && photo.Width > 0 && photo.Height > 0
                                  ? `${photo.Width} / ${photo.Height}`
                                  : '1 / 1';
                              const isSelected = selectedPhotoUidSet.has(photo.UID);
                              const isFavoritePending = favoritePendingUidSet.has(photo.UID);
                              const isRangeTarget = rangeTargetUidSet.has(photo.UID);
                              const showMeta = previewMode === 'list';
                              const isVideo = isPhotoPrismVideo(photo);
                              const thumbStyle: CSSProperties = previewMode === 'masonry' ? { aspectRatio } : {};

                              return (
                                <article
                                  key={photo.UID}
                                  ref={photo.UID === preloadTriggerPhotoUid ? galleryPreloadTriggerRef : undefined}
                                  className={`photoprism-app__photo-card photoprism-app__photo-card--${previewMode}${photo.UID === selectedPhoto?.UID ? ' photoprism-app__photo-card--active' : ''}${isSelected ? ' photoprism-app__photo-card--selected' : ''}${isSelectionMode ? ' photoprism-app__photo-card--selection-mode' : ''}${isRangeTarget ? ' photoprism-app__photo-card--range-target' : ''}`}
                                  data-photoprism-photo-uid={photo.UID}
                                  role="button"
                                  tabIndex={0}
                                  aria-pressed={isSelected}
                                  aria-label={
                                    isSelectionMode
                                      ? `${getPhotoDisplayTitle(photo)} ${isSelected ? '선택 해제' : '선택'}`
                                      : isVideo
                                        ? `${getPhotoDisplayTitle(photo)} 선택`
                                        : `${getPhotoDisplayTitle(photo)} 상세보기`
                                  }
                                  onPointerDown={(event) => handleCardPointerDown(event, photo.UID)}
                                  onPointerMove={handleCardPointerMove}
                                  onPointerUp={(event) => handleCardPointerUp(event, photo.UID)}
                                  onPointerCancel={handleCardPointerCancel}
                                  onClick={(event) => handleCardClick(event, photo.UID)}
                                  onKeyDown={(event) => handleCardKeyDown(event, photo.UID)}
                                >
                                  <span className="photoprism-app__photo-thumb-shell" style={thumbStyle}>
                                    {thumbUrl ? (
                                      <img
                                        src={thumbUrl}
                                        alt={getPhotoDisplayTitle(photo)}
                                        className="photoprism-app__photo-thumb"
                                        loading="lazy"
                                        decoding="async"
                                        fetchPriority="low"
                                      />
                                    ) : (
                                      <PictureOutlined />
                                    )}
                                    {isVideo ? (
                                      <span className="photoprism-app__photo-media-badge" aria-hidden="true">
                                        <PlayCircleFilled />
                                        <span>동영상</span>
                                      </span>
                                    ) : null}
                                    <button
                                      type="button"
                                      className={`photoprism-app__photo-overlay-action photoprism-app__photo-favorite${photo.Favorite ? ' photoprism-app__photo-favorite--active' : ''}`}
                                      data-photoprism-overlay-action="true"
                                      disabled={isFavoritePending}
                                      onPointerDown={(event) => {
                                        event.stopPropagation();
                                      }}
                                      onPointerUp={(event) => {
                                        event.stopPropagation();
                                      }}
                                      onClick={(event) => {
                                        event.stopPropagation();
                                        void handleToggleFavorite(photo);
                                      }}
                                      aria-label={photo.Favorite ? '즐겨찾기 해제' : '즐겨찾기 추가'}
                                      aria-pressed={photo.Favorite === true}
                                    >
                                      {photo.Favorite ? <HeartFilled /> : <HeartOutlined />}
                                    </button>
                                    <button
                                      type="button"
                                      className="photoprism-app__photo-overlay-action photoprism-app__photo-detail"
                                      data-photoprism-overlay-action="true"
                                      onPointerDown={(event) => {
                                        stopOverlayActionEvent(event);
                                      }}
                                      onPointerUp={(event) => {
                                        handlePhotoDetailAction(event, photo.UID);
                                      }}
                                      onClick={(event) => {
                                        handlePhotoDetailAction(event, photo.UID);
                                      }}
                                      onKeyDown={(event) => {
                                        if (event.key === 'Enter' || event.key === ' ') {
                                          handlePhotoDetailAction(event, photo.UID);
                                        }
                                      }}
                                      aria-label="상세보기"
                                    >
                                      <SearchOutlined />
                                    </button>
                                    <button
                                      type="button"
                                      className={`photoprism-app__photo-overlay-action photoprism-app__photo-check${isSelected ? ' photoprism-app__photo-check--selected' : ''}`}
                                      data-photoprism-overlay-action="true"
                                      onPointerDown={(event) => {
                                        event.stopPropagation();
                                      }}
                                      onPointerUp={(event) => {
                                        event.stopPropagation();
                                      }}
                                      onClick={(event) => {
                                        event.stopPropagation();
                                        togglePhotoSelection(photo.UID);
                                      }}
                                      aria-label={isSelected ? '선택 해제' : '선택'}
                                      aria-pressed={isSelected}
                                    >
                                      {isSelected ? <CheckOutlined /> : <BorderOutlined />}
                                    </button>
                                  </span>
                                  {showMeta ? (
                                    <span className="photoprism-app__photo-meta">
                                      <strong>
                                        {getPhotoDisplayTitle(photo)}
                                        {photo.Favorite ? <span className="photoprism-app__photo-favorite-chip">즐겨찾기</span> : null}
                                      </strong>
                                      <span>{getPhotoDisplayDate(photo)}</span>
                                    </span>
                                  ) : null}
                                </article>
                              );
                            })}
                          </div>
                        </section>
                      ))
                    : null}
                  {photos.length > 0 && !isPullRefreshContentLoading ? (
                    <div className="photoprism-app__gallery-load-more" aria-hidden={!isLoadingMore && !hasMorePhotos}>
                      {isLoadingMore ? <Spin size="small" /> : null}
                      <span>
                        {isLoadingMore ? '사진 더 불러오는 중' : hasMorePhotos ? '일부 내려오면 다음 사진을 미리 불러옵니다.' : '마지막 사진입니다.'}
                      </span>
                    </div>
                  ) : null}
                </div>
                {isBusy && photos.length > 0 && !isPullRefreshContentLoading ? (
                  <div className="photoprism-app__loading-overlay" aria-hidden="true">
                    <Spin size="small" />
                  </div>
                ) : null}
              </div>
            )}
          </main>
        </div>
      )}

      <FullscreenPreviewModal
        open={Boolean(viewerPhotoUid)}
        className="photoprism-app__viewer-modal"
        modalStyle={viewerThemeStyle}
        shellStyle={viewerThemeStyle}
        zIndex={1600}
        getContainer={isEmbeddedLaunch ? () => appRootRef.current ?? document.body : undefined}
        title={viewerDetail ? getPhotoDisplayTitle(viewerDetail) : viewerPhoto ? getPhotoDisplayTitle(viewerPhoto) : ''}
        meta={
          viewerDisplayPhoto ? (
            <>
              {getPhotoDisplayDate(viewerDisplayPhoto)}
              {viewerDisplayPhoto.PlaceLabel ? ` · ${viewerDisplayPhoto.PlaceLabel}` : ''}
            </>
          ) : undefined
        }
        actions={
          <Dropdown
            trigger={['click']}
            open={isViewerThemeMenuOpen}
            onOpenChange={(open) => {
              setIsViewerThemeMenuOpen(open);

              if (open) {
                revealViewerActions();
              }
            }}
            placement="bottomRight"
            popupRender={() => (
              <div
                className="photoprism-app__viewer-theme-menu"
                role="menu"
                aria-label="상세보기 테마 프리셋"
                data-photoprism-overlay-action="true"
              >
                {VIEWER_THEME_OPTIONS.map((option) => {
                  const isActive = viewerThemePreset === option.value;

                  return (
                    <button
                      key={option.value}
                      type="button"
                      role="menuitemradio"
                      aria-checked={isActive}
                      className={`photoprism-app__viewer-theme-menu-item${
                        isActive ? ' photoprism-app__viewer-theme-menu-item--active' : ''
                      }`}
                      onClick={() => {
                        setViewerThemePreset(option.value);
                        setIsViewerThemeMenuOpen(false);
                      }}
                    >
                      <span
                        className="photoprism-app__viewer-theme-menu-swatch"
                        style={{ background: option.swatch }}
                        aria-hidden="true"
                      />
                      <span className="photoprism-app__viewer-theme-menu-copy">
                        <strong>{option.label}</strong>
                        <span>{option.description}</span>
                      </span>
                      {isActive ? <CheckOutlined className="photoprism-app__viewer-theme-menu-check" /> : null}
                    </button>
                  );
                })}
              </div>
            )}
          >
            <Button
              type="text"
              aria-label={`상세보기 설정, 현재 ${selectedViewerThemeOption.label} 프리셋`}
              className={`fullscreen-preview-modal__icon-button photoprism-app__viewer-settings-trigger${
                isViewerThemeMenuOpen ? ' photoprism-app__viewer-settings-trigger--active' : ''
              }`}
              icon={<SettingOutlined />}
              data-photoprism-overlay-action="true"
              onClick={() => {
                revealViewerActions();
              }}
            />
          </Dropdown>
        }
        onClose={closeViewer}
      >
        <div
          className="photoprism-app__viewer-shell"
          onPointerDownCapture={() => {
            revealViewerActions();
          }}
          onTouchStart={handleViewerTouchStart}
          onTouchEnd={handleViewerTouchEnd}
        >
          <div className="photoprism-app__viewer-topbar">
            <div className="photoprism-app__viewer-tabbar" role="tablist" aria-label="상세보기 탭">
              <button
                type="button"
                role="tab"
                aria-selected={viewerDisplayTab === 'photo'}
                className={`photoprism-app__viewer-tab${viewerDisplayTab === 'photo' ? ' photoprism-app__viewer-tab--active' : ''}`}
                onClick={() => setViewerDisplayTab('photo')}
              >
                보기
              </button>
              <button
                type="button"
                role="tab"
                aria-selected={viewerDisplayTab === 'info'}
                className={`photoprism-app__viewer-tab${viewerDisplayTab === 'info' ? ' photoprism-app__viewer-tab--active' : ''}`}
                onClick={() => setViewerDisplayTab('info')}
              >
                정보
              </button>
            </div>
            <div className="photoprism-app__viewer-toolbar">
              <span className="photoprism-app__viewer-tab-summary" aria-live="polite">
                {viewerTabSummaryParts.map((part, index) => (
                  <span
                    key={`${part}-${index}`}
                    className={`photoprism-app__viewer-tab-summary-item${
                      index === 0 ? ' photoprism-app__viewer-tab-summary-item--primary' : ''
                    }`}
                  >
                    {part}
                  </span>
                ))}
              </span>
            </div>
          </div>
          {viewerDisplayTab === 'photo' ? (
            <>
              <div
                className={`photoprism-app__viewer-stage${isViewerVideo ? ' photoprism-app__viewer-stage--video' : ''}${
                  viewerStageStyle && shouldShowViewerPlaceholder ? ' photoprism-app__viewer-stage--placeholder' : ''
                }`}
                style={viewerStageStyle}
              >
                <Button
                  type="text"
                  className="photoprism-app__viewer-nav photoprism-app__viewer-nav--previous"
                  aria-label="이전 사진"
                  icon={<ArrowLeftOutlined />}
                  disabled={!canViewPreviousPhoto}
                  onClick={() => navigateViewer('previous')}
                />
                <div className="photoprism-app__viewer-main">
                  {isViewerVideo ? (
                    <div
                      className={`photoprism-app__viewer-video-frame${
                        viewerStageStyle && shouldShowViewerPlaceholder ? ' photoprism-app__viewer-video-frame--placeholder' : ''
                      }`}
                    >
                      {viewerPhotoUrl ? (
                        <>
                          <video
                            key={viewerPhotoUrl}
                            ref={viewerDisplayTab === 'photo' ? viewerVideoRef : null}
                            className="photoprism-app__viewer-video"
                            poster={viewerPhotoThumbUrl || undefined}
                            controls
                            playsInline
                            preload="metadata"
                            onCanPlay={handleViewerVideoReady}
                            onError={handleViewerVideoError}
                            onLoadedData={handleViewerVideoReady}
                          >
                            <source src={viewerPhotoUrl} type={viewerVideoMime || undefined} />
                          </video>
                        </>
                      ) : viewerPhoto && viewerPhotoThumbUrl ? (
                        <img
                          src={viewerPhotoThumbUrl}
                          alt={getPhotoDisplayTitle(viewerPhoto)}
                          className="photoprism-app__viewer-image"
                          loading="eager"
                          decoding="async"
                          fetchPriority="high"
                          onLoad={handleViewerImageReady}
                          onError={handleViewerImageError}
                        />
                      ) : null}
                    </div>
                  ) : (
                    <ZoomablePreviewSurface
                      className="photoprism-app__zoom-surface"
                      stageClassName="photoprism-app__zoom-stage"
                      contentClassName="photoprism-app__zoom-content"
                      onZoomChange={setViewerZoomScale}
                    >
                      {viewerPhotoUrl ? (
                        <img
                          src={viewerPhotoUrl}
                          alt={viewerDisplayPhoto ? getPhotoDisplayTitle(viewerDisplayPhoto) : viewerPhoto ? getPhotoDisplayTitle(viewerPhoto) : ''}
                          className="photoprism-app__viewer-image"
                          loading="eager"
                          decoding="async"
                          fetchPriority="high"
                          onLoad={handleViewerImageReady}
                          onError={handleViewerImageError}
                        />
                      ) : viewerPhoto && viewerPhotoThumbUrl ? (
                        <img
                          src={viewerPhotoThumbUrl}
                          alt={getPhotoDisplayTitle(viewerPhoto)}
                          className="photoprism-app__viewer-image"
                          loading="eager"
                          decoding="async"
                          fetchPriority="high"
                          onLoad={handleViewerImageReady}
                          onError={handleViewerImageError}
                        />
                      ) : null}
                    </ZoomablePreviewSurface>
                  )}
                </div>
                <Button
                  type="text"
                  className="photoprism-app__viewer-nav photoprism-app__viewer-nav--next"
                  aria-label="다음 사진"
                  icon={<ArrowRightOutlined />}
                  disabled={!canViewNextPhoto}
                  onClick={() => navigateViewer('next')}
                />
                {viewerQuickActions}
              </div>
            </>
          ) : null}
          {viewerDisplayTab === 'info' && viewerDisplayPhoto ? (
            <section className="photoprism-app__viewer-info-page">
              <div className="photoprism-app__viewer-panel-head">
                <strong>{getPhotoDisplayTitle(viewerDisplayPhoto)}</strong>
                <span>
                  {viewerPhotoIndex >= 0 ? `${viewerPhotoIndex + 1} / ${photos.length}` : ''}
                  {selectedCount > 0 ? ` · 선택 ${selectedCount}장` : ''}
                  {viewerFileTypeText ? ` · ${viewerFileTypeText}` : ''}
                </span>
              </div>
              {isViewerVideo && viewerPhotoUrl ? (
                <div className="photoprism-app__viewer-info-hero photoprism-app__viewer-info-hero--video">
                  <video
                    key={`${viewerPhotoUrl}-info`}
                    ref={viewerDisplayTab === 'info' ? viewerVideoRef : null}
                    poster={viewerPhotoThumbUrl || undefined}
                    controls
                    playsInline
                    preload="metadata"
                    onCanPlay={handleViewerVideoReady}
                    onError={handleViewerVideoError}
                    onLoadedData={handleViewerVideoReady}
                  >
                    <source src={viewerPhotoUrl} type={viewerVideoMime || undefined} />
                  </video>
                </div>
              ) : viewerPhotoThumbUrl ? (
                <div className="photoprism-app__viewer-info-hero">
                  <img src={viewerPhotoThumbUrl} alt={getPhotoDisplayTitle(viewerDisplayPhoto)} loading="lazy" decoding="async" />
                </div>
              ) : null}
              {viewerVideoError ? (
                <div className="photoprism-app__viewer-video-status photoprism-app__viewer-video-status--inline">
                  {viewerVideoError}
                </div>
              ) : null}
              <div className="photoprism-app__viewer-info-actions" role="group" aria-label="상세보기 작업">
                <Button type="default" icon={<DownloadOutlined />} onClick={handleDownloadSelected} disabled={actionTargetCount === 0}>
                  {selectedPhotos.length > 0 ? `다운로드 ${actionTargetCount}장` : '현재 사진 다운로드'}
                </Button>
                <Button type="default" icon={<ShareAltOutlined />} onClick={() => void handleShareSelected()} disabled={actionTargetCount === 0}>
                  {selectedPhotos.length > 0 ? `공유 ${actionTargetCount}장` : '현재 사진 공유'}
                </Button>
              </div>
              <section className="photoprism-app__viewer-album-panel" aria-label="앨범 등록 및 해제">
                <div className="photoprism-app__viewer-album-panel-head">
                  <strong>앨범 관리</strong>
                  <span>
                    {isViewerDetailLoading
                      ? '소속 앨범을 확인하는 중입니다.'
                      : viewerMembershipAlbums.length > 0
                        ? `${viewerMembershipAlbums.length}개 앨범에 등록됨`
                        : '등록된 수동 앨범이 없습니다.'}
                  </span>
                </div>
                <div className="photoprism-app__viewer-album-panel-actions">
                  <Select
                    showSearch
                    allowClear
                    value={viewerAlbumInputUid}
                    options={assignableAlbumOptions}
                    placeholder={assignableAlbumOptions.length > 0 ? '등록할 앨범 선택' : '추가할 앨범이 없습니다.'}
                    className="photoprism-app__viewer-album-select"
                    popupMatchSelectWidth={false}
                    optionFilterProp="label"
                    onChange={(value) => setViewerAlbumInputUid(value)}
                    disabled={viewerAlbumMutationDisabled || assignableAlbumOptions.length === 0}
                  />
                  <Button
                    type="default"
                    icon={<FolderOpenOutlined />}
                    onClick={() => void handleAddViewerPhotoToAlbum()}
                    disabled={viewerAlbumMutationDisabled || !viewerAlbumInputUid}
                  >
                    앨범 등록
                  </Button>
                </div>
                {viewerMembershipAlbums.length > 0 ? (
                  <div className="photoprism-app__viewer-album-chip-list">
                    {viewerMembershipAlbums.map((album) => {
                      const uid = album.UID?.trim() ?? '';
                      const isPending = viewerAlbumMutationKey === `remove:${uid}`;

                      return (
                        <button
                          key={uid || album.Title || album.Name}
                          type="button"
                          className="photoprism-app__viewer-album-chip"
                          onClick={() => void handleRemoveViewerPhotoFromAlbum(album)}
                          disabled={viewerAlbumMutationDisabled}
                        >
                          <span>{album.Title ?? album.Name ?? 'Album'}</span>
                          {isPending ? <Spin size="small" /> : <CloseCircleOutlined />}
                        </button>
                      );
                    })}
                  </div>
                ) : (
                  <div className="photoprism-app__viewer-album-empty">
                    해제할 수동 앨범이 아직 없습니다.
                  </div>
                )}
              </section>
              {isViewerDetailLoading ? (
                <div className="photoprism-app__viewer-panel-loading">
                  <Spin size="small" />
                  <span>상세 정보를 불러오는 중입니다.</span>
                </div>
              ) : null}
              <dl className="photoprism-app__viewer-details photoprism-app__viewer-details--flat">
                {viewerDetailRows.map((item) => (
                  <div
                    key={item.key}
                    className={`photoprism-app__viewer-detail-row${item.tone === 'muted' ? ' is-muted' : ''}`}
                  >
                    <dt>{item.label}</dt>
                    <dd>{item.value}</dd>
                  </div>
                ))}
              </dl>
            </section>
          ) : null}
          {viewerRail}
        </div>
      </FullscreenPreviewModal>
    </section>
  );

  if (!isMounted || typeof document === 'undefined') {
    return null;
  }

  if (isEmbeddedLaunch) {
    return overlay;
  }

  return createPortal(overlay, document.body);
}
