Files
ai-code-app/src/features/planBoard/PlanSchedulePage.tsx

871 lines
29 KiB
TypeScript
Executable File

import { CopyOutlined } from '@ant-design/icons';
import {
Alert,
Button,
Card,
Checkbox,
Empty,
Flex,
Input,
InputNumber,
List,
Segmented,
Select,
Space,
Tabs,
Tag,
Typography,
message,
} from 'antd';
import { memo, useEffect, useMemo, useState, type Dispatch, type SetStateAction } from 'react';
import {
buildAutomationTypeOptions,
useAutomationTypeRegistry,
} from '../../app/main/automationTypeAccess';
import { useTokenAccess } from '../../app/main/tokenAccess';
import './planBoard.css';
import './planSchedule.css';
import { maskNotePreviewByWord } from './noteMasking';
import { PlanListDetailLayout } from './PlanListDetailLayout';
import {
createPlanScheduledTask,
deletePlanScheduledTask,
fetchPlanScheduledTasks,
setupPlanBoard,
updatePlanScheduledTask,
type PlanScheduleMode,
type PlanScheduleRepeatUnit,
type PlanScheduledTask,
type PlanScheduledTaskDraft,
} from './api';
const { Paragraph, Text, Title } = Typography;
const { TextArea } = Input;
const FUNCTION_CHECK_OPTIONS = ['완료', '오동작'];
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: 'minute' },
{ label: '시간', value: 'hour' },
{ label: '일', value: 'day' },
{ label: '주', value: 'week' },
{ label: '월', value: 'month' },
];
const REPEAT_UNIT_LABELS: Record<PlanScheduleRepeatUnit, string> = {
minute: '분',
hour: '시간',
day: '일',
week: '주',
month: '개월',
};
const REPEAT_PRESET_OPTIONS: { label: string; value: number; unit: PlanScheduleRepeatUnit }[] = [
{ label: '10분', value: 10, 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;
function getRepeatIntervalMinutes(value: number, unit: PlanScheduleRepeatUnit) {
const normalizedValue = Math.max(1, Math.round(Number(value) || 1));
if (unit === 'day') {
return normalizedValue * 24 * 60;
}
if (unit === 'week') {
return normalizedValue * 7 * 24 * 60;
}
if (unit === 'month') {
return normalizedValue * 30 * 24 * 60;
}
if (unit === 'hour') {
return normalizedValue * 60;
}
return normalizedValue;
}
function getRepeatIntervalValueMax(unit: PlanScheduleRepeatUnit) {
if (unit === 'month') {
return 12;
}
if (unit === 'week') {
return 52;
}
if (unit === 'day') {
return 365;
}
if (unit === 'hour') {
return 8760;
}
return 525600;
}
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, fallbackMinutes: number) {
if (!value || !unit) {
return `${fallbackMinutes}분마다`;
}
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' && /^([01]\d|2[0-3]):[0-5]\d$/.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 formatScheduleCycle(item: PlanScheduledTask) {
const scheduleMode = normalizeScheduleMode(item.scheduleMode);
if (scheduleMode === 'daily') {
return `매일 ${normalizeDailyRunTime(item.dailyRunTime)} 실행`;
}
return formatRepeatInterval(item.repeatIntervalValue, item.repeatIntervalUnit, item.repeatIntervalMinutes);
}
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 resolveNextPlanScheduleRunAt(item: PlanScheduledTask, now = new Date()) {
if (!item.enabled) {
return 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.repeatIntervalMinutes * 60 * 1000);
return nextRunAt.getTime() <= now.getTime() ? now : nextRunAt;
}
function createEmptyScheduleDraft(defaultReleaseTarget = 'release'): PlanScheduledTaskDraft {
return {
id: null,
workId: '',
note: '',
automationType: 'none',
releaseTarget: defaultReleaseTarget,
jangsingProcessingRequired: true,
autoDeployToMain: true,
enabled: true,
immediateRunEnabled: true,
scheduleMode: 'interval',
repeatIntervalValue: 60,
repeatIntervalUnit: 'minute',
repeatIntervalMinutes: 60,
dailyRunTime: DEFAULT_DAILY_RUN_TIME,
};
}
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,
releaseTarget: item.releaseTarget,
jangsingProcessingRequired: item.jangsingProcessingRequired,
autoDeployToMain: item.autoDeployToMain,
enabled: item.enabled,
immediateRunEnabled: item.immediateRunEnabled,
scheduleMode: normalizeScheduleMode(item.scheduleMode),
repeatIntervalValue,
repeatIntervalUnit,
repeatIntervalMinutes: item.repeatIntervalMinutes ?? getRepeatIntervalMinutes(repeatIntervalValue, repeatIntervalUnit),
dailyRunTime: normalizeDailyRunTime(item.dailyRunTime),
};
}
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 '중지';
}
return formatPlanScheduleDateTime(resolveNextPlanScheduleRunAt(item));
}
function validateScheduleDraft(draft: PlanScheduledTaskDraft, items: PlanScheduledTask[]) {
const messages: string[] = [];
const workId = draft.workId.trim();
const note = draft.note.trim();
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 (draft.scheduleMode === 'interval' && getRepeatIntervalMinutes(draft.repeatIntervalValue, draft.repeatIntervalUnit) < 10) {
messages.push('반복 주기는 최소 10분 이상으로 설정하세요.');
}
if (!draft.enabled) {
messages.push('비활성 스케줄은 자동 등록되지 않습니다.');
}
return messages;
}
export function PlanSchedulePage() {
const { hasAccess } = useTokenAccess();
const { automationTypes } = useAutomationTypeRegistry();
const [messageApi, contextHolder] = message.useMessage();
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]);
async function loadItems() {
setLoading(true);
setErrorMessage(null);
try {
setItems(await fetchPlanScheduledTasks());
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : '스케줄 목록을 불러오지 못했습니다.');
} 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,
repeatIntervalMinutes: getRepeatIntervalMinutes(repeatIntervalValue, draft.repeatIntervalUnit),
};
const savedItem = draft.id ? await updatePlanScheduledTask(draftToSave) : await createPlanScheduledTask(draftToSave);
messageApi.success(draft.id ? '스케줄을 수정했습니다.' : '스케줄을 등록했습니다.');
setDraft(toDraft(savedItem));
await loadItems();
} catch (error) {
messageApi.error(error instanceof Error ? error.message : '스케줄 저장에 실패했습니다.');
} finally {
setSaving(false);
}
}
async function handleDelete() {
if (!draft.id) {
return;
}
if (!hasAccess) {
messageApi.error('설정 > 토큰 관리에서 권한 토큰을 등록한 사용자만 사용할 수 있습니다.');
return;
}
if (!window.confirm('선택한 스케줄을 삭제할까요?')) {
return;
}
setSaving(true);
try {
await deletePlanScheduledTask(draft.id);
messageApi.success('스케줄을 삭제했습니다.');
setDraft(createEmptyScheduleDraft(draft.releaseTarget));
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));
setEditorOpen(true);
}
function handleSelectItem(item: PlanScheduledTask) {
setDraft(toDraft(item));
setEditorOpen(true);
}
function closeEditor() {
setEditorOpen(false);
}
return (
<div className="plan-schedule-page">
{contextHolder}
<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)}
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>{formatScheduleCycle(item)}</Tag>
<Tag color="blue"> {formatNextPlanScheduleRunAt(item)}</Tag>
<Tag>{item.immediateRunEnabled ? '즉시실행' : '주기 후 실행'}</Tag>
<Tag>{item.autoDeployToMain ? 'main 자동등록' : 'release만'}</Tag>
<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,
draft,
hasAccess,
selectedItem,
validationMessages,
onChangeDraft,
onCopyText,
}: {
automationTypeOptions: Array<{ label: string; value: string }>;
draft: PlanScheduledTaskDraft;
hasAccess: boolean;
selectedItem: PlanScheduledTask | null;
validationMessages: string[];
onChangeDraft: Dispatch<SetStateAction<PlanScheduledTaskDraft>>;
onCopyText: (text: string) => Promise<void>;
}) {
return (
<Space direction="vertical" size={14} style={{ width: '100%' }}>
{selectedItem ? (
<Alert
showIcon
type="info"
className="plan-schedule-page__alert"
message="등록 정보"
description={
<Space direction="vertical" size={4}>
<Text> : {formatNextPlanScheduleRunAt(selectedItem)}</Text>
<Text> : {formatPlanScheduleDateTime(selectedItem.lastRegisteredAt)}</Text>
<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>
<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' ? (
<Space align="center" wrap>
<Text></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',
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',
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,
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,
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,
repeatIntervalMinutes: getRepeatIntervalMinutes(option.value, option.unit),
}))
}
>
{option.label}
</Button>
))}
</Space>
</>
)}
</div>
<div>
<Checkbox
checked={draft.immediateRunEnabled}
disabled={!hasAccess}
onChange={(event) =>
onChangeDraft((previous) => ({ ...previous, immediateRunEnabled: event.target.checked }))
}
>
</Checkbox>
</div>
<div>
<Checkbox
checked={draft.autoDeployToMain}
disabled={!hasAccess}
onChange={(event) => onChangeDraft((previous) => ({ ...previous, autoDeployToMain: event.target.checked }))}
>
</Checkbox>
</div>
<div>
<Text strong></Text>
<Segmented
options={FUNCTION_CHECK_OPTIONS}
value={draft.jangsingProcessingRequired ? '완료' : '오동작'}
disabled={!hasAccess}
onChange={(value) =>
onChangeDraft((previous) => ({
...previous,
jangsingProcessingRequired: value === '완료',
}))
}
/>
</div>
</div>
</Space>
);
}