249 lines
10 KiB
TypeScript
249 lines
10 KiB
TypeScript
import { ReloadOutlined, SaveOutlined } from '@ant-design/icons';
|
|
import { Alert, App, Button, Card, Checkbox, Flex, Form, Input, InputNumber, Select, Space, Spin, Typography } from 'antd';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import {
|
|
DEFAULT_APP_CONFIG,
|
|
getWeeklyScheduleOptions,
|
|
saveAppConfigToServer,
|
|
type AppConfig,
|
|
type PlanCostTimeUnit,
|
|
} from './appConfig';
|
|
import './SharedAppSettingsPage.css';
|
|
|
|
const { Paragraph, Text, Title } = Typography;
|
|
|
|
const PLAN_COST_TIME_UNIT_OPTIONS: Array<{ value: PlanCostTimeUnit; label: string }> = [
|
|
{ value: 'hour', label: '시간' },
|
|
{ value: 'minute', label: '분' },
|
|
{ value: 'second', label: '초' },
|
|
];
|
|
|
|
type SharedAppSettingsPageProps = {
|
|
shareToken: string;
|
|
};
|
|
|
|
type SharedAppSettingsFormValue = AppConfig;
|
|
|
|
export function SharedAppSettingsPage({ shareToken }: SharedAppSettingsPageProps) {
|
|
const { message } = App.useApp();
|
|
const [form] = Form.useForm<SharedAppSettingsFormValue>();
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [errorMessage, setErrorMessage] = useState('');
|
|
const [savedConfig, setSavedConfig] = useState<AppConfig>(DEFAULT_APP_CONFIG);
|
|
|
|
const loadConfig = useCallback(async () => {
|
|
setIsLoading(true);
|
|
setErrorMessage('');
|
|
|
|
try {
|
|
const response = await fetch('/api/app-config', {
|
|
headers: {
|
|
'X-Chat-Share-Token': shareToken,
|
|
},
|
|
cache: 'no-store',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const payload = (await response.json().catch(() => null)) as { message?: string } | null;
|
|
throw new Error(payload?.message || '앱 설정을 불러오지 못했습니다.');
|
|
}
|
|
|
|
const payload = (await response.json()) as { config?: AppConfig };
|
|
const nextConfig = payload.config ?? DEFAULT_APP_CONFIG;
|
|
setSavedConfig(nextConfig);
|
|
form.setFieldsValue(nextConfig);
|
|
} catch (error) {
|
|
setErrorMessage(error instanceof Error ? error.message : '앱 설정을 불러오지 못했습니다.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [form, shareToken]);
|
|
|
|
useEffect(() => {
|
|
void loadConfig();
|
|
}, [loadConfig]);
|
|
|
|
const handleSave = useCallback(
|
|
async (values: SharedAppSettingsFormValue) => {
|
|
setIsSaving(true);
|
|
setErrorMessage('');
|
|
|
|
try {
|
|
const saved = await saveAppConfigToServer(values, {
|
|
shareToken,
|
|
skipAutomationNotifications: true,
|
|
});
|
|
setSavedConfig(saved);
|
|
form.setFieldsValue(saved);
|
|
message.success('앱 설정을 저장했습니다.');
|
|
} catch (error) {
|
|
setErrorMessage(error instanceof Error ? error.message : '앱 설정 저장에 실패했습니다.');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
},
|
|
[form, message, shareToken],
|
|
);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="shared-app-settings-page shared-app-settings-page--loading">
|
|
<Spin size="large" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="shared-app-settings-page">
|
|
<Flex align="center" justify="space-between" gap={12} wrap>
|
|
<div>
|
|
<Title level={4}>앱 설정</Title>
|
|
<Paragraph type="secondary">
|
|
공유 링크에서 허용된 핵심 앱 설정만 바로 수정합니다.
|
|
</Paragraph>
|
|
</div>
|
|
<Space>
|
|
<Button icon={<ReloadOutlined />} onClick={() => void loadConfig()} disabled={isSaving}>
|
|
새로고침
|
|
</Button>
|
|
<Button type="primary" icon={<SaveOutlined />} onClick={() => void form.submit()} loading={isSaving}>
|
|
저장
|
|
</Button>
|
|
</Space>
|
|
</Flex>
|
|
|
|
{errorMessage ? <Alert showIcon type="error" message={errorMessage} /> : null}
|
|
|
|
<Form<SharedAppSettingsFormValue>
|
|
form={form}
|
|
layout="vertical"
|
|
initialValues={savedConfig}
|
|
onFinish={(values) => void handleSave(values)}
|
|
>
|
|
<div className="shared-app-settings-page__grid">
|
|
<Card size="small" title="채팅 문맥 설정">
|
|
<Form.Item label="최근 메시지 수" name={['chat', 'maxContextMessages']}>
|
|
<InputNumber min={1} max={50} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="최대 문자 수" name={['chat', 'maxContextChars']}>
|
|
<InputNumber min={500} max={20000} step={100} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="Codex Live 최대 실행 시간(초)" name={['chat', 'codexLiveMaxExecutionSeconds']}>
|
|
<InputNumber min={60} max={7200} step={30} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="무출력 실패 시간(초)" name={['chat', 'codexLiveIdleTimeoutSeconds']}>
|
|
<InputNumber min={30} max={3600} step={10} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="재기동 완료 자동 실행 대기(초)" name={['chat', 'restartReservationCompletionDelaySeconds']}>
|
|
<InputNumber min={1} max={300} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item name={['chat', 'receiveRoomNotifications']} valuePropName="checked">
|
|
<Checkbox>채팅방 알림 수신</Checkbox>
|
|
</Form.Item>
|
|
</Card>
|
|
|
|
<Card size="small" title="자동접수 / 주기">
|
|
<Form.Item name={['automation', 'autoRefreshEnabled']} valuePropName="checked">
|
|
<Checkbox>자동 새로고침 사용</Checkbox>
|
|
</Form.Item>
|
|
<Form.Item label="자동 새로고침 간격(초)" name={['automation', 'autoRefreshIntervalSeconds']}>
|
|
<InputNumber min={1} max={3600} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="자동접수 방식" name={['automation', 'autoReceiveScheduleType']}>
|
|
<Select
|
|
options={[
|
|
{ value: 'interval', label: '간격' },
|
|
{ value: 'daily', label: '매일' },
|
|
{ value: 'weekly', label: '매주' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item label="간격(초)" name={['automation', 'autoReceiveIntervalSeconds']}>
|
|
<InputNumber min={1} max={3600} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="매일 시각" name={['automation', 'autoReceiveDailyTime']}>
|
|
<Input placeholder="09:00" />
|
|
</Form.Item>
|
|
<Form.Item label="매주 요일" name={['automation', 'autoReceiveWeeklyDay']}>
|
|
<Select options={getWeeklyScheduleOptions()} />
|
|
</Form.Item>
|
|
<Form.Item label="매주 시각" name={['automation', 'autoReceiveWeeklyTime']}>
|
|
<Input placeholder="09:00" />
|
|
</Form.Item>
|
|
</Card>
|
|
|
|
<Card size="small" title="자동화 기본값">
|
|
<Form.Item name={['planDefaults', 'jangsingProcessingRequired']} valuePropName="checked">
|
|
<Checkbox>장싱 처리 필수</Checkbox>
|
|
</Form.Item>
|
|
<Form.Item name={['planDefaults', 'autoDeployToMain']} valuePropName="checked">
|
|
<Checkbox>main 자동 반영</Checkbox>
|
|
</Form.Item>
|
|
<Form.Item name={['planDefaults', 'openEditorAfterCreate']} valuePropName="checked">
|
|
<Checkbox>생성 후 에디터 열기</Checkbox>
|
|
</Form.Item>
|
|
</Card>
|
|
|
|
<Card size="small" title="업무일지 자동화">
|
|
<Form.Item name={['worklogAutomation', 'autoCreateDailyWorklog']} valuePropName="checked">
|
|
<Checkbox>일일 업무일지 자동 생성</Checkbox>
|
|
</Form.Item>
|
|
<Form.Item label="생성 시각" name={['worklogAutomation', 'dailyCreateTime']}>
|
|
<Input placeholder="18:00" />
|
|
</Form.Item>
|
|
<Form.Item name={['worklogAutomation', 'includeScreenshots']} valuePropName="checked">
|
|
<Checkbox>스크린샷 포함</Checkbox>
|
|
</Form.Item>
|
|
<Form.Item name={['worklogAutomation', 'includeChangedFiles']} valuePropName="checked">
|
|
<Checkbox>변경 파일 포함</Checkbox>
|
|
</Form.Item>
|
|
<Form.Item name={['worklogAutomation', 'includeCommandLogs']} valuePropName="checked">
|
|
<Checkbox>명령 로그 포함</Checkbox>
|
|
</Form.Item>
|
|
<Form.Item label="템플릿" name={['worklogAutomation', 'template']}>
|
|
<Select
|
|
options={[
|
|
{ value: 'simple', label: '간단' },
|
|
{ value: 'detailed', label: '상세' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
</Card>
|
|
|
|
<Card size="small" title="비용 표시 / 단축키">
|
|
<Form.Item label="백만 토큰당 기본 비용" name={['planCost', 'baseCostPerMillionTokens']}>
|
|
<InputNumber min={100} max={1000000} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="재시도 비용 배수(%)" name={['planCost', 'retryCostMultiplierPercent']}>
|
|
<InputNumber min={0} max={500} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="시간 비용 배수(%)" name={['planCost', 'hourlyCostMultiplierPercent']}>
|
|
<InputNumber min={0} max={500} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="시간 단위" name={['planCost', 'timeCostUnit']}>
|
|
<Select options={PLAN_COST_TIME_UNIT_OPTIONS} />
|
|
</Form.Item>
|
|
<Form.Item label="주의 배수" name={['planCost', 'attentionCostThresholdMultiplier']}>
|
|
<InputNumber min={0.1} max={100} step={0.1} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="경고 배수" name={['planCost', 'warningCostThresholdMultiplier']}>
|
|
<InputNumber min={0.1} max={100} step={0.1} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="고비용 배수" name={['planCost', 'highCostThresholdMultiplier']}>
|
|
<InputNumber min={0.1} max={100} step={0.1} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item label="검색 단축키" name={['gestureShortcuts', 'openSearch']}>
|
|
<Input />
|
|
</Form.Item>
|
|
</Card>
|
|
</div>
|
|
</Form>
|
|
|
|
<Text type="secondary">
|
|
알림 토큰 등록과 업데이트 확인처럼 현재 기기 상태가 필요한 항목은 공유 링크에서 제외했습니다.
|
|
</Text>
|
|
</div>
|
|
);
|
|
}
|