import { CopyOutlined, PlusOutlined } from '@ant-design/icons';
import {
  Alert,
  Button,
  Card,
  Checkbox,
  Empty,
  Flex,
  Input,
  InputNumber,
  List,
  Segmented,
  Select,
  Space,
  Tabs,
  Tag,
  Typography,
  Modal,
  message,
} from 'antd';
import { memo, useEffect, useMemo, useState, type Dispatch, type SetStateAction } from 'react';
import { useNavigate } from 'react-router-dom';
import {
  buildAutomationTypeOptions,
  useAutomationTypeRegistry,
} from '../../app/main/automationTypeAccess';
import {
  buildAutomationContextOptions,
  resolveDefaultAutomationContextIds,
  useAutomationContextRegistry,
} from '../../app/main/automationContextAccess';
import { buildPlansPath } from '../../app/main/routes';
import { useTokenAccess } from '../../app/main/tokenAccess';
import { confirmWithKeyboard } from '../../app/main/modalKeyboard';
import './planBoard.css';
import './planSchedule.css';
import { maskNotePreviewByWord } from './noteMasking';
import { PlanListDetailLayout } from './PlanListDetailLayout';
import {
  createPlanScheduledTask,
  deletePlanScheduledTask,
  fetchPlanScheduledTasks,
  setupPlanBoard,
  type PlanScheduleExecutionMode,
  updatePlanScheduledTask,
  type PlanScheduleMode,
  type PlanScheduleRepeatUnit,
  type PlanScheduledTask,
  type PlanScheduledTaskDateRange,
  type PlanScheduledTaskDraft,
  type PlanScheduledTaskSaveResult,
  type PlanScheduledTaskTimeWindow,
} from './api';

const { Paragraph, Text, Title } = Typography;
const { TextArea } = Input;
const FUNCTION_CHECK_OPTIONS = ['완료', '오동작'];
const EXECUTION_MODE_OPTIONS: { label: string; value: PlanScheduleExecutionMode }[] = [
  { label: 'Codex 직접 처리', value: 'codex' },
  { label: '별도 서비스 관리', value: 'managed-service' },
];
const SCHEDULE_MODE_TAB_ITEMS: { key: PlanScheduleMode; label: string }[] = [
  { key: 'interval', label: '반복 주기' },
  { key: 'daily', label: '매일 시간' },
];
const REPEAT_UNIT_OPTIONS: { label: string; value: PlanScheduleRepeatUnit }[] = [
  { label: '초', value: 'second' },
  { label: '분', value: 'minute' },
  { label: '시간', value: 'hour' },
  { label: '일', value: 'day' },
  { label: '주', value: 'week' },
  { label: '월', value: 'month' },
];
const REPEAT_UNIT_LABELS: Record<PlanScheduleRepeatUnit, string> = {
  second: '초',
  minute: '분',
  hour: '시간',
  day: '일',
  week: '주',
  month: '개월',
};
const REPEAT_PRESET_OPTIONS: { label: string; value: number; unit: PlanScheduleRepeatUnit }[] = [
  { label: '10초', value: 10, unit: 'second' },
  { label: '30초', value: 30, unit: 'second' },
  { label: '1분', value: 1, unit: 'minute' },
  { label: '30분', value: 30, unit: 'minute' },
  { label: '1시간', value: 1, unit: 'hour' },
  { label: '6시간', value: 6, unit: 'hour' },
  { label: '매일 1회', value: 1, unit: 'day' },
  { label: '매주 1회', value: 1, unit: 'week' },
  { label: '매월 1회', value: 1, unit: 'month' },
];
const HOUR_OPTIONS = Array.from({ length: 24 }, (_, hour) => ({
  label: `${String(hour).padStart(2, '0')}시`,
  value: String(hour).padStart(2, '0'),
}));
const MINUTE_OPTIONS = Array.from({ length: 60 }, (_, minute) => ({
  label: `${String(minute).padStart(2, '0')}분`,
  value: String(minute).padStart(2, '0'),
}));
const DEFAULT_DAILY_RUN_TIME = '09:00';
const KST_TIME_ZONE = 'Asia/Seoul';
const DAY_MS = 24 * 60 * 60 * 1000;
const TIME_OF_DAY_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/;
const DATE_KEY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const WEEKDAY_OPTIONS = [
  { label: '월', value: 1 },
  { label: '화', value: 2 },
  { label: '수', value: 3 },
  { label: '목', value: 4 },
  { label: '금', value: 5 },
  { label: '토', value: 6 },
  { label: '일', value: 0 },
] as const;
const WEEKDAY_LABELS: Record<number, string> = Object.fromEntries(
  WEEKDAY_OPTIONS.map((item) => [item.value, item.label] as const),
);

function normalizeScheduleWeekdays(value: number[] | null | undefined) {
  if (!Array.isArray(value)) {
    return [];
  }

  return Array.from(
    new Set(value.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0 && item <= 6)),
  ).sort((left, right) => left - right);
}

function buildScheduleWeekdaysSummary(value: number[] | null | undefined) {
  const weekdays = normalizeScheduleWeekdays(value);

  if (!weekdays.length) {
    return '매일';
  }

  return weekdays.map((weekday) => WEEKDAY_LABELS[weekday] ?? String(weekday)).join(', ');
}

function createEmptyScheduleTimeWindow(): PlanScheduledTaskTimeWindow {
  return {
    startTime: null,
    endTime: null,
  };
}
function getRepeatIntervalSeconds(value: number, unit: PlanScheduleRepeatUnit) {
  const normalizedValue = Math.max(1, Math.round(Number(value) || 1));

  if (unit === 'second') {
    return normalizedValue;
  }

  if (unit === 'day') {
    return normalizedValue * 24 * 60 * 60;
  }

  if (unit === 'week') {
    return normalizedValue * 7 * 24 * 60 * 60;
  }

  if (unit === 'month') {
    return normalizedValue * 30 * 24 * 60 * 60;
  }

  if (unit === 'hour') {
    return normalizedValue * 60 * 60;
  }

  return normalizedValue * 60;
}

function getRepeatIntervalMinutes(value: number, unit: PlanScheduleRepeatUnit) {
  return Math.max(1, Math.ceil(getRepeatIntervalSeconds(value, unit) / 60));
}

function getRepeatIntervalValueMax(unit: PlanScheduleRepeatUnit) {
  if (unit === 'second') {
    return 86400;
  }

  if (unit === 'month') {
    return 12;
  }

  if (unit === 'week') {
    return 52;
  }

  if (unit === 'day') {
    return 365;
  }

  if (unit === 'hour') {
    return 8760;
  }

  return 525600;
}

function buildScheduleSaveMessage(
  isUpdate: boolean,
  saveResult: PlanScheduledTaskSaveResult,
) {
  const baseMessage = isUpdate ? '스케줄을 수정했습니다.' : '스케줄을 등록했습니다.';

  if (!saveResult.registeredPlan) {
    return baseMessage;
  }

  return `${baseMessage} 자동화도 바로 접수되어 Plan #${saveResult.registeredPlan.id}가 생성됐습니다.`;
}

function normalizeRepeatIntervalValue(value: number, unit: PlanScheduleRepeatUnit) {
  const roundedValue = Math.max(1, Math.round(Number(value) || 1));
  return Math.min(getRepeatIntervalValueMax(unit), roundedValue);
}

function formatRepeatInterval(value: number, unit: PlanScheduleRepeatUnit, fallbackSeconds: number) {
  if (!value || !unit) {
    return `${fallbackSeconds}초마다`;
  }

  return `${value}${REPEAT_UNIT_LABELS[unit]}마다`;
}

function normalizeScheduleMode(value: PlanScheduleMode | null | undefined): PlanScheduleMode {
  return value === 'daily' ? 'daily' : 'interval';
}

function normalizeDailyRunTime(value: string | null | undefined) {
  return typeof value === 'string' && TIME_OF_DAY_PATTERN.test(value) ? value : DEFAULT_DAILY_RUN_TIME;
}

function updateDailyRunTime(value: string, part: 'hour' | 'minute', nextPartValue: string) {
  const [hour, minute] = normalizeDailyRunTime(value).split(':');
  return part === 'hour' ? `${nextPartValue}:${minute}` : `${hour}:${nextPartValue}`;
}

function normalizeOptionalTimeOfDay(value: string | null | undefined) {
  return typeof value === 'string' && TIME_OF_DAY_PATTERN.test(value) ? value : null;
}

function normalizeDateKey(value: string | null | undefined) {
  const trimmedValue = typeof value === 'string' ? value.trim() : '';
  return DATE_KEY_PATTERN.test(trimmedValue) ? trimmedValue : null;
}

function normalizeScheduleDateRanges(value: PlanScheduledTaskDateRange[] | null | undefined) {
  if (!Array.isArray(value)) {
    return [];
  }

  return value
    .map((range) => ({
      startDate: normalizeDateKey(range?.startDate) ?? '',
      endDate: normalizeDateKey(range?.endDate) ?? '',
    }))
    .filter((range) => range.startDate && range.endDate)
    .sort((left, right) =>
      left.startDate === right.startDate
        ? left.endDate.localeCompare(right.endDate)
        : left.startDate.localeCompare(right.startDate),
    )
    .filter((range, index, ranges) => {
      const previousRange = ranges[index - 1];
      return !previousRange || previousRange.startDate !== range.startDate || previousRange.endDate !== range.endDate;
    });
}

function normalizeScheduleTimeWindows(value: PlanScheduledTaskTimeWindow[] | null | undefined) {
  if (!Array.isArray(value)) {
    return [];
  }

  return value
    .map((window) => ({
      startTime: normalizeOptionalTimeOfDay(window?.startTime),
      endTime: normalizeOptionalTimeOfDay(window?.endTime),
    }))
    .filter((window) => window.startTime || window.endTime)
    .sort((left, right) => {
      const leftKey = `${left.startTime ?? ''}:${left.endTime ?? ''}`;
      const rightKey = `${right.startTime ?? ''}:${right.endTime ?? ''}`;
      return leftKey.localeCompare(rightKey);
    })
    .filter((window, index, windows) => {
      const previousWindow = windows[index - 1];
      return !previousWindow
        || previousWindow.startTime !== window.startTime
        || previousWindow.endTime !== window.endTime;
    });
}

function formatSingleRepeatWindowLabel(window: PlanScheduledTaskTimeWindow) {
  if (window.startTime && window.endTime) {
    return `${window.startTime}~${window.endTime}`;
  }

  if (window.startTime) {
    return `${window.startTime} 이후`;
  }

  if (window.endTime) {
    return `${window.endTime} 이전`;
  }

  return '시간 제한 없음';
}

function buildRepeatWindowsSummary(value: PlanScheduledTaskTimeWindow[] | null | undefined) {
  const normalizedWindows = normalizeScheduleTimeWindows(value);

  if (!normalizedWindows.length) {
    return '시간 제한 없음';
  }

  if (normalizedWindows.length === 1) {
    return formatSingleRepeatWindowLabel(normalizedWindows[0]);
  }

  return `${formatSingleRepeatWindowLabel(normalizedWindows[0])} 외 ${normalizedWindows.length - 1}개`;
}

function updateOptionalTimeOfDay(
  value: string | null | undefined,
  part: 'hour' | 'minute',
  nextPartValue: string | undefined,
) {
  if (nextPartValue === undefined) {
    return null;
  }

  const [hour, minute] = (normalizeOptionalTimeOfDay(value) ?? '00:00').split(':');
  return part === 'hour' ? `${nextPartValue}:${minute}` : `${hour}:${nextPartValue}`;
}

function formatScheduleCycle(item: PlanScheduledTask) {
  const scheduleMode = normalizeScheduleMode(item.scheduleMode);

  if (scheduleMode === 'daily') {
    return `매일 ${normalizeDailyRunTime(item.dailyRunTime)} 실행`;
  }

  return `${formatRepeatInterval(item.repeatIntervalValue, item.repeatIntervalUnit, item.repeatIntervalSeconds)} · ${buildRepeatWindowsSummary(item.repeatWindows)}`;
}

function getValidDate(value: string | null | undefined) {
  if (!value) {
    return null;
  }

  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? null : date;
}

function getKstDateTimeParts(value: Date) {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone: KST_TIME_ZONE,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    hour12: false,
    hourCycle: 'h23',
  }).formatToParts(value);
  const partMap = Object.fromEntries(parts.map((part) => [part.type, part.value]));

  return {
    year: Number(partMap.year),
    month: Number(partMap.month),
    day: Number(partMap.day),
    hour: Number(partMap.hour),
    minute: Number(partMap.minute),
  };
}

function getKstDateKey(value: Date) {
  const parts = getKstDateTimeParts(value);
  return `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')}`;
}

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

function createDateFromKstDateKey(dateKey: string, hour: number, minute: number) {
  const [year, month, day] = dateKey.split('-').map((value) => Number(value));
  return createDateFromKstParts(year, month, day, hour, minute);
}

function resolveRangeStartRunAt(
  item: PlanScheduledTask,
  dateRange: PlanScheduledTaskDateRange,
) {
  if (normalizeScheduleMode(item.scheduleMode) === 'daily') {
    const [hour, minute] = normalizeDailyRunTime(item.dailyRunTime).split(':').map((value) => Number(value));
    return createDateFromKstDateKey(dateRange.startDate, hour, minute);
  }

  const startTime = normalizeScheduleTimeWindows(item.repeatWindows)[0]?.startTime ?? normalizeOptionalTimeOfDay(item.repeatWindowStartTime);
  const [hour, minute] = (startTime ?? '00:00').split(':').map((value) => Number(value));
  const rangeStartAt = createDateFromKstDateKey(dateRange.startDate, hour, minute);

  if (item.immediateRunEnabled) {
    return rangeStartAt;
  }

  return new Date(rangeStartAt.getTime() + Math.max(1, item.repeatIntervalSeconds) * 1000);
}

function isWithinScheduleDateRanges(ranges: PlanScheduledTaskDateRange[], now = new Date()) {
  const normalizedRanges = normalizeScheduleDateRanges(ranges);

  if (!normalizedRanges.length) {
    return true;
  }

  const dateKey = getKstDateKey(now);
  return normalizedRanges.some((range) => range.startDate <= dateKey && dateKey <= range.endDate);
}

function resolveUpcomingScheduleRange(
  ranges: PlanScheduledTaskDateRange[],
  now = new Date(),
) {
  const normalizedRanges = normalizeScheduleDateRanges(ranges);
  const dateKey = getKstDateKey(now);

  if (!dateKey) {
    return null;
  }

  return normalizedRanges.find((range) => range.startDate > dateKey) ?? null;
}

function resolveNextPlanScheduleRunAt(item: PlanScheduledTask, now = new Date()) {
  if (!item.enabled) {
    return null;
  }

  const scheduleDateRanges = normalizeScheduleDateRanges(item.scheduleDateRanges);

  if (scheduleDateRanges.length && !isWithinScheduleDateRanges(scheduleDateRanges, now)) {
    const upcomingRange = resolveUpcomingScheduleRange(scheduleDateRanges, now);
    return upcomingRange ? resolveRangeStartRunAt(item, upcomingRange) : null;
  }

  const lastRegisteredAt = getValidDate(item.lastRegisteredAt);

  if (!lastRegisteredAt && item.immediateRunEnabled) {
    return now;
  }

  if (normalizeScheduleMode(item.scheduleMode) === 'daily') {
    const [hour, minute] = normalizeDailyRunTime(item.dailyRunTime).split(':').map((value) => Number(value));
    const nowParts = getKstDateTimeParts(now);
    const scheduledToday = createDateFromKstParts(nowParts.year, nowParts.month, nowParts.day, hour, minute);
    const lastRegisteredToday = lastRegisteredAt ? getKstDateKey(lastRegisteredAt) === getKstDateKey(now) : false;

    if (!lastRegisteredToday && scheduledToday.getTime() <= now.getTime()) {
      return now;
    }

    if (!lastRegisteredToday) {
      return scheduledToday;
    }

    return new Date(scheduledToday.getTime() + DAY_MS);
  }

  const baseAt = lastRegisteredAt ?? getValidDate(item.createdAt) ?? now;
  const nextRunAt = new Date(baseAt.getTime() + item.repeatIntervalSeconds * 1000);

  return nextRunAt.getTime() <= now.getTime() ? now : nextRunAt;
}

function createEmptyScheduleDraft(defaultReleaseTarget = 'release'): PlanScheduledTaskDraft {
  return {
    id: null,
    workId: '',
    note: '',
    automationType: 'none',
    automationContextIds: [],
    releaseTarget: defaultReleaseTarget,
    jangsingProcessingRequired: true,
    autoDeployToMain: true,
    suppressWebPush: false,
    enabled: true,
    immediateRunEnabled: true,
    refreshContextSnapshotOnNextRun: false,
    executionMode: 'codex',
    recreateManagedServiceOnNextSave: false,
    scheduleMode: 'interval',
    repeatIntervalValue: 60,
    repeatIntervalUnit: 'minute',
    repeatIntervalSeconds: getRepeatIntervalSeconds(60, 'minute'),
    repeatIntervalMinutes: 60,
    dailyRunTime: DEFAULT_DAILY_RUN_TIME,
    scheduleWeekdays: [],
    scheduleDateRanges: [],
    repeatWindows: [],
    repeatWindowStartTime: null,
    repeatWindowEndTime: null,
  };
}

function toDraft(item: PlanScheduledTask): PlanScheduledTaskDraft {
  const repeatIntervalUnit = item.repeatIntervalUnit ?? 'minute';
  const repeatIntervalValue = normalizeRepeatIntervalValue(
    item.repeatIntervalValue ?? item.repeatIntervalMinutes,
    repeatIntervalUnit,
  );

  return {
    id: item.id,
    workId: item.workId,
    note: item.note,
    automationType: item.automationType,
    automationContextIds: item.automationContextIds ?? [],
    releaseTarget: item.releaseTarget,
    jangsingProcessingRequired: item.jangsingProcessingRequired,
    autoDeployToMain: item.autoDeployToMain,
    suppressWebPush: item.suppressWebPush,
    enabled: item.enabled,
    immediateRunEnabled: item.immediateRunEnabled,
    refreshContextSnapshotOnNextRun: item.refreshContextSnapshotOnNextRun,
    executionMode: item.executionMode ?? 'codex',
    recreateManagedServiceOnNextSave: item.recreateManagedServiceOnNextSave ?? false,
    scheduleMode: normalizeScheduleMode(item.scheduleMode),
    repeatIntervalValue,
    repeatIntervalUnit,
    repeatIntervalSeconds: item.repeatIntervalSeconds ?? getRepeatIntervalSeconds(repeatIntervalValue, repeatIntervalUnit),
    repeatIntervalMinutes: item.repeatIntervalMinutes ?? getRepeatIntervalMinutes(repeatIntervalValue, repeatIntervalUnit),
    dailyRunTime: normalizeDailyRunTime(item.dailyRunTime),
    scheduleWeekdays: normalizeScheduleWeekdays(item.scheduleWeekdays),
    scheduleDateRanges: normalizeScheduleDateRanges(item.scheduleDateRanges),
    repeatWindows: normalizeScheduleTimeWindows(item.repeatWindows),
    repeatWindowStartTime: normalizeOptionalTimeOfDay(item.repeatWindowStartTime),
    repeatWindowEndTime: normalizeOptionalTimeOfDay(item.repeatWindowEndTime),
  };
}

function formatPlanScheduleDateTime(value: string | Date | null | undefined) {
  if (!value) {
    return '미등록';
  }

  return new Date(value).toLocaleString('ko-KR', {
    timeZone: KST_TIME_ZONE,
  });
}

function formatNextPlanScheduleRunAt(item: PlanScheduledTask) {
  if (!item.enabled) {
    return '중지';
  }

  const nextRunAt = resolveNextPlanScheduleRunAt(item);
  return nextRunAt ? formatPlanScheduleDateTime(nextRunAt) : '기간 종료';
}

function validateScheduleDraft(draft: PlanScheduledTaskDraft, items: PlanScheduledTask[]) {
  const messages: string[] = [];
  const workId = draft.workId.trim();
  const note = draft.note.trim();
  const normalizedScheduleWeekdays = normalizeScheduleWeekdays(draft.scheduleWeekdays);
  const normalizedRepeatWindows = normalizeScheduleTimeWindows(draft.repeatWindows);

  if (!workId) {
    messages.push('작업 ID를 입력하세요.');
  } else if (items.some((item) => item.id !== draft.id && item.workId.trim() === workId)) {
    messages.push('같은 작업 ID의 스케줄이 이미 있습니다.');
  }

  if (!note) {
    messages.push('반복 등록할 작업 메모를 입력하세요.');
  }

  if (normalizedScheduleWeekdays.length > 7) {
    messages.push('적용 요일은 최대 7개까지 등록할 수 있습니다.');
  }

  if (draft.scheduleMode === 'interval') {
    if (normalizedRepeatWindows.length > 24) {
      messages.push('적용 시간은 최대 24개까지 등록할 수 있습니다.');
    }
  }

  if (!draft.enabled) {
    messages.push('비활성 스케줄은 자동 등록되지 않습니다.');
  }

  return messages;
}

export function PlanSchedulePage() {
  const { hasAccess } = useTokenAccess();
  const { automationTypes } = useAutomationTypeRegistry();
  const { automationContexts } = useAutomationContextRegistry();
  const [messageApi, contextHolder] = message.useMessage();
  const [modalApi, modalContextHolder] = Modal.useModal();
  const [items, setItems] = useState<PlanScheduledTask[]>([]);
  const [draft, setDraft] = useState(() => createEmptyScheduleDraft());
  const [editorOpen, setEditorOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [saving, setSaving] = useState(false);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const selectedItem = useMemo(
    () => items.find((item) => item.id === draft.id) ?? null,
    [draft.id, items],
  );
  const validationMessages = useMemo(() => validateScheduleDraft(draft, items), [draft, items]);
  const automationContextOptions = useMemo(
    () => buildAutomationContextOptions(automationContexts, draft.automationContextIds),
    [automationContexts, draft.automationContextIds],
  );

  async function loadItems() {
    setLoading(true);
    setErrorMessage(null);

    try {
      const nextItems = await fetchPlanScheduledTasks();
      setItems(nextItems);
      return nextItems;
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '스케줄 목록을 불러오지 못했습니다.');
      return [];
    } finally {
      setLoading(false);
    }
  }

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

  async function handleSetup() {
    if (!hasAccess) {
      messageApi.error('설정 > 토큰 관리에서 권한 토큰을 등록한 사용자만 사용할 수 있습니다.');
      return;
    }

    try {
      await setupPlanBoard();
      messageApi.success('작업 스케줄 테이블을 준비했습니다.');
      await loadItems();
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '테이블 생성에 실패했습니다.');
    }
  }

  async function handleSave() {
    if (!hasAccess) {
      messageApi.error('설정 > 토큰 관리에서 권한 토큰을 등록한 사용자만 사용할 수 있습니다.');
      return;
    }

    const blockingMessages = validationMessages.filter((message) => message !== '비활성 스케줄은 자동 등록되지 않습니다.');

    if (blockingMessages.length) {
      messageApi.warning(blockingMessages[0]);
      return;
    }

    setSaving(true);

    try {
      const repeatIntervalValue = normalizeRepeatIntervalValue(draft.repeatIntervalValue, draft.repeatIntervalUnit);
      const draftToSave = {
        ...draft,
        repeatIntervalValue,
        repeatIntervalSeconds: getRepeatIntervalSeconds(repeatIntervalValue, draft.repeatIntervalUnit),
        repeatIntervalMinutes: getRepeatIntervalMinutes(repeatIntervalValue, draft.repeatIntervalUnit),
        scheduleWeekdays: normalizeScheduleWeekdays(draft.scheduleWeekdays),
        scheduleDateRanges: draft.scheduleMode === 'interval' ? [] : normalizeScheduleDateRanges(draft.scheduleDateRanges),
        repeatWindows: draft.scheduleMode === 'interval' ? normalizeScheduleTimeWindows(draft.repeatWindows) : [],
        repeatWindowStartTime:
          draft.scheduleMode === 'interval' ? normalizeScheduleTimeWindows(draft.repeatWindows)[0]?.startTime ?? null : null,
        repeatWindowEndTime:
          draft.scheduleMode === 'interval' ? normalizeScheduleTimeWindows(draft.repeatWindows)[0]?.endTime ?? null : null,
      };
      const saveResult = draft.id
        ? await updatePlanScheduledTask(draftToSave)
        : await createPlanScheduledTask(draftToSave);
      const nextItems = await loadItems();
      const latestSavedItem = nextItems.find((item) => item.id === saveResult.item.id) ?? saveResult.item;
      messageApi.success(buildScheduleSaveMessage(Boolean(draft.id), saveResult));
      setDraft(toDraft(latestSavedItem));
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '스케줄 저장에 실패했습니다.');
    } finally {
      setSaving(false);
    }
  }

  async function handleDelete() {
    if (!draft.id) {
      return;
    }

    if (!hasAccess) {
      messageApi.error('설정 > 토큰 관리에서 권한 토큰을 등록한 사용자만 사용할 수 있습니다.');
      return;
    }

    const confirmed = await confirmWithKeyboard(modalApi, {
      title: '선택한 스케줄을 삭제할까요?',
      okText: '삭제',
      cancelText: '취소',
      okButtonProps: { danger: true },
    });

    if (!confirmed) {
      return;
    }

    setSaving(true);

    try {
      await deletePlanScheduledTask(draft.id);
      messageApi.success('스케줄을 삭제했습니다.');
      setDraft({
        ...createEmptyScheduleDraft(draft.releaseTarget),
        automationContextIds: resolveDefaultAutomationContextIds(automationContexts),
      });
      setEditorOpen(false);
      await loadItems();
    } catch (error) {
      messageApi.error(error instanceof Error ? error.message : '스케줄 삭제에 실패했습니다.');
    } finally {
      setSaving(false);
    }
  }

  async function handleCopyText(text: string) {
    try {
      await navigator.clipboard.writeText(text);
      messageApi.success('복사했습니다.');
    } catch {
      messageApi.error('복사에 실패했습니다.');
    }
  }

  function handleCreateNew() {
    setDraft({
      ...createEmptyScheduleDraft(draft.releaseTarget),
      automationContextIds: resolveDefaultAutomationContextIds(automationContexts),
    });
    setEditorOpen(true);
  }

  function handleSelectItem(item: PlanScheduledTask) {
    setDraft(toDraft(item));
    setEditorOpen(true);
  }

  function closeEditor() {
    setEditorOpen(false);
  }

  return (
    <div className="plan-schedule-page">
      {contextHolder}
      {modalContextHolder}
      <Card className="plan-schedule-page__overview" bordered={false}>
        <Flex justify="space-between" align="center" gap={12} wrap>
          <div>
            <Title level={4}>자동화 Schedule</Title>
            <Paragraph className="plan-schedule-page__intro">
              반복 작업 내용을 스케줄로 등록하고 활성 상태와 반복 주기를 관리합니다.
            </Paragraph>
          </div>
          <Space wrap>
            <Button onClick={() => void loadItems()} loading={loading}>
              조회
            </Button>
            <Button onClick={handleCreateNew} disabled={!hasAccess}>
              새 스케줄
            </Button>
            <Button onClick={() => void handleSetup()} disabled={!hasAccess}>
              테이블 생성
            </Button>
          </Space>
        </Flex>
      </Card>

      {!hasAccess ? (
        <Alert
          showIcon
          type="warning"
          className="plan-schedule-page__alert"
          message="권한 토큰이 없어 작업 스케줄은 조회만 사용할 수 있습니다."
          description="설정 > 토큰 관리에서 권한 토큰을 등록하기 전에는 조회 외 버튼과 입력을 사용할 수 없고, 작업 메모는 단어별로 마스킹됩니다."
        />
      ) : null}

      {errorMessage ? (
        <Alert
          showIcon
          type="error"
          className="plan-schedule-page__alert"
          message="스케줄을 사용할 수 없습니다."
          description={errorMessage}
        />
      ) : null}

      <PlanListDetailLayout
        classNamePrefix="plan-schedule-page"
        listTitle="스케줄 목록"
        listExtra={<Text code>{items.length} items</Text>}
        listContent={
          <PlanScheduleList
            activeDraftId={draft.id}
            editorOpen={editorOpen}
            hasAccess={hasAccess}
            items={items}
            loading={loading}
            onSelectItem={handleSelectItem}
          />
        }
        desktopDetailOpen={editorOpen}
        mobileDetailOpen={editorOpen}
        detailTitle={draft.id ? `스케줄 #${draft.id}` : '새 스케줄'}
        detailActions={
          <>
            {draft.id ? (
              <Button danger onClick={() => void handleDelete()} loading={saving} disabled={!hasAccess}>
                삭제
              </Button>
            ) : null}
            <Button type="primary" onClick={() => void handleSave()} loading={saving} disabled={!hasAccess}>
              저장
            </Button>
          </>
        }
        onCloseDetail={closeEditor}
        showDesktopClose
        emptyDetailTitle="스케줄 상세"
        detailContent={
          <PlanScheduleDetail
            automationTypeOptions={buildAutomationTypeOptions(automationTypes, draft.automationType)}
            automationContextOptions={automationContextOptions}
            draft={draft}
            hasAccess={hasAccess}
            selectedItem={selectedItem}
            validationMessages={validationMessages}
            onChangeDraft={setDraft}
            onCopyText={handleCopyText}
          />
        }
      />
    </div>
  );
}

const PlanScheduleList = memo(function PlanScheduleList({
  activeDraftId,
  editorOpen,
  hasAccess,
  items,
  loading,
  onSelectItem,
}: {
  activeDraftId: number | null;
  editorOpen: boolean;
  hasAccess: boolean;
  items: PlanScheduledTask[];
  loading: boolean;
  onSelectItem: (item: PlanScheduledTask) => void;
}) {
  return (
    <List
      className="plan-schedule-page__list"
      loading={loading}
      dataSource={items}
      locale={{ emptyText: <Empty description="등록된 스케줄이 없습니다." /> }}
      renderItem={(item) => (
        <List.Item
          key={item.id}
          className={`plan-schedule-page__list-item${
            editorOpen && activeDraftId === item.id ? ' plan-schedule-page__list-item--active' : ''
          }`}
          onClick={() => onSelectItem(item)}
        >
          <div className="plan-schedule-page__list-body">
            <Flex justify="space-between" align="start" gap={8}>
              <Text strong>{item.workId || `스케줄 #${item.id}`}</Text>
              <Tag color={item.enabled ? 'green' : 'default'}>{item.enabled ? '적용' : '중지'}</Tag>
            </Flex>
            <Paragraph ellipsis={{ rows: 2 }} className="plan-schedule-page__list-note">
              {item.note ? (hasAccess ? item.note : maskNotePreviewByWord(item.note)) : '작업 내용이 없습니다.'}
            </Paragraph>
            <Space wrap size={8}>
              <Tag color={item.executionMode === 'managed-service' ? 'geekblue' : 'default'}>
                {item.executionMode === 'managed-service' ? '외부 서비스 관리형' : 'Codex 직접'}
              </Tag>
              <Tag>{formatScheduleCycle(item)}</Tag>
              <Tag color={item.scheduleWeekdays.length ? 'cyan' : 'default'}>
                {item.scheduleWeekdays.length ? `요일 ${buildScheduleWeekdaysSummary(item.scheduleWeekdays)}` : '매일'}
              </Tag>
              {item.scheduleMode === 'interval' ? (
                <Tag color={item.repeatWindows.length ? 'magenta' : 'default'}>
                  {item.repeatWindows.length ? `시간 ${item.repeatWindows.length}개` : '상시'}
                </Tag>
              ) : null}
              <Tag color="blue">다음 실행 {formatNextPlanScheduleRunAt(item)}</Tag>
              <Tag>{item.immediateRunEnabled ? '즉시실행' : '주기 후 실행'}</Tag>
              {item.refreshContextSnapshotOnNextRun ? <Tag color="purple">다음 실행 시 문서 재정리</Tag> : null}
              {item.executionMode === 'managed-service' && item.managedServiceKey ? (
                <Tag color="cyan">{item.managedServiceKey}</Tag>
              ) : null}
              <Tag>{item.autoDeployToMain ? 'main 자동등록' : 'release만'}</Tag>
              {item.suppressWebPush ? <Tag color="gold">웹푸쉬 끔</Tag> : null}
              <Tag>기능동작확인 {item.jangsingProcessingRequired ? '완료' : '오동작'}</Tag>
            </Space>
            <Flex justify="space-between" align="center" gap={8} wrap style={{ marginTop: 10 }}>
              <Text type="secondary">최근 등록 {formatPlanScheduleDateTime(item.lastRegisteredAt)}</Text>
              <Text type="secondary">수정 {formatPlanScheduleDateTime(item.updatedAt)}</Text>
            </Flex>
          </div>
        </List.Item>
      )}
    />
  );
});

function PlanScheduleDetail({
  automationTypeOptions,
  automationContextOptions,
  draft,
  hasAccess,
  selectedItem,
  validationMessages,
  onChangeDraft,
  onCopyText,
}: {
  automationTypeOptions: Array<{ label: string; value: string }>;
  automationContextOptions: Array<{ label: string; value: string }>;
  draft: PlanScheduledTaskDraft;
  hasAccess: boolean;
  selectedItem: PlanScheduledTask | null;
  validationMessages: string[];
  onChangeDraft: Dispatch<SetStateAction<PlanScheduledTaskDraft>>;
  onCopyText: (text: string) => Promise<void>;
}) {
  const navigate = useNavigate();

  return (
    <div className="plan-schedule-page__detail">
      {selectedItem ? (
        <Alert
          showIcon
          type="info"
          className="plan-schedule-page__alert"
          message="등록 정보"
          description={
            <Space direction="vertical" size={4}>
              <Text>다음 실행: {formatNextPlanScheduleRunAt(selectedItem)}</Text>
              <Text>
                적용 요일: {buildScheduleWeekdaysSummary(selectedItem.scheduleWeekdays)}
              </Text>
              {selectedItem.scheduleMode === 'interval' ? (
                <Text>적용 시간: {buildRepeatWindowsSummary(selectedItem.repeatWindows)}</Text>
              ) : null}
              <Text>문서 재정리 예약: {selectedItem.refreshContextSnapshotOnNextRun ? '다음 실행 1회' : '없음'}</Text>
              <Text>실행 방식: {selectedItem.executionMode === 'managed-service' ? '별도 서비스 관리형' : 'Codex 직접 처리'}</Text>
              {selectedItem.executionMode === 'managed-service' ? (
                <Text>서비스 키: {selectedItem.managedServiceKey ?? `schedule-${selectedItem.id}`}</Text>
              ) : null}
              <Text>최근 작업 등록: {formatPlanScheduleDateTime(selectedItem.lastRegisteredAt)}</Text>
              {selectedItem.executionMode === 'managed-service' ? (
                <Text>서비스 재생성: {selectedItem.managedServiceGeneratedAt ? formatPlanScheduleDateTime(selectedItem.managedServiceGeneratedAt) : '미생성'}</Text>
              ) : null}
              <Text>생성: {formatPlanScheduleDateTime(selectedItem.createdAt)}</Text>
              <Text>수정: {formatPlanScheduleDateTime(selectedItem.updatedAt)}</Text>
            </Space>
          }
        />
      ) : null}

      {validationMessages.length ? (
        <Alert
          showIcon
          type={validationMessages.some((message) => message !== '비활성 스케줄은 자동 등록되지 않습니다.') ? 'warning' : 'info'}
          className="plan-schedule-page__alert"
          message="스케줄 등록 검증"
          description={
            <Space direction="vertical" size={2}>
              {validationMessages.map((message) => (
                <Text key={message}>{message}</Text>
              ))}
            </Space>
          }
        />
      ) : null}

      <div className="plan-schedule-page__form">
        <div>
          <Text strong>작업 ID</Text>
          <Input
            value={draft.workId}
            placeholder="예: 반복-정리"
            disabled={!hasAccess}
            onChange={(event) => onChangeDraft((previous) => ({ ...previous, workId: event.target.value }))}
          />
        </div>
        <div>
          <Flex justify="space-between" align="center" gap={8} wrap>
            <Text strong>작업 메모</Text>
            <Space size={8}>
              <Button size="small" icon={<CopyOutlined />} onClick={() => void onCopyText(draft.note)}>
                복사
              </Button>
            </Space>
          </Flex>
          <div className="plan-schedule-page__notepad-frame">
            <TextArea
              value={hasAccess ? draft.note : maskNotePreviewByWord(draft.note)}
              rows={10}
              placeholder={hasAccess ? '반복 등록할 작업 내용을 입력하세요.' : '권한 토큰 등록 후 편집할 수 있습니다.'}
              className="plan-schedule-page__notepad"
              disabled={!hasAccess}
              onChange={(event) => onChangeDraft((previous) => ({ ...previous, note: event.target.value }))}
            />
          </div>
          {!hasAccess ? <Text type="secondary">조회 화면에서는 작업 메모를 단어별 마스킹으로 표시합니다.</Text> : null}
        </div>
        <div>
          <Text strong>release 브랜치</Text>
          <Input
            value={draft.releaseTarget}
            placeholder="release"
            disabled={!hasAccess}
            onChange={(event) =>
              onChangeDraft((previous) => ({ ...previous, releaseTarget: event.target.value || 'release' }))
            }
          />
        </div>
        <div>
          <Text strong>자동화 처리</Text>
          <Select
            className="plan-schedule-page__select plan-schedule-page__select--automation"
            value={draft.automationType}
            options={automationTypeOptions}
            popupClassName="plan-schedule-page__select-popup"
            getPopupContainer={(triggerNode) => triggerNode.parentElement ?? document.body}
            disabled={!hasAccess}
            onChange={(automationType) =>
              onChangeDraft((previous) => ({
                ...previous,
                automationType,
              }))
            }
          />
        </div>
        <div>
          <Text strong>실행 방식</Text>
          <Segmented
            options={EXECUTION_MODE_OPTIONS}
            value={draft.executionMode}
            disabled={!hasAccess}
            onChange={(value) =>
              onChangeDraft((previous) => ({
                ...previous,
                executionMode: value as PlanScheduleExecutionMode,
                recreateManagedServiceOnNextSave:
                  value === 'managed-service' ? previous.recreateManagedServiceOnNextSave : false,
              }))
            }
          />
          <div style={{ marginTop: 8 }}>
            <Text type="secondary">
              {draft.executionMode === 'managed-service'
                ? `스케줄 PK를 포함한 고정 패키지 경로로 별도 서비스 번들을 관리합니다.`
                : '현재처럼 Codex 기반 자동화 메모 등록 흐름으로 처리합니다.'}
            </Text>
          </div>
        </div>
        <div>
          <Flex justify="space-between" align="center" gap={8} wrap>
            <Text strong>Context</Text>
            <Space size={8} wrap>
              <Text type="secondary">복수 선택 가능, 모두 해제 가능</Text>
              <Button size="small" onClick={() => navigate(buildPlansPath('automation-context'))}>
                Context 관리
              </Button>
            </Space>
          </Flex>
          <Select
            mode="multiple"
            allowClear
            className="plan-schedule-page__select"
            value={draft.automationContextIds}
            options={automationContextOptions}
            popupClassName="plan-schedule-page__select-popup"
            getPopupContainer={(triggerNode) => triggerNode.parentElement ?? document.body}
            disabled={!hasAccess}
            placeholder="선택된 Context 없음"
            onChange={(automationContextIds) => onChangeDraft((previous) => ({ ...previous, automationContextIds }))}
          />
        </div>
        <div>
          <Checkbox
            checked={draft.enabled}
            disabled={!hasAccess}
            onChange={(event) => onChangeDraft((previous) => ({ ...previous, enabled: event.target.checked }))}
          >
            적용
          </Checkbox>
        </div>
        <div>
          <Text strong>반복 주기</Text>
          <Tabs
            size="small"
            items={SCHEDULE_MODE_TAB_ITEMS.map((item) => ({
              ...item,
              disabled: !hasAccess,
            }))}
            activeKey={draft.scheduleMode}
            onChange={(key) => onChangeDraft((previous) => ({ ...previous, scheduleMode: key as PlanScheduleMode }))}
          />
          {draft.scheduleMode === 'daily' ? (
            <>
              <div className="plan-schedule-page__weekday-section">
                <Text strong>적용 요일</Text>
                <Checkbox.Group
                  className="plan-schedule-page__weekday-group"
                  options={WEEKDAY_OPTIONS.map((item) => ({ label: item.label, value: item.value }))}
                  value={normalizeScheduleWeekdays(draft.scheduleWeekdays)}
                  disabled={!hasAccess}
                  onChange={(value) =>
                    onChangeDraft((previous) => ({
                      ...previous,
                      scheduleWeekdays: normalizeScheduleWeekdays(value as number[]),
                    }))
                  }
                />
                <Text type="secondary">선택하지 않으면 매일 실행합니다.</Text>
              </div>
              <Space align="center" wrap>
                <Text>{draft.scheduleWeekdays.length ? `${buildScheduleWeekdaysSummary(draft.scheduleWeekdays)} ` : '매일 '}</Text>
                <Select
                  style={{ width: 96 }}
                  options={HOUR_OPTIONS}
                  value={normalizeDailyRunTime(draft.dailyRunTime).split(':')[0]}
                  popupClassName="plan-schedule-page__select-popup"
                  getPopupContainer={(triggerNode) => triggerNode.parentElement ?? document.body}
                  disabled={!hasAccess}
                  onChange={(value) =>
                    onChangeDraft((previous) => ({
                      ...previous,
                      dailyRunTime: updateDailyRunTime(previous.dailyRunTime, 'hour', value),
                      repeatIntervalValue: 1,
                      repeatIntervalUnit: 'day',
                      repeatIntervalSeconds: getRepeatIntervalSeconds(1, 'day'),
                      repeatIntervalMinutes: getRepeatIntervalMinutes(1, 'day'),
                    }))
                  }
                />
                <Select
                  style={{ width: 96 }}
                  options={MINUTE_OPTIONS}
                  value={normalizeDailyRunTime(draft.dailyRunTime).split(':')[1]}
                  popupClassName="plan-schedule-page__select-popup"
                  getPopupContainer={(triggerNode) => triggerNode.parentElement ?? document.body}
                  disabled={!hasAccess}
                  onChange={(value) =>
                    onChangeDraft((previous) => ({
                      ...previous,
                      dailyRunTime: updateDailyRunTime(previous.dailyRunTime, 'minute', value),
                      repeatIntervalValue: 1,
                      repeatIntervalUnit: 'day',
                      repeatIntervalSeconds: getRepeatIntervalSeconds(1, 'day'),
                      repeatIntervalMinutes: getRepeatIntervalMinutes(1, 'day'),
                    }))
                  }
                />
                <Text type="secondary">에 작업 메모로 등록</Text>
              </Space>
            </>
          ) : (
            <>
              <Space align="center" wrap>
                <InputNumber
                  min={1}
                  max={getRepeatIntervalValueMax(draft.repeatIntervalUnit)}
                  value={draft.repeatIntervalValue}
                  disabled={!hasAccess}
                  onChange={(value) => {
                    const repeatIntervalValue = normalizeRepeatIntervalValue(
                      Number(value) || 1,
                      draft.repeatIntervalUnit,
                    );
                    onChangeDraft((previous) => ({
                      ...previous,
                      repeatIntervalValue,
                      repeatIntervalSeconds: getRepeatIntervalSeconds(repeatIntervalValue, previous.repeatIntervalUnit),
                      repeatIntervalMinutes: getRepeatIntervalMinutes(repeatIntervalValue, previous.repeatIntervalUnit),
                    }));
                  }}
                />
                <Select
                  style={{ width: 96 }}
                  options={REPEAT_UNIT_OPTIONS}
                  value={draft.repeatIntervalUnit}
                  popupClassName="plan-schedule-page__select-popup"
                  getPopupContainer={(triggerNode) => triggerNode.parentElement ?? document.body}
                  disabled={!hasAccess}
                  onChange={(value) =>
                    onChangeDraft((previous) => ({
                      ...previous,
                      repeatIntervalValue: normalizeRepeatIntervalValue(previous.repeatIntervalValue, value),
                      repeatIntervalUnit: value,
                      repeatIntervalSeconds: getRepeatIntervalSeconds(
                        normalizeRepeatIntervalValue(previous.repeatIntervalValue, value),
                        value,
                      ),
                      repeatIntervalMinutes: getRepeatIntervalMinutes(
                        normalizeRepeatIntervalValue(previous.repeatIntervalValue, value),
                        value,
                      ),
                    }))
                  }
                />
                <Text type="secondary">마다 작업 메모로 등록</Text>
              </Space>
              <Space size={6} wrap style={{ marginTop: 8 }}>
                {REPEAT_PRESET_OPTIONS.map((option) => (
                  <Button
                    key={`${option.value}-${option.unit}`}
                    size="small"
                    disabled={!hasAccess}
                    onClick={() =>
                      onChangeDraft((previous) => ({
                        ...previous,
                        scheduleMode: option.unit === 'day' && option.value === 1 ? 'daily' : 'interval',
                        repeatIntervalValue: option.value,
                        repeatIntervalUnit: option.unit,
                        repeatIntervalSeconds: getRepeatIntervalSeconds(option.value, option.unit),
                        repeatIntervalMinutes: getRepeatIntervalMinutes(option.value, option.unit),
                      }))
                    }
                  >
                    {option.label}
                  </Button>
                ))}
              </Space>
              <div className="plan-schedule-page__weekday-section" style={{ marginTop: 12 }}>
                <Flex justify="space-between" align="center" gap={8} wrap>
                  <Text strong>적용 요일</Text>
                  <Text type="secondary">선택하지 않으면 매일 반복합니다.</Text>
                </Flex>
                <Checkbox.Group
                  className="plan-schedule-page__weekday-group"
                  options={WEEKDAY_OPTIONS.map((item) => ({ label: item.label, value: item.value }))}
                  value={normalizeScheduleWeekdays(draft.scheduleWeekdays)}
                  disabled={!hasAccess}
                  onChange={(value) =>
                    onChangeDraft((previous) => ({
                      ...previous,
                      scheduleWeekdays: normalizeScheduleWeekdays(value as number[]),
                    }))
                  }
                />
              </div>
              <div className="plan-schedule-page__time-window-section" style={{ marginTop: 12 }}>
                <Flex justify="space-between" align="center" gap={8} wrap>
                  <Text strong>적용 시간</Text>
                  <Button
                    size="small"
                    icon={<PlusOutlined />}
                    disabled={!hasAccess}
                    onClick={() =>
                      onChangeDraft((previous) => ({
                        ...previous,
                        repeatWindows: [...previous.repeatWindows, createEmptyScheduleTimeWindow()],
                      }))
                    }
                  >
                    시간 추가
                  </Button>
                </Flex>
                {draft.repeatWindows.length ? (
                  <div className="plan-schedule-page__time-window-list">
                    {draft.repeatWindows.map((window, index) => (
                      <div key={`schedule-time-window-${index}`} className="plan-schedule-page__time-window-item">
                        <Text type="secondary">시작</Text>
                        <Select
                          allowClear
                          placeholder="시"
                          style={{ width: 88 }}
                          options={HOUR_OPTIONS}
                          value={normalizeOptionalTimeOfDay(window.startTime)?.split(':')[0]}
                          popupClassName="plan-schedule-page__select-popup"
                          getPopupContainer={(triggerNode) => triggerNode.parentElement ?? document.body}
                          disabled={!hasAccess}
                          onChange={(value) =>
                            onChangeDraft((previous) => ({
                              ...previous,
                              repeatWindows: previous.repeatWindows.map((item, itemIndex) =>
                                itemIndex === index
                                  ? { ...item, startTime: updateOptionalTimeOfDay(item.startTime, 'hour', value) }
                                  : item,
                              ),
                            }))
                          }
                        />
                        <Select
                          allowClear
                          placeholder="분"
                          style={{ width: 88 }}
                          options={MINUTE_OPTIONS}
                          value={normalizeOptionalTimeOfDay(window.startTime)?.split(':')[1]}
                          popupClassName="plan-schedule-page__select-popup"
                          getPopupContainer={(triggerNode) => triggerNode.parentElement ?? document.body}
                          disabled={!hasAccess}
                          onChange={(value) =>
                            onChangeDraft((previous) => ({
                              ...previous,
                              repeatWindows: previous.repeatWindows.map((item, itemIndex) =>
                                itemIndex === index
                                  ? { ...item, startTime: updateOptionalTimeOfDay(item.startTime, 'minute', value) }
                                  : item,
                              ),
                            }))
                          }
                        />
                        <Text type="secondary">종료</Text>
                        <Select
                          allowClear
                          placeholder="시"
                          style={{ width: 88 }}
                          options={HOUR_OPTIONS}
                          value={normalizeOptionalTimeOfDay(window.endTime)?.split(':')[0]}
                          popupClassName="plan-schedule-page__select-popup"
                          getPopupContainer={(triggerNode) => triggerNode.parentElement ?? document.body}
                          disabled={!hasAccess}
                          onChange={(value) =>
                            onChangeDraft((previous) => ({
                              ...previous,
                              repeatWindows: previous.repeatWindows.map((item, itemIndex) =>
                                itemIndex === index
                                  ? { ...item, endTime: updateOptionalTimeOfDay(item.endTime, 'hour', value) }
                                  : item,
                              ),
                            }))
                          }
                        />
                        <Select
                          allowClear
                          placeholder="분"
                          style={{ width: 88 }}
                          options={MINUTE_OPTIONS}
                          value={normalizeOptionalTimeOfDay(window.endTime)?.split(':')[1]}
                          popupClassName="plan-schedule-page__select-popup"
                          getPopupContainer={(triggerNode) => triggerNode.parentElement ?? document.body}
                          disabled={!hasAccess}
                          onChange={(value) =>
                            onChangeDraft((previous) => ({
                              ...previous,
                              repeatWindows: previous.repeatWindows.map((item, itemIndex) =>
                                itemIndex === index
                                  ? { ...item, endTime: updateOptionalTimeOfDay(item.endTime, 'minute', value) }
                                  : item,
                              ),
                            }))
                          }
                        />
                        <Button
                          danger
                          size="small"
                          disabled={!hasAccess}
                          onClick={() =>
                            onChangeDraft((previous) => ({
                              ...previous,
                              repeatWindows: previous.repeatWindows.filter((_, itemIndex) => itemIndex !== index),
                            }))
                          }
                        >
                          삭제
                        </Button>
                      </div>
                    ))}
                  </div>
                ) : (
                  <Text type="secondary">시간을 추가하지 않으면 하루 종일 반복합니다.</Text>
                )}
              </div>
            </>
          )}
        </div>
        <div>
          <Checkbox
            checked={draft.immediateRunEnabled}
            disabled={!hasAccess}
            onChange={(event) =>
              onChangeDraft((previous) => ({ ...previous, immediateRunEnabled: event.target.checked }))
            }
          >
            즉시실행 여부
          </Checkbox>
        </div>
        <div>
          <Checkbox
            checked={draft.refreshContextSnapshotOnNextRun}
            disabled={!hasAccess}
            onChange={(event) =>
              onChangeDraft((previous) => ({
                ...previous,
                refreshContextSnapshotOnNextRun: event.target.checked,
              }))
            }
          >
            다음 실행 시 프로젝트/소스 다시 읽고 문서 재정리
          </Checkbox>
          <div>
            <Text type="secondary">켜두면 다음 자동 실행 1회에 한해 `.auto_codex/schedule/...` 문서를 다시 생성한 뒤 자동으로 꺼집니다.</Text>
          </div>
        </div>
        {draft.executionMode === 'managed-service' ? (
          <div>
            <Checkbox
              checked={draft.recreateManagedServiceOnNextSave}
              disabled={!hasAccess}
              onChange={(event) =>
                onChangeDraft((previous) => ({
                  ...previous,
                  recreateManagedServiceOnNextSave: event.target.checked,
                }))
              }
            >
              저장 시 관리 서비스 패키지 생성 Plan 자동 접수
            </Checkbox>
            <div>
              <Text type="secondary">
                체크하면 저장 직후 서비스 패키지 생성 Plan을 자동 접수하고, 생성된
                {' '}
                <Text code>.auto_codex/schedule/&lt;id&gt;</Text>
                {' '}
                서비스 결과물을 다음 실행부터 사용합니다.
              </Text>
            </div>
            {selectedItem?.managedServiceDirectory ? (
              <div style={{ marginTop: 8 }}>
                <Text code>{selectedItem.managedServiceDirectory}</Text>
              </div>
            ) : null}
          </div>
        ) : null}
        <div>
          <Checkbox
            checked={draft.autoDeployToMain}
            disabled={!hasAccess}
            onChange={(event) => onChangeDraft((previous) => ({ ...previous, autoDeployToMain: event.target.checked }))}
          >
            메인까지 자동등록
          </Checkbox>
        </div>
        <div>
          <Checkbox
            checked={draft.suppressWebPush}
            disabled={!hasAccess}
            onChange={(event) => onChangeDraft((previous) => ({ ...previous, suppressWebPush: event.target.checked }))}
          >
            자동화 웹푸쉬 보내지 않기
          </Checkbox>
          <div>
            <Text type="secondary">자동화가 직접 요청한 알림은 이 설정과 관계없이 보냅니다.</Text>
          </div>
        </div>
        <div>
          <Text strong>기능동작확인</Text>
          <Segmented
            options={FUNCTION_CHECK_OPTIONS}
            value={draft.jangsingProcessingRequired ? '완료' : '오동작'}
            disabled={!hasAccess}
            onChange={(value) =>
              onChangeDraft((previous) => ({
                ...previous,
                jangsingProcessingRequired: value === '완료',
              }))
            }
          />
        </div>
      </div>
    </div>
  );
}
