import {
  AppstoreOutlined,
  ArrowLeftOutlined,
  ArrowRightOutlined,
  BellOutlined,
  CloseOutlined,
  DeleteOutlined,
  EditOutlined,
  HistoryOutlined,
  PauseCircleOutlined,
  PlayCircleOutlined,
  PlusOutlined,
  ReloadOutlined,
  SendOutlined,
  SettingOutlined,
} from '@ant-design/icons';
import { Button, Checkbox, Drawer, Input, InputNumber, Select, Tag, message } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import {
  fetchWebPushConfig,
  fetchWebPushSubscriptions,
  sendClientNotification,
  shouldFallbackToLocalNotification,
  showLocalClientNotification,
  type WebPushSubscriptionItem,
} from '../../../../app/main/notificationApi';
import { getSavedNotificationDeviceId } from '../../../../app/main/notificationIdentity';
import { getOrCreateClientId } from '../../../../app/main/clientIdentity';
import {
  ensureWebPushSubscriptionRegistered,
  serializePushSubscription,
  syncExistingWebPushSubscriptionRegistration,
} from '../../../../app/main/webPushRegistration';
import { useTokenAccess } from '../../../../app/main/tokenAccess';
import {
  createBaseballTicketBayAlert,
  deleteBaseballTicketBayAlert,
  deleteBaseballTicketBayLog,
  fetchBaseballTicketBayAlerts,
  fetchBaseballTicketBayLogs,
  runBaseballTicketBayAlert as runBaseballTicketBayAlertRequest,
  setBaseballTicketBayShareTokenOverride,
  updateBaseballTicketBayAlert,
  type BaseballTicketBayAlertItem as TicketAlertItem,
  type BaseballTicketBayAlertLogItem as AlertLogItem,
  type BaseballTicketBayMatchResult,
  type BaseballTicketBayTimeWindow as TimeWindow,
} from './baseballTicketBayApi';
import './BaseballTicketBayPlayAppView.css';

type BaseballTicketBayPlayAppViewProps = {
  onBack: () => void;
  launchContext?: 'direct' | 'embedded';
  shareToken?: string | null;
};

type DraftAlert = {
  title: string;
  eventDate: string;
  team: string;
  zone: string;
  aisleSide: string;
  seatDirections: string[];
  maxPrice: number | null;
  seatCount: number;
  batchIntervalMinutes: number;
  sameProductAlertEnabled: boolean;
  sameProductNotifyOnce: boolean;
  timeWindows: TimeWindow[];
};

type AppPanel = 'list' | 'editor' | 'success-list' | 'success-detail';
type EditorStep = 'basic' | 'seat' | 'schedule';
type AlertLogAction = 'create' | 'run' | 'pause' | 'resume' | 'delete' | 'push';

type SuccessLogRow = {
  id: string;
  sourceLogId: string;
  clientId: string;
  ownerType: 'client' | 'shared-token';
  ownerId: string;
  ownerLabel?: string | null;
  alertTitle: string;
  ticketTitle: string;
  eventDateTime: string;
  teamName: string;
  zone: string;
  rowLabel: string;
  row: string;
  opponentOrFloor: string;
  grade: string;
  price: string;
  seatCount: string;
  transactionType: string;
  sellerPost: string;
  productRemarks: string[];
  seatRemarks: string[];
  seatMapImageUrl: string | null;
  photoUrls: string[];
  sellerPhotoCount: number;
  saleUrl: string;
  createdAt: string;
  summary: string;
};

type PushRegistrationStatusRow = {
  key: string;
  label: string;
  value: string;
  detail?: string;
};

type PushTestDiagnostic = {
  status: 'success' | 'error';
  rows: PushRegistrationStatusRow[];
};

type BaseballTicketBayAccessScope = 'all' | 'client' | 'shared-token';

const TEAM_OPTIONS = ['전체', 'LG', '두산', 'SSG', '키움', 'KT', 'KIA', 'NC', '롯데', '삼성', '한화'].map((value) => ({
  label: value,
  value,
}));
const ZONE_OPTIONS = ['전체', '내야', '외야', '테이블석', '중앙석', '응원석', '프리미엄석', '휠체어석'].map((value) => ({
  label: value,
  value,
}));
const AISLE_SIDE_OPTIONS = ['전체', '통로측', '중앙측', '내측', '외측'].map((value) => ({
  label: value,
  value,
}));
const SEAT_DIRECTION_OPTIONS = ['1루', '3루', '외야'].map((value) => ({
  label: value,
  value,
}));
const KST_TIME_ZONE = 'Asia/Seoul';
const NEXT_BATCH_SOON_THRESHOLD_MS = 60_000;

function createId(prefix: string) {
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
    return `${prefix}-${crypto.randomUUID()}`;
  }

  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}

function createTimeWindow(start = '18:00', end = '23:30'): TimeWindow {
  return {
    id: createId('window'),
    start,
    end,
  };
}

function createInitialDraft(): DraftAlert {
  return {
    title: '',
    eventDate: '',
    team: '전체',
    zone: '전체',
    aisleSide: '전체',
    seatDirections: [],
    maxPrice: null,
    seatCount: 2,
    batchIntervalMinutes: 3,
    sameProductAlertEnabled: true,
    sameProductNotifyOnce: false,
    timeWindows: [createTimeWindow()],
  };
}

function formatDateLabel(value: string) {
  if (!value) {
    return '-';
  }

  return value.replaceAll('-', '.');
}

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

  const date = new Date(value);

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

  return new Intl.DateTimeFormat('ko-KR', {
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false,
  }).format(date);
}

function formatBatchDateTimeLabel(value: Date) {
  return new Intl.DateTimeFormat('ko-KR', {
    timeZone: KST_TIME_ZONE,
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    hour12: false,
  }).format(value);
}

function getKstDateParts(value: Date) {
  const parts = new Intl.DateTimeFormat('en-CA', {
    timeZone: KST_TIME_ZONE,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false,
  }).formatToParts(value);
  const read = (type: Intl.DateTimeFormatPartTypes) => Number(parts.find((part) => part.type === type)?.value ?? '0');

  return {
    year: read('year'),
    month: read('month'),
    day: read('day'),
    hour: read('hour'),
    minute: read('minute'),
  };
}

function createKstDate(year: number, month: number, day: number, hour: number, minute: number) {
  return new Date(Date.UTC(year, month - 1, day, hour - 9, minute, 0));
}

function getKstMinutesOfDay(value: Date) {
  const parts = getKstDateParts(value);
  return parts.hour * 60 + parts.minute;
}

function parseTimeWindowMinutes(value: string) {
  const [hourText, minuteText] = value.split(':');
  return Number(hourText ?? '0') * 60 + Number(minuteText ?? '0');
}

function createKstDateFromMinutes(baseDate: Date, minutesOfDay: number) {
  const parts = getKstDateParts(baseDate);
  const normalizedMinutes = ((minutesOfDay % (24 * 60)) + 24 * 60) % (24 * 60);
  const hour = Math.floor(normalizedMinutes / 60);
  const minute = normalizedMinutes % 60;
  return createKstDate(parts.year, parts.month, parts.day, hour, minute);
}

function sortTimeWindows(timeWindows: TimeWindow[]) {
  return [...timeWindows].sort((left, right) => parseTimeWindowMinutes(left.start) - parseTimeWindowMinutes(right.start));
}

function findBlockedTimeWindow(timeWindows: TimeWindow[], value: Date) {
  if (!timeWindows.length) {
    return null;
  }

  const currentMinutes = getKstMinutesOfDay(value);
  return timeWindows.find((timeWindow) => {
    const startMinutes = parseTimeWindowMinutes(timeWindow.start);
    const endMinutes = parseTimeWindowMinutes(timeWindow.end);
    return startMinutes <= currentMinutes && currentMinutes <= endMinutes;
  });
}

function isWithinBlockedTimeWindow(timeWindows: TimeWindow[], value: Date) {
  return Boolean(findBlockedTimeWindow(timeWindows, value));
}

function resolveBlockedWindowEnd(timeWindows: TimeWindow[], value: Date) {
  if (!timeWindows.length) {
    return value;
  }

  const blockedWindow = findBlockedTimeWindow(timeWindows, value);

  if (!blockedWindow) {
    return value;
  }

  const endMinutes = parseTimeWindowMinutes(blockedWindow.end);
  const nextMinutes = endMinutes + 1;

  if (nextMinutes < 24 * 60) {
    return createKstDateFromMinutes(value, nextMinutes);
  }

  const nextDay = new Date(createKstDateFromMinutes(value, 0).getTime() + 24 * 60 * 60 * 1000);
  return createKstDateFromMinutes(nextDay, 0);
}

function resolveNextBatchRunAt(item: TicketAlertItem, now = new Date()) {
  if (!item.active) {
    return null;
  }

  const intervalMs = item.batchIntervalMinutes * 60 * 1000;
  const lastRunAt = item.lastRunAt ? Date.parse(item.lastRunAt) : Number.NaN;
  const dueBase = Number.isFinite(lastRunAt) ? new Date(lastRunAt + intervalMs) : now;

  if (dueBase.getTime() <= now.getTime()) {
    return isWithinBlockedTimeWindow(item.timeWindows, now) ? resolveBlockedWindowEnd(item.timeWindows, now) : now;
  }

  return isWithinBlockedTimeWindow(item.timeWindows, dueBase) ? resolveBlockedWindowEnd(item.timeWindows, dueBase) : dueBase;
}

function formatNextBatchScheduleLabel(item: TicketAlertItem, now = new Date()) {
  if (!item.active) {
    return '다음 배치 중지';
  }

  const nextRunAt = resolveNextBatchRunAt(item, now);

  if (!nextRunAt) {
    return '다음 배치 확인 불가';
  }

  if (nextRunAt.getTime() - now.getTime() <= NEXT_BATCH_SOON_THRESHOLD_MS) {
    return '다음 배치 곧 실행';
  }

  return `다음 배치 ${formatBatchDateTimeLabel(nextRunAt)}`;
}

function formatCurrencyLabel(value: number | null | undefined) {
  if (typeof value !== 'number' || !Number.isFinite(value)) {
    return '-';
  }

  return `${value.toLocaleString()}원`;
}

function formatTransactionTypeLabel(result: BaseballTicketBayMatchResult) {
  const labels = [];

  if (result.pinTrade) {
    labels.push('PIN');
  }

  if (result.deliveryTrade) {
    labels.push('배송');
  }

  if (result.fieldTrade) {
    labels.push('현장');
  }

  if (result.etcTrade) {
    labels.push('기타');
  }

  if (result.transactionType) {
    return result.transactionType;
  }

  return labels.join(', ') || '-';
}

function resolveLogTagColor(status: AlertLogItem['status']) {
  if (status === 'success') {
    return 'green';
  }

  if (status === 'warning') {
    return 'gold';
  }

  if (status === 'error') {
    return 'red';
  }

  return 'default';
}

function resolveLogStatusLabel(status: AlertLogItem['status']) {
  if (status === 'success') {
    return '성공';
  }

  if (status === 'warning') {
    return '확인';
  }

  if (status === 'error') {
    return '실패';
  }

  return '기록';
}

function waitForPushServiceWorkerStateChange(
  worker: ServiceWorker,
  timeoutMs: number,
) {
  return new Promise<void>((resolve) => {
    let completed = false;

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

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

    const onStateChange = () => {
      if (worker.state === 'activated' || worker.state === 'installed') {
        finish();
      }
    };

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

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

  const serviceWorkerUrl = import.meta.env.DEV ? '/dev-sw.js?dev-sw' : '/sw.js';
  const registrationOptions = import.meta.env.DEV ? { scope: '/', type: 'module' as const } : { scope: '/' };
  const registration =
    (await navigator.serviceWorker.getRegistration()) ??
    (await navigator.serviceWorker.register(serviceWorkerUrl, registrationOptions));

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

  if (registration.installing) {
    await waitForPushServiceWorkerStateChange(registration.installing, 30_000);
  }

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

  try {
    return await Promise.race([
      navigator.serviceWorker.ready,
      new Promise<null>((resolve) => {
        window.setTimeout(() => resolve(null), 30_000);
      }),
    ]);
  } catch {
    return registration;
  }
}

async function ensurePushRegistration() {
  if (typeof window === 'undefined' || typeof navigator === 'undefined') {
    throw new Error('브라우저 환경에서만 사용할 수 있습니다.');
  }

  if (!('serviceWorker' in navigator) || !('PushManager' in window) || typeof Notification === 'undefined') {
    throw new Error('이 브라우저는 Web Push를 지원하지 않습니다.');
  }

  const permission = Notification.permission === 'granted' ? 'granted' : await Notification.requestPermission();

  if (permission !== 'granted') {
    throw new Error('알림 권한이 허용되지 않았습니다.');
  }

  const config = await fetchWebPushConfig();

  if (!config.enabled || !config.publicKey) {
    throw new Error('서버 Web Push 설정이 비어 있습니다.');
  }

  const registration = await resolvePushServiceWorkerRegistration();

  if (!registration) {
    throw new Error('서비스 워커를 준비하지 못했습니다.');
  }

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

async function getCurrentPushRegistration() {
  if (typeof window === 'undefined' || typeof navigator === 'undefined') {
    return null;
  }

  if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
    return null;
  }

  const registration = await resolvePushServiceWorkerRegistration();

  if (!registration) {
    return null;
  }

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

  return {
    registration,
    subscription,
  };
}

function getNotificationPermissionLabel() {
  if (typeof Notification === 'undefined') {
    return '미지원';
  }

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

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

  return '미결정';
}

function buildPushStatusRows(args: {
  clientId: string;
  deviceId: string;
  browserSubscription: PushSubscription | null;
  serverSubscriptions: WebPushSubscriptionItem[];
  configEnabled: boolean;
  serviceWorkerReady: boolean;
}) {
  const serverSubscription = args.serverSubscriptions[0] ?? null;
  const endpointLabel = args.browserSubscription?.endpoint || serverSubscription?.endpoint || '-';
  const lastRegisteredAt = serverSubscription?.lastRegisteredAt ? formatDateTimeLabel(serverSubscription.lastRegisteredAt) : '-';
  const serverRegistrationDetail = serverSubscription
    ? `현재 기기 매칭 ${args.serverSubscriptions.length}건 · 마지막 등록 ${lastRegisteredAt}`
    : '현재 기기 기준 등록 레코드 없음';

  return [
    {
      key: 'permission',
      label: '브라우저 권한',
      value: getNotificationPermissionLabel(),
      detail: args.configEnabled ? '서버 Web Push 사용 가능' : '서버 Web Push 설정 없음',
    },
    {
      key: 'serviceWorker',
      label: '서비스워커',
      value: args.serviceWorkerReady ? '준비됨' : '없음',
      detail: args.browserSubscription ? '브라우저 구독 있음' : '브라우저 구독 없음',
    },
    {
      key: 'serverRegistration',
      label: '서버 등록',
      value: serverSubscription ? '등록됨' : '미등록',
      detail: serverRegistrationDetail,
    },
    {
      key: 'deviceId',
      label: '기기 ID',
      value: args.deviceId || '-',
      detail: args.clientId ? `기기 식별값 ${args.clientId}` : undefined,
    },
    {
      key: 'endpoint',
      label: '엔드포인트',
      value: endpointLabel === '-' ? '-' : '확인됨',
      detail: endpointLabel,
    },
  ] satisfies PushRegistrationStatusRow[];
}

function matchCurrentServerSubscriptions(args: {
  browserSubscription: PushSubscription | null;
  serverSubscriptions: WebPushSubscriptionItem[];
  deviceId: string;
  clientId: string;
}) {
  return args.serverSubscriptions.filter((item) => {
    if (args.browserSubscription?.endpoint && item.endpoint === args.browserSubscription.endpoint) {
      return true;
    }

    if (args.deviceId && item.deviceId === args.deviceId) {
      return true;
    }

    return Boolean(args.clientId) && item.clientId === args.clientId;
  });
}

function buildAlertSummary(item: DraftAlert | TicketAlertItem) {
  const priceLabel = item.maxPrice ? `${item.maxPrice.toLocaleString()}원 이하` : '가격 미지정';
  const directionLabel = item.seatDirections.length > 0 ? item.seatDirections.join(', ') : '방향 전체';
  const windowLabel = item.timeWindows.map((timeWindow) => `${timeWindow.start}-${timeWindow.end}`).join(', ');
  const sameProductLabel = item.sameProductAlertEnabled
    ? item.sameProductNotifyOnce
      ? '같은상품 1회 알림'
      : '같은상품 반복 알림'
    : '같은상품 제외';
  return [
    item.team,
    item.zone,
    item.aisleSide,
    directionLabel,
    `매당 ${priceLabel}`,
    `${item.seatCount}연석`,
    `${item.batchIntervalMinutes}분 주기`,
    `안받는 시간 ${windowLabel}`,
    sameProductLabel,
  ]
    .filter(Boolean)
    .join(' · ');
}

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

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

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

  return `${normalized.slice(0, 8)}...${normalized.slice(-4)}`;
}

function formatScopeIdentifierLabel(value: string | null | undefined) {
  return formatClientIdLabel(value?.trim() ?? '');
}

function formatOwnershipLabel(
  ownerType: 'client' | 'shared-token',
  ownerId: string | null | undefined,
  clientId: string,
  ownerLabel?: string | null,
) {
  if (ownerType === 'shared-token') {
    const normalizedOwnerLabel = ownerLabel?.trim() ?? '';
    return normalizedOwnerLabel || `토큰 ${formatScopeIdentifierLabel(ownerId)}`;
  }

  return `기기 ${formatClientIdLabel(clientId)}`;
}

function formatAccessScopeLabel(scope: BaseballTicketBayAccessScope) {
  if (scope === 'all') {
    return '전체 보기';
  }

  if (scope === 'shared-token') {
    return '공유토큰 기준';
  }

  return '내 기기 기준';
}

function normalizeShareToken(value: string | null | undefined) {
  return value?.trim() ?? '';
}

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

  return normalizeShareToken(new URLSearchParams(window.location.search).get('shareToken'));
}

export function BaseballTicketBayPlayAppView({ onBack, launchContext = 'direct', shareToken }: BaseballTicketBayPlayAppViewProps) {
  const [messageApi, contextHolder] = message.useMessage();
  const { hasAccess: hasGlobalAccess } = useTokenAccess();
  const [clientId, setClientId] = useState('');
  const [draft, setDraft] = useState<DraftAlert>(() => createInitialDraft());
  const [alerts, setAlerts] = useState<TicketAlertItem[]>([]);
  const [logs, setLogs] = useState<AlertLogItem[]>([]);
  const [hasAllClientScope, setHasAllClientScope] = useState(false);
  const [accessScope, setAccessScope] = useState<BaseballTicketBayAccessScope>('client');
  const [scopeOwnerId, setScopeOwnerId] = useState<string | null>(null);
  const [isPushPending, setIsPushPending] = useState(false);
  const [pushStatusLoading, setPushStatusLoading] = useState(false);
  const [pushStatusRows, setPushStatusRows] = useState<PushRegistrationStatusRow[]>([]);
  const [pushTestDiagnostic, setPushTestDiagnostic] = useState<PushTestDiagnostic | null>(null);
  const [isTestPushPending, setIsTestPushPending] = useState(false);
  const [isImmediateRunPending, setIsImmediateRunPending] = useState(false);
  const [activePanel, setActivePanel] = useState<AppPanel>('list');
  const [editorStep, setEditorStep] = useState<EditorStep>('basic');
  const [isSettingsOpen, setIsSettingsOpen] = useState(false);
  const [isLogDrawerOpen, setIsLogDrawerOpen] = useState(false);
  const [selectedLogAlertId, setSelectedLogAlertId] = useState<string | null>(null);
  const [selectedSuccessLogId, setSelectedSuccessLogId] = useState<string | null>(null);
  const [selectedSuccessRowIds, setSelectedSuccessRowIds] = useState<string[]>([]);
  const [editingAlertId, setEditingAlertId] = useState<string | null>(null);
  const isEmbeddedLaunch = launchContext === 'embedded';
  const effectiveShareToken = normalizeShareToken(shareToken) || readShareTokenFromUrl();

  useEffect(() => {
    setBaseballTicketBayShareTokenOverride(effectiveShareToken);

    return () => {
      setBaseballTicketBayShareTokenOverride('');
    };
  }, [effectiveShareToken]);

  useEffect(() => {
    const resolvedClientId = getOrCreateClientId().trim();
    setClientId(resolvedClientId);

    void (async () => {
      try {
        const [nextAlerts, nextLogs] = await Promise.all([fetchBaseballTicketBayAlerts(), fetchBaseballTicketBayLogs()]);
        setAlerts(nextAlerts.items);
        setLogs(nextLogs.items);
        setHasAllClientScope(nextAlerts.includeAllClients || nextLogs.includeAllClients);
        setAccessScope(nextAlerts.accessScope === nextLogs.accessScope ? nextAlerts.accessScope : nextAlerts.accessScope);
        setScopeOwnerId(nextAlerts.scopeOwnerId ?? nextLogs.scopeOwnerId ?? null);
      } catch (error) {
        messageApi.error(error instanceof Error ? error.message : '저장된 알림을 불러오지 못했습니다.');
      }
    })();
  }, []);

  const refreshPushStatus = async (options?: { syncExistingRegistration?: boolean }) => {
    const deviceId = getSavedNotificationDeviceId().trim();
    setPushStatusLoading(true);

    try {
      const config = await fetchWebPushConfig();
      const pushRegistration = await getCurrentPushRegistration();
      const browserSubscription = pushRegistration?.subscription ?? null;
      const serviceWorkerReady = Boolean(pushRegistration?.registration);

      if (
        options?.syncExistingRegistration &&
        config.enabled &&
        typeof Notification !== 'undefined' &&
        Notification.permission === 'granted' &&
        pushRegistration?.registration &&
        browserSubscription
      ) {
        await syncExistingWebPushSubscriptionRegistration(pushRegistration.registration, {
          deviceId,
        });
      }

      const serverSubscriptions = config.enabled ? await fetchWebPushSubscriptions() : [];
      const matchedSubscriptions = matchCurrentServerSubscriptions({
        browserSubscription,
        serverSubscriptions,
        deviceId,
        clientId,
      });

      setPushStatusRows(
        buildPushStatusRows({
          clientId,
          deviceId,
          browserSubscription,
          serverSubscriptions: matchedSubscriptions,
          configEnabled: Boolean(config.enabled && config.publicKey),
          serviceWorkerReady,
        }),
      );
    } catch (error) {
      setPushStatusRows([
        {
          key: 'error',
          label: '등록 상태 확인',
          value: '실패',
          detail: error instanceof Error ? error.message : '푸시 등록 상태를 확인하지 못했습니다.',
        },
      ]);
    } finally {
      setPushStatusLoading(false);
    }
  };

  useEffect(() => {
    if (!isSettingsOpen || !clientId) {
      return;
    }

    void refreshPushStatus({ syncExistingRegistration: true });
  }, [clientId, isSettingsOpen]);

  const activeCount = useMemo(() => alerts.filter((item) => item.active).length, [alerts]);
  const hasDraftTitle = draft.title.trim().length > 0;
  const visibleLogs = useMemo(
    () => (selectedLogAlertId ? logs.filter((item) => item.alertId === selectedLogAlertId) : logs),
    [logs, selectedLogAlertId],
  );
  const latestLogByAlertId = useMemo(() => {
    const map = new Map<string, AlertLogItem>();

    logs.forEach((item) => {
      if (!item.alertId || map.has(item.alertId)) {
        return;
      }

      map.set(item.alertId, item);
    });

    return map;
  }, [logs]);
  const alertMap = useMemo(() => new Map(alerts.map((item) => [item.id, item])), [alerts]);
  const successRows = useMemo<SuccessLogRow[]>(() => {
    return logs
      .filter((item) => item.status === 'success' && item.alertId && Array.isArray(item.payload?.results) && item.payload.results.length > 0)
      .flatMap((item) => {
        const alert = item.alertId ? alertMap.get(item.alertId) : null;

        if (!alert) {
          return [];
        }

        return item.payload!.results.map((result) => ({
          id: `${item.id}:${result.displayNumber}`,
          sourceLogId: item.id,
          clientId: item.clientId,
          ownerType: item.ownerType,
          ownerId: item.ownerId,
          ownerLabel: item.ownerLabel ?? alert?.ownerLabel ?? null,
          alertTitle: alert.title,
          ticketTitle: result.title,
          eventDateTime: result.eventDateTime,
          teamName: result.teamName || alert.team,
          zone: result.area,
          rowLabel: result.rowLabel,
          row: result.row,
          opponentOrFloor: result.opponentOrFloor,
          grade: result.grade,
          price: formatCurrencyLabel(result.price),
          seatCount: result.seatCount ? `${result.seatCount}연석` : '-',
          transactionType: formatTransactionTypeLabel(result),
          sellerPost: result.sellerPost,
          productRemarks: result.productRemarks,
          seatRemarks: result.seatRemarks,
          seatMapImageUrl: result.seatMapImageUrl,
          photoUrls: result.photoUrls,
          sellerPhotoCount: typeof result.sellerPhotoCount === 'number' ? result.sellerPhotoCount : result.photoUrls.length,
          saleUrl: result.saleUrl,
          createdAt: item.createdAt,
          summary: [result.area && `${result.area}구역`, result.row && `${result.rowLabel} ${result.row}`, result.grade, formatCurrencyLabel(result.price)]
            .filter(Boolean)
            .join(' · '),
        }));
      });
  }, [alertMap, logs]);
  const selectedSuccessRow = useMemo(
    () => successRows.find((item) => item.id === selectedSuccessLogId) ?? successRows[0] ?? null,
    [selectedSuccessLogId, successRows],
  );
  const selectedSuccessRowIdSet = useMemo(() => new Set(selectedSuccessRowIds), [selectedSuccessRowIds]);
  const selectedSuccessRows = useMemo(
    () => successRows.filter((item) => selectedSuccessRowIdSet.has(item.id)),
    [selectedSuccessRowIdSet, successRows],
  );
  const selectedSuccessSourceLogIds = useMemo(
    () => Array.from(new Set(selectedSuccessRows.map((item) => item.sourceLogId))),
    [selectedSuccessRows],
  );
  const selectableSuccessRows = useMemo(
    () => (accessScope === 'client' ? successRows.filter((item) => item.clientId === clientId) : successRows),
    [accessScope, clientId, successRows],
  );
  const isAllSuccessRowsSelected = selectableSuccessRows.length > 0 && selectedSuccessRowIds.length === selectableSuccessRows.length;
  const isSomeSuccessRowsSelected = selectedSuccessRowIds.length > 0 && selectedSuccessRowIds.length < selectableSuccessRows.length;
  const isViewingAllClients = !effectiveShareToken && (hasGlobalAccess || hasAllClientScope);
  const isSharedTokenScope = accessScope === 'shared-token';
  const scopeSummaryLabel = isSharedTokenScope ? '공유 토큰' : '접속 기기';
  const scopeSummaryValue = isSharedTokenScope
    ? formatScopeIdentifierLabel(scopeOwnerId || effectiveShareToken || clientId)
    : (clientId ? clientId.slice(0, 12) : '-');
  const canManageByClientId = (targetClientId: string) => {
    if (accessScope === 'shared-token') {
      return true;
    }

    if (accessScope === 'all') {
      return true;
    }

    return targetClientId === clientId;
  };

  useEffect(() => {
    if (!successRows.length) {
      if (selectedSuccessLogId !== null) {
        setSelectedSuccessLogId(null);
      }
      return;
    }

    if (!selectedSuccessLogId || !successRows.some((item) => item.id === selectedSuccessLogId)) {
      setSelectedSuccessLogId(successRows[0].id);
    }
  }, [selectedSuccessLogId, successRows]);

  useEffect(() => {
    setSelectedSuccessRowIds((previous) => previous.filter((rowId) => successRows.some((item) => item.id === rowId)));
  }, [successRows]);

  const isSuccessPanel = activePanel === 'success-list' || activePanel === 'success-detail';

  const openSuccessList = () => {
    setSelectedSuccessLogId((previous) => previous ?? successRows[0]?.id ?? null);
    setIsLogDrawerOpen(false);
    setSelectedLogAlertId(null);
    setActivePanel('success-list');
  };

  const openSuccessDetail = (successLogId: string) => {
    setSelectedSuccessLogId(successLogId);
    setActivePanel('success-detail');
  };

  const editorSteps = useMemo(
    () => [
      { key: 'basic' as const, label: '기본', description: hasDraftTitle ? draft.title.trim() : '제목, 날짜, 팀' },
      {
        key: 'seat' as const,
        label: '좌석',
        description:
          draft.seatDirections.length > 0 ? `${draft.seatDirections.join(', ')} · ${draft.zone}` : `${draft.zone} · 방향 전체`,
      },
      {
        key: 'schedule' as const,
        label: '시간',
        description: `${draft.batchIntervalMinutes}분 주기 · ${draft.timeWindows.length}개`,
      },
    ],
    [draft.batchIntervalMinutes, draft.seatDirections, draft.timeWindows.length, draft.title, draft.zone, hasDraftTitle],
  );

  const handleDraftChange = <Key extends keyof DraftAlert>(key: Key, value: DraftAlert[Key]) => {
    setDraft((previous) => ({
      ...previous,
      [key]: value,
    }));
  };

  const handleTimeWindowChange = (windowId: string, key: 'start' | 'end', value: string) => {
    setDraft((previous) => ({
      ...previous,
      timeWindows: previous.timeWindows.map((timeWindow) =>
        timeWindow.id === windowId
          ? {
              ...timeWindow,
              [key]: value,
            }
          : timeWindow,
      ),
    }));
  };

  const handleAddTimeWindow = () => {
    setDraft((previous) => ({
      ...previous,
      timeWindows: [...previous.timeWindows, createTimeWindow()],
    }));
  };

  const handleRemoveTimeWindow = (windowId: string) => {
    setDraft((previous) => ({
      ...previous,
      timeWindows:
        previous.timeWindows.length <= 1
          ? previous.timeWindows
          : previous.timeWindows.filter((timeWindow) => timeWindow.id !== windowId),
    }));
  };

  const appendLog = (
    item: Pick<AlertLogItem, 'alertId' | 'alertTitle' | 'action' | 'status' | 'message' | 'detail'>
      & Partial<Pick<AlertLogItem, 'ownerType' | 'ownerId' | 'ownerLabel'>>,
  ) => {
    setLogs((previous) => [
      {
        id: createId('log'),
        clientId,
        createdAt: new Date().toISOString(),
        ownerType: 'client',
        ownerId: clientId,
        ownerLabel: null,
        ...item,
      },
      ...previous,
    ].slice(0, 200));
  };

  const reloadServerData = async () => {
    const [nextAlerts, nextLogs] = await Promise.all([fetchBaseballTicketBayAlerts(), fetchBaseballTicketBayLogs()]);
    setAlerts(nextAlerts.items);
    setLogs(nextLogs.items);
    setHasAllClientScope(nextAlerts.includeAllClients || nextLogs.includeAllClients);
    setAccessScope(nextAlerts.accessScope === nextLogs.accessScope ? nextAlerts.accessScope : nextAlerts.accessScope);
    setScopeOwnerId(nextAlerts.scopeOwnerId ?? nextLogs.scopeOwnerId ?? null);
  };

  const runAlertNow = async (
    alert: TicketAlertItem,
    action: 'create' | 'run' | 'resume',
    options?: { openLogsOnComplete?: boolean },
  ) => {
    setIsImmediateRunPending(true);

    try {
      const result = await runBaseballTicketBayAlertRequest(alert.id);
      await reloadServerData();
      const latestMessage = result.log.message || '즉시 실행을 완료했습니다.';

      if (result.log.status === 'success') {
        messageApi.success(action === 'create' ? '저장 후 실제 검색까지 완료했습니다.' : latestMessage);
      } else if (result.log.status === 'warning') {
        messageApi.warning(latestMessage);
      } else if (result.log.status === 'error') {
        messageApi.error(latestMessage);
      } else {
        messageApi.info(latestMessage);
      }
    } catch (error) {
      appendLog({
        alertId: alert.id,
        alertTitle: alert.title,
        action,
        status: 'error',
        message: error instanceof Error ? error.message : '즉시 실행에 실패했습니다.',
        detail: buildAlertSummary(alert),
        ownerType: alert.ownerType,
        ownerId: alert.ownerId,
        ownerLabel: alert.ownerLabel ?? null,
      });
      messageApi.error(error instanceof Error ? error.message : '즉시 실행에 실패했습니다.');
    } finally {
      setIsImmediateRunPending(false);
      if (options?.openLogsOnComplete) {
        setSelectedLogAlertId(alert.id);
        setIsLogDrawerOpen(true);
      }
    }
  };

  const handleCreateAlert = async () => {
    if (!draft.title.trim()) {
      messageApi.error('알림 제목을 입력해 주세요.');
      return;
    }

    if (!draft.eventDate) {
      messageApi.error('경기 날짜를 먼저 입력해 주세요.');
      return;
    }

    const normalizedWindows = draft.timeWindows
      .map((timeWindow) => ({
        ...timeWindow,
        start: timeWindow.start || '00:00',
        end: timeWindow.end || '23:59',
      }))
      .filter((timeWindow) => timeWindow.start <= timeWindow.end);

    if (!normalizedWindows.length) {
      messageApi.error('안받을 시간 범위를 1개 이상 올바르게 입력해 주세요.');
      return;
    }

    const nextItem: TicketAlertItem = {
      id: editingAlertId ?? createId('alert'),
      clientId,
      ownerType: accessScope === 'shared-token' ? 'shared-token' : 'client',
      ownerId: accessScope === 'shared-token' ? scopeOwnerId ?? clientId : clientId,
      title: draft.title.trim(),
      eventDate: draft.eventDate,
      team: draft.team,
      zone: draft.zone,
      aisleSide: draft.aisleSide,
      seatDirections: draft.seatDirections,
      maxPrice: draft.maxPrice,
      seatCount: draft.seatCount,
      batchIntervalMinutes: draft.batchIntervalMinutes,
      sameProductAlertEnabled: draft.sameProductAlertEnabled,
      sameProductNotifyOnce: draft.sameProductAlertEnabled ? draft.sameProductNotifyOnce : false,
      active: true,
      timeWindows: normalizedWindows,
      createdAt: editingAlertId
        ? alerts.find((item) => item.id === editingAlertId)?.createdAt ?? new Date().toISOString()
        : new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      lastRunAt: null,
      lastMatchAt: null,
    };

    const isEditing = Boolean(editingAlertId);
    try {
      const savedAlert = isEditing
        ? await updateBaseballTicketBayAlert(nextItem.id, {
            title: nextItem.title,
            eventDate: nextItem.eventDate,
            team: nextItem.team,
            zone: nextItem.zone,
            aisleSide: nextItem.aisleSide,
            seatDirections: nextItem.seatDirections,
            maxPrice: nextItem.maxPrice,
            seatCount: nextItem.seatCount,
            batchIntervalMinutes: nextItem.batchIntervalMinutes,
            sameProductAlertEnabled: nextItem.sameProductAlertEnabled,
            sameProductNotifyOnce: nextItem.sameProductNotifyOnce,
            active: nextItem.active,
            timeWindows: nextItem.timeWindows,
          })
        : await createBaseballTicketBayAlert({
            title: nextItem.title,
            eventDate: nextItem.eventDate,
            team: nextItem.team,
            zone: nextItem.zone,
            aisleSide: nextItem.aisleSide,
            seatDirections: nextItem.seatDirections,
            maxPrice: nextItem.maxPrice,
            seatCount: nextItem.seatCount,
            batchIntervalMinutes: nextItem.batchIntervalMinutes,
            sameProductAlertEnabled: nextItem.sameProductAlertEnabled,
            sameProductNotifyOnce: nextItem.sameProductNotifyOnce,
            active: nextItem.active,
            timeWindows: nextItem.timeWindows,
          });

      await reloadServerData();
      setDraft(createInitialDraft());
      setEditingAlertId(null);
      setEditorStep('basic');
      setActivePanel('list');
      await runAlertNow(savedAlert, isEditing ? 'run' : 'create');
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '알림 저장에 실패했습니다.');
    }
  };

  const handleEditAlert = (alertId: string) => {
    const target = alerts.find((item) => item.id === alertId);

    if (!target) {
      return;
    }

    setDraft({
      title: target.title,
      eventDate: target.eventDate,
      team: target.team,
      zone: target.zone,
      aisleSide: target.aisleSide,
      seatDirections: [...target.seatDirections],
      maxPrice: target.maxPrice,
      seatCount: target.seatCount,
      batchIntervalMinutes: target.batchIntervalMinutes,
      sameProductAlertEnabled: target.sameProductAlertEnabled,
      sameProductNotifyOnce: target.sameProductNotifyOnce,
      timeWindows: target.timeWindows.map((timeWindow) => ({ ...timeWindow })),
    });
    setEditingAlertId(target.id);
    setEditorStep('basic');
    setActivePanel('editor');
  };

  const handleResetDraft = () => {
    setDraft(createInitialDraft());
    setEditingAlertId(null);
    setEditorStep('basic');
  };

  const handleToggleAlert = async (alertId: string) => {
    const target = alerts.find((item) => item.id === alertId);

    if (!target) {
      return;
    }

    const nextActive = !target.active;

    try {
      const updated = await updateBaseballTicketBayAlert(alertId, { active: nextActive });
      await reloadServerData();

      if (nextActive) {
        await runAlertNow(updated, 'resume');
      }
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '알림 상태를 변경하지 못했습니다.');
    }
  };

  const handleDeleteAlert = async (alertId: string) => {
    try {
      await deleteBaseballTicketBayAlert(alertId);
      await reloadServerData();
      messageApi.success('알림 항목을 삭제했습니다.');
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '알림 삭제에 실패했습니다.');
    }
  };

  const handleDeleteSuccessLog = async (logId: string) => {
    if (typeof window !== 'undefined' && !window.confirm('이 성공 로그를 삭제할까요? 같은 실행에서 저장된 성공 항목이 함께 사라집니다.')) {
      return;
    }

    try {
      await deleteBaseballTicketBayLog(logId);
      await reloadServerData();
      messageApi.success('성공 로그를 삭제했습니다.');
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '성공 로그 삭제에 실패했습니다.');
    }
  };

  const handleToggleSuccessRowSelection = (rowId: string) => {
    const target = successRows.find((item) => item.id === rowId);

    if (!target || !canManageByClientId(target.clientId)) {
      return;
    }

    setSelectedSuccessRowIds((previous) => (previous.includes(rowId) ? previous.filter((item) => item !== rowId) : [...previous, rowId]));
  };

  const handleToggleAllSuccessRows = () => {
    setSelectedSuccessRowIds((previous) => (previous.length === selectableSuccessRows.length ? [] : selectableSuccessRows.map((item) => item.id)));
  };

  const handleDeleteSelectedSuccessLogs = async () => {
    if (!selectedSuccessSourceLogIds.length) {
      messageApi.error('삭제할 성공 항목을 먼저 선택해 주세요.');
      return;
    }

    const confirmMessage =
      selectedSuccessSourceLogIds.length === 1
        ? '선택한 성공 항목을 삭제할까요? 같은 실행에서 저장된 성공 항목이 함께 사라집니다.'
        : `선택한 성공 항목 ${selectedSuccessRowIds.length}건을 삭제할까요? ${selectedSuccessSourceLogIds.length}개 실행 로그가 함께 삭제됩니다.`;

    if (typeof window !== 'undefined' && !window.confirm(confirmMessage)) {
      return;
    }

    const results = await Promise.allSettled(selectedSuccessSourceLogIds.map((logId) => deleteBaseballTicketBayLog(logId)));
    const successCount = results.filter((result) => result.status === 'fulfilled').length;
    const failureResults = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');

    try {
      await reloadServerData();
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '성공 로그 목록을 다시 불러오지 못했습니다.');
      return;
    }

    setSelectedSuccessRowIds([]);

    if (!failureResults.length) {
      messageApi.success(`선택한 성공 항목 ${selectedSuccessRowIds.length}건을 삭제했습니다.`);
      return;
    }

    if (successCount > 0) {
      messageApi.warning(
        `선택 삭제 중 ${successCount}개 실행 로그만 삭제했습니다. ${
          failureResults[0].reason instanceof Error ? failureResults[0].reason.message : '일부 성공 로그 삭제에 실패했습니다.'
        }`,
      );
      return;
    }

    messageApi.error(
      failureResults[0].reason instanceof Error ? failureResults[0].reason.message : '선택한 성공 로그 삭제에 실패했습니다.',
    );
  };

  const handleReload = async () => {
    try {
      await reloadServerData();
      await refreshPushStatus();
      messageApi.success('서버 알림 목록을 다시 불렀습니다.');
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '알림 목록을 다시 불러오지 못했습니다.');
    }
  };

  const handleEnablePush = async () => {
    setIsPushPending(true);

    try {
      await ensurePushRegistration();
      await refreshPushStatus({ syncExistingRegistration: false });
      appendLog({
        alertId: null,
        alertTitle: '공통',
        action: 'push',
        status: 'success',
        message: '현재 기기의 Web Push 등록을 완료했습니다.',
        detail: clientId || '-',
      });
      messageApi.success('현재 기기의 Web Push 등록을 완료했습니다.');
    } catch (error) {
      appendLog({
        alertId: null,
        alertTitle: '공통',
        action: 'push',
        status: 'error',
        message: error instanceof Error ? error.message : 'Web Push 등록에 실패했습니다.',
        detail: clientId || '-',
      });
      messageApi.error(error instanceof Error ? error.message : 'Web Push 등록에 실패했습니다.');
    } finally {
      setIsPushPending(false);
    }
  };

  const handleSendTestPush = async () => {
    if (!clientId) {
      messageApi.error('기기 식별값을 확인하지 못했습니다.');
      return;
    }

    setIsTestPushPending(true);

    try {
      const deviceId = getSavedNotificationDeviceId().trim();
      const config = await fetchWebPushConfig();
      const pushRegistration = await getCurrentPushRegistration();
      const browserSubscription = pushRegistration?.subscription ?? null;
      const serviceWorkerReady = Boolean(pushRegistration?.registration);

      if (
        config.enabled &&
        typeof Notification !== 'undefined' &&
        Notification.permission === 'granted' &&
        pushRegistration?.registration &&
        browserSubscription
      ) {
        await syncExistingWebPushSubscriptionRegistration(pushRegistration.registration, {
          deviceId,
        });
      }

      const serverSubscriptions = config.enabled ? await fetchWebPushSubscriptions() : [];
      const matchedSubscriptions = matchCurrentServerSubscriptions({
        browserSubscription,
        serverSubscriptions,
        deviceId,
        clientId,
      });

      setPushStatusRows(
        buildPushStatusRows({
          clientId,
          deviceId,
          browserSubscription,
          serverSubscriptions: matchedSubscriptions,
          configEnabled: Boolean(config.enabled && config.publicKey),
          serviceWorkerReady,
        }),
      );

      if (config.enabled && !browserSubscription) {
        throw new Error('브라우저 Web Push 구독이 없습니다. 먼저 푸시 등록을 다시 실행해 주세요.');
      }

      if (config.enabled && matchedSubscriptions.length < 1) {
        throw new Error('현재 기기와 일치하는 서버 Web Push 등록을 찾지 못했습니다. 상태 새로고침 후 다시 확인해 주세요.');
      }

      const targetClientIds = [
        clientId,
        deviceId,
        ...matchedSubscriptions.flatMap((item) => [item.clientId, item.deviceId]),
      ].filter((value, index, values) => Boolean(value) && values.indexOf(value) === index);
      const notificationTitle = draft.title.trim() || '야구-티켓베이 테스트';
      const result = await sendClientNotification({
        title: notificationTitle,
        body: `${draft.team} · ${draft.eventDate || '날짜 미지정'} · ${buildAlertSummary(draft)}`,
        targetClientIds,
        targetAppOrigins: typeof window !== 'undefined' ? [window.location.origin] : undefined,
        data: {
          category: 'play-app',
          type: 'baseball-ticket-bay',
          notificationScope: 'automation',
          notificationKey: `baseball-ticket-bay:test:${Date.now()}`,
          targetUrl: typeof window !== 'undefined' ? window.location.href : '/play/apps?app=baseball-ticket-bay',
        },
      });

      if (shouldFallbackToLocalNotification(result)) {
        await showLocalClientNotification({
          title: notificationTitle,
          body: `${draft.team} · ${draft.eventDate || '날짜 미지정'} · ${buildAlertSummary(draft)}`,
          data: {
            category: 'play-app',
            type: 'baseball-ticket-bay',
            notificationScope: 'automation',
          },
        });
      }

      const matchedCount = result.web.matchedCount ?? matchedSubscriptions.length;
      const matchedLabels = (result.web.matchedSubscriptions ?? matchedSubscriptions)
        .map((item) => item.deviceId || item.clientId || item.endpoint)
        .filter(Boolean);

      setPushTestDiagnostic({
        status: 'success',
        rows: [
          {
            key: 'test-send-result',
            label: '최근 테스트 발송',
            value: `${result.web.sentCount}건 전송`,
            detail: `매칭 ${matchedCount}건 · 실패 ${result.web.failedCount}건`,
          },
          {
            key: 'test-send-targets',
            label: '매칭 대상',
            value: matchedLabels.length > 0 ? `${matchedLabels.length}건` : '-',
            detail: matchedLabels.join(', ') || '현재 기기 기준 대상 없음',
          },
        ],
      });

      if (result.web.sentCount < 1) {
        throw new Error(result.web.reason || '현재 기기와 일치하는 Web Push 구독을 찾지 못했습니다.');
      }

      messageApi.success(`테스트 알림을 전송했습니다. 매칭 ${matchedCount}건`);
    } catch (error) {
      setPushTestDiagnostic({
        status: 'error',
        rows: [
          {
            key: 'test-send-result-error',
            label: '최근 테스트 발송',
            value: '실패',
            detail: error instanceof Error ? error.message : '테스트 알림 전송에 실패했습니다.',
          },
        ],
      });
      messageApi.error(error instanceof Error ? error.message : '테스트 알림 전송에 실패했습니다.');
    } finally {
      setIsTestPushPending(false);
    }
  };

  const footerActions: JSX.Element[] = [];

  if (editingAlertId) {
    footerActions.push(
      <Button key="cancel" aria-label="수정 취소" title="수정 취소" onClick={handleResetDraft}>
        취소
      </Button>,
    );
  }

  if (editorStep === 'basic') {
    footerActions.push(
      <Button key="basic-next" icon={<ArrowRightOutlined />} aria-label="좌석 단계로 이동" title="다음" onClick={() => setEditorStep('seat')}>
        다음
      </Button>,
    );
  }

  if (editorStep === 'seat') {
    footerActions.push(
      <Button key="seat-prev" icon={<ArrowLeftOutlined />} aria-label="기본 단계로 이동" title="이전" onClick={() => setEditorStep('basic')}>
        이전
      </Button>,
      <Button key="seat-next" icon={<ArrowRightOutlined />} aria-label="시간 단계로 이동" title="다음" onClick={() => setEditorStep('schedule')}>
        다음
      </Button>,
    );
  }

  if (editorStep === 'schedule') {
    footerActions.push(
      <Button key="schedule-prev" icon={<ArrowLeftOutlined />} aria-label="좌석 단계로 이동" title="이전" onClick={() => setEditorStep('seat')}>
        이전
      </Button>,
      <Button
        key="schedule-submit"
        type="primary"
        icon={editingAlertId ? <EditOutlined /> : <PlusOutlined />}
        aria-label={editingAlertId ? '알림 수정 저장' : '알림 생성'}
        title={editingAlertId ? '알림 수정 저장' : '알림 생성'}
        onClick={handleCreateAlert}
      >
        {editingAlertId ? '수정 저장' : '알림 생성'}
      </Button>,
    );
  }

  return (
    <div className={`baseball-ticket-bay-app${isEmbeddedLaunch ? ' baseball-ticket-bay-app--embedded' : ''}`}>
      {contextHolder}
      <header className="baseball-ticket-bay-app__header">
        <div className="baseball-ticket-bay-app__title-block">
          <div>
            <strong>야구-티켓베이</strong>
            <div className="baseball-ticket-bay-app__meta">
              <span>{scopeSummaryLabel} {scopeSummaryValue}</span>
              {isViewingAllClients || isSharedTokenScope ? (
                <Tag color="gold" bordered={false}>
                  {formatAccessScopeLabel(accessScope)}
                </Tag>
              ) : null}
              <Tag color="blue" bordered={false}>
                활성 {activeCount}
              </Tag>
            </div>
          </div>
        </div>
        <div className="baseball-ticket-bay-app__actions">
          <Button
            className="baseball-ticket-bay-app__header-shortcut"
            icon={<AppstoreOutlined />}
            aria-label="성공 내역 화면 열기"
            title="성공 내역"
            onClick={openSuccessList}
          />
          <Button icon={<HistoryOutlined />} aria-label="실행 로그 열기" title="실행 로그" onClick={() => setIsLogDrawerOpen(true)} />
          <Button icon={<SettingOutlined />} aria-label="앱 설정 열기" title="앱 설정" onClick={() => setIsSettingsOpen(true)} />
        </div>
      </header>

      <div className="baseball-ticket-bay-app__layout">
        {isSuccessPanel ? (
          <section className="baseball-ticket-bay-app__success-screen" aria-label="성공 내역">
            <div className="baseball-ticket-bay-app__section-tabs" role="tablist" aria-label="화면 전환">
              <button
                type="button"
                className={`baseball-ticket-bay-app__section-tab${activePanel === 'list' ? ' is-active' : ''}`}
                aria-label="알림 목록 화면"
                title="알림 목록"
                onClick={() => setActivePanel('list')}
              >
                <HistoryOutlined />
                <span>목록</span>
              </button>
              <button
                type="button"
                className={`baseball-ticket-bay-app__section-tab${activePanel === 'editor' ? ' is-active' : ''}`}
                aria-label="새 알림 화면"
                title="새 알림"
                onClick={() => setActivePanel('editor')}
              >
                <PlusOutlined />
                <span>새 알림</span>
              </button>
              <button
                type="button"
                className={`baseball-ticket-bay-app__section-tab${isSuccessPanel ? ' is-active' : ''}`}
                aria-label="성공 내역 화면"
                title="성공 내역"
                onClick={openSuccessList}
              >
                <AppstoreOutlined />
                <span>성공</span>
              </button>
            </div>
            <div className="baseball-ticket-bay-app__success-screen-header">
              <div className="baseball-ticket-bay-app__success-screen-title">
                <strong>{activePanel === 'success-detail' ? '성공 상세' : '성공 목록'}</strong>
                <span>{successRows.length}건</span>
              </div>
              <div className="baseball-ticket-bay-app__success-screen-actions">
                {activePanel === 'success-detail' ? (
                  <Button icon={<ArrowLeftOutlined />} aria-label="성공 목록으로 돌아가기" title="목록으로" onClick={() => setActivePanel('success-list')}>
                    목록으로
                  </Button>
                ) : null}
                <Button icon={<HistoryOutlined />} aria-label="실행 로그 열기" title="실행 로그" onClick={() => setIsLogDrawerOpen(true)}>
                  전체 로그
                </Button>
              </div>
            </div>
            <div className="baseball-ticket-bay-app__success-screen-body">
              {successRows.length ? (
                <div className="baseball-ticket-bay-app__success-board">
                  {activePanel !== 'success-detail' ? (
                    <div className="baseball-ticket-bay-app__success-selection-toolbar">
                      <label className="baseball-ticket-bay-app__success-selection-toggle">
                        <Checkbox checked={isAllSuccessRowsSelected} indeterminate={isSomeSuccessRowsSelected} onChange={handleToggleAllSuccessRows} />
                        <span>전체 선택</span>
                      </label>
                      <div className="baseball-ticket-bay-app__success-selection-summary">
                        <span>
                          {selectedSuccessRowIds.length}건 선택
                          {selectedSuccessSourceLogIds.length ? ` · ${selectedSuccessSourceLogIds.length}개 실행 삭제` : ''}
                        </span>
                        <Button danger disabled={!selectedSuccessSourceLogIds.length} icon={<DeleteOutlined />} onClick={() => void handleDeleteSelectedSuccessLogs()}>
                          선택 삭제
                        </Button>
                      </div>
                    </div>
                  ) : null}
                  <div className={`baseball-ticket-bay-app__success-layout${activePanel === 'success-detail' ? ' is-detail-open' : ''}`}>
                    <div className="baseball-ticket-bay-app__success-list" role="list" aria-label="성공한 실행 목록">
                      {successRows.map((item) => (
                        <article
                          key={item.id}
                          className={`baseball-ticket-bay-app__success-item${selectedSuccessRow?.id === item.id ? ' is-active' : ''}`}
                        >
                          <div className="baseball-ticket-bay-app__success-item-selection">
                            <Checkbox
                              checked={selectedSuccessRowIdSet.has(item.id)}
                              disabled={!canManageByClientId(item.clientId)}
                              aria-label={`${item.ticketTitle} 성공 항목 선택`}
                              onChange={() => handleToggleSuccessRowSelection(item.id)}
                            />
                          </div>
                          <button
                            type="button"
                            className="baseball-ticket-bay-app__success-item-button"
                            onClick={() => openSuccessDetail(item.id)}
                          >
                            <div className="baseball-ticket-bay-app__success-item-top">
                              <strong>{item.ticketTitle}</strong>
                              <div className="baseball-ticket-bay-app__success-item-top-tags">
                                {isViewingAllClients || isSharedTokenScope ? (
                                  <Tag bordered={false}>{formatOwnershipLabel(item.ownerType, item.ownerId, item.clientId, item.ownerLabel)}</Tag>
                                ) : null}
                                <Tag bordered={false} color="green">
                                  성공
                                </Tag>
                              </div>
                            </div>
                            <div className="baseball-ticket-bay-app__success-item-meta">
                              <span>{formatDateTimeLabel(item.eventDateTime)}</span>
                              <span>{item.teamName}</span>
                              <span>{item.seatCount}</span>
                            </div>
                            <div className="baseball-ticket-bay-app__success-item-summary">{item.summary}</div>
                            <div className="baseball-ticket-bay-app__success-item-time">{formatDateTimeLabel(item.createdAt)}</div>
                          </button>
                        </article>
                      ))}
                    </div>
                    {selectedSuccessRow ? (
                      <article className="baseball-ticket-bay-app__success-detail" aria-label="성공 티켓 상세">
                      <div className="baseball-ticket-bay-app__success-detail-header">
                        <div className="baseball-ticket-bay-app__success-detail-copy">
                          <strong>{selectedSuccessRow.ticketTitle}</strong>
                          <div className="baseball-ticket-bay-app__success-detail-subtitle">
                            {formatDateTimeLabel(selectedSuccessRow.eventDateTime)} · {selectedSuccessRow.teamName}
                          </div>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-actions">
                          <Tag bordered={false} color="green">
                            성공
                          </Tag>
                          {isViewingAllClients || isSharedTokenScope ? (
                            <Tag bordered={false}>
                              {formatOwnershipLabel(selectedSuccessRow.ownerType, selectedSuccessRow.ownerId, selectedSuccessRow.clientId, selectedSuccessRow.ownerLabel)}
                            </Tag>
                          ) : null}
                          <Button
                            type="text"
                            danger
                            icon={<DeleteOutlined />}
                            aria-label="성공 로그 삭제"
                            title="성공 로그 삭제"
                            disabled={!canManageByClientId(selectedSuccessRow.clientId)}
                            onClick={() => void handleDeleteSuccessLog(selectedSuccessRow.sourceLogId)}
                          >
                            삭제
                          </Button>
                        </div>
                      </div>
                      <div className="baseball-ticket-bay-app__success-detail-grid">
                        <div className="baseball-ticket-bay-app__success-detail-field">
                          <span>구역</span>
                          <strong>{selectedSuccessRow.zone}</strong>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-field">
                          <span>열</span>
                          <strong>{selectedSuccessRow.row ? `${selectedSuccessRow.rowLabel} ${selectedSuccessRow.row}` : '-'}</strong>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-field">
                          <span>좌석 등급</span>
                          <strong>{selectedSuccessRow.grade || '-'}</strong>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-field">
                          <span>거래가</span>
                          <strong>{selectedSuccessRow.price}</strong>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-field">
                          <span>연석</span>
                          <strong>{selectedSuccessRow.seatCount}</strong>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-field">
                          <span>거래 방식</span>
                          <strong>{selectedSuccessRow.transactionType}</strong>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-field">
                          <span>직관샷</span>
                          <strong>{selectedSuccessRow.sellerPhotoCount > 0 ? `${selectedSuccessRow.sellerPhotoCount}장` : '없음'}</strong>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-field">
                          <span>경기</span>
                          <strong>{selectedSuccessRow.opponentOrFloor || '-'}</strong>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-field">
                          <span>실행 시각</span>
                          <strong>{formatDateTimeLabel(selectedSuccessRow.createdAt)}</strong>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-field">
                          <span>알림 제목</span>
                          <strong>{selectedSuccessRow.alertTitle}</strong>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-field baseball-ticket-bay-app__success-detail-field--span-2">
                          <span>판매글 요약</span>
                          <strong>{selectedSuccessRow.summary}</strong>
                        </div>
                        <div className="baseball-ticket-bay-app__success-detail-field baseball-ticket-bay-app__success-detail-field--span-2">
                          <span>판매자 글</span>
                          <strong>{selectedSuccessRow.sellerPost || '등록된 판매글 설명이 없습니다.'}</strong>
                        </div>
                        {selectedSuccessRow.productRemarks.length ? (
                          <div className="baseball-ticket-bay-app__success-detail-field baseball-ticket-bay-app__success-detail-field--span-2">
                            <span>상품 특이사항</span>
                            <strong>{selectedSuccessRow.productRemarks.join(', ')}</strong>
                          </div>
                        ) : null}
                        {selectedSuccessRow.seatRemarks.length ? (
                          <div className="baseball-ticket-bay-app__success-detail-field baseball-ticket-bay-app__success-detail-field--span-2">
                            <span>좌석 특이사항</span>
                            <strong>{selectedSuccessRow.seatRemarks.join(', ')}</strong>
                          </div>
                        ) : null}
                        <div className="baseball-ticket-bay-app__success-detail-field baseball-ticket-bay-app__success-detail-field--span-2">
                          <span>판매글 링크</span>
                          <strong>
                            <a href={selectedSuccessRow.saleUrl} target="_blank" rel="noreferrer">
                              티켓베이 판매글 열기
                            </a>
                          </strong>
                        </div>
                      </div>
                      {selectedSuccessRow.seatMapImageUrl ? (
                        <div className="baseball-ticket-bay-app__success-media">
                          <span>좌석 배치도</span>
                          <img src={selectedSuccessRow.seatMapImageUrl} alt={`${selectedSuccessRow.ticketTitle} 좌석 배치도`} />
                        </div>
                      ) : null}
                      {selectedSuccessRow.photoUrls.length ? (
                        <div className="baseball-ticket-bay-app__success-media">
                          <span>판매자 공유 사진</span>
                          <div className="baseball-ticket-bay-app__success-photo-grid">
                            {selectedSuccessRow.photoUrls.map((photoUrl) => (
                              <a key={photoUrl} href={photoUrl} target="_blank" rel="noreferrer">
                                <img src={photoUrl} alt={`${selectedSuccessRow.ticketTitle} 판매자 사진`} />
                              </a>
                            ))}
                          </div>
                        </div>
                      ) : (
                        <div className="baseball-ticket-bay-app__success-media baseball-ticket-bay-app__success-media--empty">
                          <span>판매자 공유 사진</span>
                          <strong>직관샷이 없어도 조건에 맞으면 성공으로 수집합니다.</strong>
                        </div>
                      )}
                      </article>
                    ) : null}
                  </div>
                </div>
              ) : (
                <div className="baseball-ticket-bay-app__empty">성공 로그가 아직 없습니다.</div>
              )}
            </div>
          </section>
        ) : null}
        {!isSuccessPanel ? (
          <>
        <section
          className={`baseball-ticket-bay-app__editor${activePanel === 'editor' ? ' baseball-ticket-bay-app__editor--active' : ''}`}
          aria-label="알림 조건 편집"
        >
          <div className="baseball-ticket-bay-app__section-tabs" role="tablist" aria-label="화면 전환">
            <button
              type="button"
              className={`baseball-ticket-bay-app__section-tab${activePanel === 'list' ? ' is-active' : ''}`}
              aria-label="알림 목록 화면"
              title="알림 목록"
              onClick={() => setActivePanel('list')}
            >
              <HistoryOutlined />
              <span>목록</span>
            </button>
            <button
              type="button"
              className={`baseball-ticket-bay-app__section-tab${activePanel === 'editor' ? ' is-active' : ''}`}
              aria-label="새 알림 화면"
              title="새 알림"
              onClick={() => setActivePanel('editor')}
            >
              <PlusOutlined />
              <span>새 알림</span>
            </button>
            <button
              type="button"
              className={`baseball-ticket-bay-app__section-tab${isSuccessPanel ? ' is-active' : ''}`}
              aria-label="성공 내역 화면"
              title="성공 내역"
              onClick={openSuccessList}
            >
              <AppstoreOutlined />
              <span>성공</span>
            </button>
          </div>
          <div className="baseball-ticket-bay-app__step-nav" role="tablist" aria-label="입력 단계">
            {editorSteps.map((step, index) => (
              <button
                key={step.key}
                type="button"
                className={`baseball-ticket-bay-app__step-chip${editorStep === step.key ? ' is-active' : ''}`}
                onClick={() => setEditorStep(step.key)}
              >
                <span className="baseball-ticket-bay-app__step-chip-title">
                  {index + 1}. {step.label}
                </span>
                <small className="baseball-ticket-bay-app__step-chip-summary">{step.description}</small>
              </button>
            ))}
          </div>
          <div className="baseball-ticket-bay-app__editor-scroll">
            {editorStep === 'basic' ? (
              <div className="baseball-ticket-bay-app__grid">
                <label className="baseball-ticket-bay-app__field baseball-ticket-bay-app__field--span-2">
                  <span>알림 제목</span>
                  <Input
                    className="baseball-ticket-bay-app__ant-input"
                    placeholder="예: 잠실 1루 통로 2연석"
                    value={draft.title}
                    onChange={(event) => handleDraftChange('title', event.target.value)}
                  />
                </label>
                <label className="baseball-ticket-bay-app__field">
                  <span>경기 날짜</span>
                  <input
                    className="baseball-ticket-bay-app__input"
                    type="date"
                    value={draft.eventDate}
                    onChange={(event) => handleDraftChange('eventDate', event.target.value)}
                  />
                </label>
                <label className="baseball-ticket-bay-app__field">
                  <span>팀</span>
                  <Select value={draft.team} options={TEAM_OPTIONS} onChange={(value) => handleDraftChange('team', value)} />
                </label>
                <label className="baseball-ticket-bay-app__field">
                  <span>같은상품 알림</span>
                  <Select
                    value={draft.sameProductAlertEnabled ? 'enabled' : 'disabled'}
                    options={[
                      { label: '알림 받기', value: 'enabled' },
                      { label: '제외', value: 'disabled' },
                    ]}
                    onChange={(value) => handleDraftChange('sameProductAlertEnabled', value === 'enabled')}
                  />
                </label>
              </div>
            ) : null}

            {editorStep === 'seat' ? (
              <div className="baseball-ticket-bay-app__grid">
                <label className="baseball-ticket-bay-app__field">
                  <span>구역</span>
                  <Select value={draft.zone} options={ZONE_OPTIONS} onChange={(value) => handleDraftChange('zone', value)} />
                </label>
                <label className="baseball-ticket-bay-app__field">
                  <span>통로측</span>
                  <Select value={draft.aisleSide} options={AISLE_SIDE_OPTIONS} onChange={(value) => handleDraftChange('aisleSide', value)} />
                </label>
                <label className="baseball-ticket-bay-app__field baseball-ticket-bay-app__field--span-2">
                  <span>1루·3루·외야 방향</span>
                  <Select
                    mode="multiple"
                    allowClear
                    placeholder="복수 선택 가능, 비워두면 전체"
                    value={draft.seatDirections}
                    options={SEAT_DIRECTION_OPTIONS}
                    onChange={(value) => handleDraftChange('seatDirections', value)}
                  />
                </label>
                <label className="baseball-ticket-bay-app__field">
                  <span>매당 최대가</span>
                  <InputNumber
                    min={0}
                    step={1000}
                    value={draft.maxPrice}
                    placeholder="이하"
                    style={{ width: '100%' }}
                    formatter={(value) => (value == null || value === '' ? '' : `${value}`)}
                    parser={(value) => Number(String(value ?? '').replaceAll(',', ''))}
                    onChange={(value) => handleDraftChange('maxPrice', typeof value === 'number' ? value : null)}
                  />
                </label>
                <label className="baseball-ticket-bay-app__field">
                  <span>연석</span>
                  <InputNumber
                    min={1}
                    max={8}
                    value={draft.seatCount}
                    style={{ width: '100%' }}
                    onChange={(value) => handleDraftChange('seatCount', typeof value === 'number' ? value : 1)}
                  />
                </label>
              </div>
            ) : null}

            {editorStep === 'schedule' ? (
              <div className="baseball-ticket-bay-app__editor-sections">
                <div className="baseball-ticket-bay-app__grid">
                  <label className="baseball-ticket-bay-app__field">
                    <span>배치 주기(분)</span>
                    <InputNumber
                      min={1}
                      max={120}
                      value={draft.batchIntervalMinutes}
                      style={{ width: '100%' }}
                      onChange={(value) => handleDraftChange('batchIntervalMinutes', typeof value === 'number' ? value : 3)}
                    />
                  </label>
                  <label className="baseball-ticket-bay-app__field">
                    <span>같은상품 알림 방식</span>
                    <Select
                      value={draft.sameProductNotifyOnce ? 'once' : 'repeat'}
                      disabled={!draft.sameProductAlertEnabled}
                      options={[
                        { label: '한번만 알림', value: 'once' },
                        { label: '계속 알림', value: 'repeat' },
                      ]}
                      onChange={(value) => handleDraftChange('sameProductNotifyOnce', value === 'once')}
                    />
                  </label>
                </div>
                <div className="baseball-ticket-bay-app__windows">
                  <div className="baseball-ticket-bay-app__windows-header">
                    <span>알림 안받을 시간</span>
                    <Button
                      type="text"
                      icon={<PlusOutlined />}
                      aria-label="안받을 시간 추가"
                      title="안받을 시간 추가"
                      onClick={handleAddTimeWindow}
                    >
                      추가
                    </Button>
                  </div>
                  <div className="baseball-ticket-bay-app__window-list">
                    {draft.timeWindows.map((timeWindow) => (
                      <div key={timeWindow.id} className="baseball-ticket-bay-app__window-row">
                        <input
                          className="baseball-ticket-bay-app__input baseball-ticket-bay-app__input--time"
                          type="time"
                          value={timeWindow.start}
                          onChange={(event) => handleTimeWindowChange(timeWindow.id, 'start', event.target.value)}
                        />
                        <span className="baseball-ticket-bay-app__window-divider">~</span>
                        <input
                          className="baseball-ticket-bay-app__input baseball-ticket-bay-app__input--time"
                          type="time"
                          value={timeWindow.end}
                          onChange={(event) => handleTimeWindowChange(timeWindow.id, 'end', event.target.value)}
                        />
                        <Button
                          type="text"
                          danger
                          icon={<DeleteOutlined />}
                          disabled={draft.timeWindows.length <= 1}
                          onClick={() => handleRemoveTimeWindow(timeWindow.id)}
                        />
                      </div>
                    ))}
                  </div>
                </div>
              </div>
            ) : null}
          </div>

          <div className="baseball-ticket-bay-app__editor-footer">
            <div className={`baseball-ticket-bay-app__footer-actions baseball-ticket-bay-app__footer-actions--count-${footerActions.length}`}>
              {footerActions}
            </div>
          </div>
        </section>

        <section
          className={`baseball-ticket-bay-app__list${activePanel === 'list' ? ' baseball-ticket-bay-app__list--active' : ''}`}
          aria-label="알림 목록"
        >
          <div className="baseball-ticket-bay-app__section-tabs" role="tablist" aria-label="화면 전환">
            <button
              type="button"
              className={`baseball-ticket-bay-app__section-tab${activePanel === 'list' ? ' is-active' : ''}`}
              aria-label="알림 목록 화면"
              title="알림 목록"
              onClick={() => setActivePanel('list')}
            >
              <HistoryOutlined />
              <span>목록</span>
            </button>
            <button
              type="button"
              className={`baseball-ticket-bay-app__section-tab${activePanel === 'editor' ? ' is-active' : ''}`}
              aria-label="새 알림 화면"
              title="새 알림"
              onClick={() => setActivePanel('editor')}
            >
              <PlusOutlined />
              <span>새 알림</span>
            </button>
            <button
              type="button"
              className={`baseball-ticket-bay-app__section-tab${isSuccessPanel ? ' is-active' : ''}`}
              aria-label="성공 내역 화면"
              title="성공 내역"
              onClick={openSuccessList}
            >
              <AppstoreOutlined />
              <span>성공</span>
            </button>
          </div>
          <div className="baseball-ticket-bay-app__list-header">
            <strong>목록</strong>
            <div className="baseball-ticket-bay-app__list-header-actions">
              <Button type="text" size="small" icon={<AppstoreOutlined />} aria-label="성공 내역 열기" title="성공 내역" onClick={openSuccessList}>
                성공 내역
              </Button>
              <Button
                type="text"
                size="small"
                icon={<HistoryOutlined />}
                aria-label="전체 로그 열기"
                title="전체 로그"
                onClick={() => setIsLogDrawerOpen(true)}
              >
                전체 로그
              </Button>
              <span>{alerts.length}건</span>
            </div>
          </div>
          <div className="baseball-ticket-bay-app__items">
            {alerts.length ? (
              alerts.map((item) => {
                const isOwnedByCurrentClient = canManageByClientId(item.clientId);

                return (
                <article
                  key={item.id}
                  className={`baseball-ticket-bay-app__item${item.active ? '' : ' baseball-ticket-bay-app__item--paused'}${!isOwnedByCurrentClient ? ' baseball-ticket-bay-app__item--readonly' : ''}`}
                >
                  <div className="baseball-ticket-bay-app__item-top">
                    <div className="baseball-ticket-bay-app__item-heading">
                      <strong>{item.title}</strong>
                      <div className="baseball-ticket-bay-app__item-date">{formatDateLabel(item.eventDate)}</div>
                      <div className="baseball-ticket-bay-app__item-tags">
                        {isViewingAllClients || isSharedTokenScope ? (
                          <Tag bordered={false} color={isOwnedByCurrentClient ? 'blue' : 'default'}>
                            {formatOwnershipLabel(item.ownerType, item.ownerId, item.clientId, item.ownerLabel)}
                          </Tag>
                        ) : null}
                        <Tag bordered={false} color={item.active ? 'green' : 'default'}>
                          {item.active ? '실행중' : '중지'}
                        </Tag>
                        <Tag bordered={false}>{item.team}</Tag>
                        <Tag bordered={false}>{item.zone}</Tag>
                        <Tag bordered={false} color={item.sameProductAlertEnabled ? 'blue' : 'default'}>
                          {item.sameProductAlertEnabled ? (item.sameProductNotifyOnce ? '같은상품 1회' : '같은상품 반복') : '같은상품 제외'}
                        </Tag>
                        {item.seatDirections.length ? (
                          item.seatDirections.map((direction) => (
                            <Tag key={`${item.id}-${direction}`} bordered={false}>
                              {direction}
                            </Tag>
                          ))
                        ) : (
                          <Tag bordered={false}>전체 방향</Tag>
                        )}
                      </div>
                    </div>
                    <div className="baseball-ticket-bay-app__item-actions">
                      <Button type="text" icon={<EditOutlined />} disabled={!isOwnedByCurrentClient} onClick={() => handleEditAlert(item.id)}>
                        수정
                      </Button>
                      <Button
                        type="text"
                        icon={<SendOutlined />}
                        disabled={!isOwnedByCurrentClient}
                        loading={isImmediateRunPending}
                        onClick={() => void runAlertNow(item, 'run', { openLogsOnComplete: true })}
                      />
                      <Button
                        type="text"
                        icon={<HistoryOutlined />}
                        onClick={() => {
                          setSelectedLogAlertId(item.id);
                          setIsLogDrawerOpen(true);
                        }}
                      />
                      <Button
                        type="text"
                        icon={item.active ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
                        disabled={!isOwnedByCurrentClient}
                        onClick={() => handleToggleAlert(item.id)}
                      />
                      <Button type="text" danger icon={<DeleteOutlined />} disabled={!isOwnedByCurrentClient} onClick={() => handleDeleteAlert(item.id)} />
                    </div>
                  </div>
                  <div className="baseball-ticket-bay-app__item-summary">{buildAlertSummary(item)}</div>
                  <div className="baseball-ticket-bay-app__item-schedule">{formatNextBatchScheduleLabel(item)}</div>
                  {latestLogByAlertId.get(item.id) ? (
                    <div className="baseball-ticket-bay-app__item-log">
                      <Tag bordered={false} color={resolveLogTagColor(latestLogByAlertId.get(item.id)?.status ?? 'info')}>
                        {resolveLogStatusLabel(latestLogByAlertId.get(item.id)?.status ?? 'info')}
                      </Tag>
                      <span>{latestLogByAlertId.get(item.id)?.message}</span>
                    </div>
                  ) : null}
                </article>
              )})
            ) : (
              <div className="baseball-ticket-bay-app__empty">저장된 알림이 없습니다.</div>
            )}
          </div>
        </section>
          </>
        ) : null}
      </div>

      <Drawer
        title="실행 로그"
        placement="right"
        width={360}
        open={isLogDrawerOpen}
        onClose={() => {
          setIsLogDrawerOpen(false);
          setSelectedLogAlertId(null);
        }}
        className="baseball-ticket-bay-app__logs-drawer"
      >
        <div className="baseball-ticket-bay-app__logs">
          <div className="baseball-ticket-bay-app__logs-toolbar">
            <Button type="primary" size="small" icon={<HistoryOutlined />} aria-label="전체 로그 보기" title="전체 로그" onClick={() => setSelectedLogAlertId(null)}>
              전체 로그
            </Button>
            <Button size="small" icon={<AppstoreOutlined />} aria-label="성공 내역 화면 열기" title="성공 내역" onClick={openSuccessList}>
              성공 내역
            </Button>
            {selectedLogAlertId ? (
              <Button size="small" icon={<CloseOutlined />} aria-label="로그 필터 해제" title="필터 해제" onClick={() => setSelectedLogAlertId(null)}>
                필터 해제
              </Button>
            ) : null}
          </div>
          <div className="baseball-ticket-bay-app__logs-list">
            {visibleLogs.length ? (
              visibleLogs.map((item) => (
                <article key={item.id} className="baseball-ticket-bay-app__log-item">
                  <div className="baseball-ticket-bay-app__log-top">
                    <div className="baseball-ticket-bay-app__log-heading">
                      <strong>{item.alertTitle}</strong>
                      {isViewingAllClients || isSharedTokenScope ? (
                        <span className="baseball-ticket-bay-app__log-client">
                          {formatOwnershipLabel(item.ownerType, item.ownerId, item.clientId, item.ownerLabel)}
                        </span>
                      ) : null}
                    </div>
                    <div className="baseball-ticket-bay-app__log-top-actions">
                      <Tag bordered={false} color={resolveLogTagColor(item.status)}>
                        {resolveLogStatusLabel(item.status)}
                      </Tag>
                      {item.status === 'success' ? (
                        <Button
                          type="text"
                          danger
                          size="small"
                          icon={<DeleteOutlined />}
                          aria-label="성공 로그 삭제"
                          title="성공 로그 삭제"
                          disabled={!canManageByClientId(item.clientId)}
                          onClick={() => void handleDeleteSuccessLog(item.id)}
                        />
                      ) : null}
                    </div>
                  </div>
                  <div className="baseball-ticket-bay-app__log-message">{item.message}</div>
                  <div className="baseball-ticket-bay-app__log-detail">{item.detail}</div>
                  <div className="baseball-ticket-bay-app__log-time">{formatDateTimeLabel(item.createdAt)}</div>
                </article>
              ))
            ) : (
              <div className="baseball-ticket-bay-app__empty">표시할 로그가 없습니다.</div>
            )}
          </div>
        </div>
      </Drawer>

      <Drawer
        title="앱 설정"
        placement="right"
        width={320}
        open={isSettingsOpen}
        onClose={() => setIsSettingsOpen(false)}
        className="baseball-ticket-bay-app__settings-drawer"
      >
        <div className="baseball-ticket-bay-app__settings">
          <div className="baseball-ticket-bay-app__settings-block">
            <span>{isSharedTokenScope ? '공유 토큰' : '접속 기기'}</span>
            <strong>{isSharedTokenScope ? formatScopeIdentifierLabel(scopeOwnerId || effectiveShareToken || clientId) : (clientId || '-')}</strong>
          </div>
          <div className="baseball-ticket-bay-app__settings-block">
            <div className="baseball-ticket-bay-app__settings-block-header">
              <span>푸시 등록 상태</span>
              <Button
                type="text"
                size="small"
                icon={<ReloadOutlined />}
                aria-label="푸시 등록 상태 새로고침"
                title="푸시 등록 상태 새로고침"
                loading={pushStatusLoading}
                onClick={() => void refreshPushStatus({ syncExistingRegistration: true })}
              >
                상태 새로고침
              </Button>
            </div>
            <div className="baseball-ticket-bay-app__settings-helper">
              앱을 닫았다 다시 열어도 브라우저 구독이 남아 있으면 서버 등록을 자동으로 다시 맞춥니다.
            </div>
            <div className="baseball-ticket-bay-app__status-table-wrap">
              <table className="baseball-ticket-bay-app__status-table">
                <thead>
                  <tr>
                    <th scope="col">항목</th>
                    <th scope="col">상태</th>
                    <th scope="col">세부</th>
                  </tr>
                </thead>
                <tbody>
                  {pushStatusRows.map((row) => (
                    <tr key={row.key}>
                      <td>{row.label}</td>
                      <td>{row.value}</td>
                      <td>{row.detail || '-'}</td>
                    </tr>
                  ))}
                  {pushStatusRows.length === 0 ? (
                    <tr>
                      <td colSpan={3}>{pushStatusLoading ? '등록 상태를 확인하는 중입니다.' : '등록 상태 정보가 아직 없습니다.'}</td>
                    </tr>
                  ) : null}
                </tbody>
              </table>
            </div>
            <div className="baseball-ticket-bay-app__status-cards" aria-label="푸시 등록 상태 카드">
              {pushStatusRows.map((row) => (
                <article key={`${row.key}-card`} className="baseball-ticket-bay-app__status-card">
                  <div className="baseball-ticket-bay-app__status-card-top">
                    <span>{row.label}</span>
                    <strong>{row.value}</strong>
                  </div>
                  <div className="baseball-ticket-bay-app__status-card-detail">{row.detail || '-'}</div>
                </article>
              ))}
              {pushStatusRows.length === 0 ? (
                <div className="baseball-ticket-bay-app__status-card baseball-ticket-bay-app__status-card--empty">
                  {pushStatusLoading ? '등록 상태를 확인하는 중입니다.' : '등록 상태 정보가 아직 없습니다.'}
                </div>
              ) : null}
            </div>
            {pushTestDiagnostic ? (
              <div className="baseball-ticket-bay-app__settings-block baseball-ticket-bay-app__settings-block--nested">
                <div className="baseball-ticket-bay-app__settings-block-header">
                  <span>최근 테스트 발송 진단</span>
                  <strong>{pushTestDiagnostic.status === 'success' ? '정상 응답' : '확인 필요'}</strong>
                </div>
                <div className="baseball-ticket-bay-app__status-cards" aria-label="최근 테스트 발송 진단 카드">
                  {pushTestDiagnostic.rows.map((row) => (
                    <article key={`${row.key}-diagnostic`} className="baseball-ticket-bay-app__status-card">
                      <div className="baseball-ticket-bay-app__status-card-top">
                        <span>{row.label}</span>
                        <strong>{row.value}</strong>
                      </div>
                      <div className="baseball-ticket-bay-app__status-card-detail">{row.detail || '-'}</div>
                    </article>
                  ))}
                </div>
              </div>
            ) : null}
          </div>
          <div className="baseball-ticket-bay-app__settings-actions">
            {isEmbeddedLaunch ? (
              <Button icon={<CloseOutlined />} aria-label="앱 닫기" title="닫기" onClick={onBack}>
                닫기
              </Button>
            ) : null}
            <Button icon={<ReloadOutlined />} aria-label="알림 목록 새로고침" title="새로고침" onClick={handleReload}>
              새로고침
            </Button>
            <Button icon={<BellOutlined />} aria-label="웹 푸시 등록" title="푸시 등록" loading={isPushPending} onClick={handleEnablePush}>
              푸시 등록
            </Button>
            <Button
              type="primary"
              icon={<SendOutlined />}
              aria-label="테스트 푸시 전송"
              title="테스트 푸시"
              loading={isTestPushPending}
              onClick={handleSendTestPush}
            >
              테스트 푸시
            </Button>
          </div>
        </div>
      </Drawer>
    </div>
  );
}
