diff --git a/src/StrategyApiView.test.tsx b/src/StrategyApiView.test.tsx index 3b53ede..886172b 100644 --- a/src/StrategyApiView.test.tsx +++ b/src/StrategyApiView.test.tsx @@ -389,7 +389,7 @@ describe('Strategy API view', () => { await waitFor(() => expect(authoringClient.releaseLease).toHaveBeenCalledWith('pagehide', 'pagehide-token')); }); - test('saves a real Basic document, validates that revision, and releases with server-owned inputs', async () => { + test('keeps a saved READY revision launchable through preview differences, then saves and releases it', async () => { const user = userEvent.setup(); const onLaunchBot = vi.fn(); const strategyId = '20000000-0000-4000-8000-000000000001'; @@ -439,9 +439,15 @@ describe('Strategy API view', () => { })), previewValidation: vi.fn().mockImplementation(async (_id, input) => ({ ...validation, + status: 'INVALID', validationRunId: `preview-${input.clientRevision}`, requestedEditSequence: input.clientRevision, semanticHash: 'preview-hash', + findings: [{ + severity: 'BLOCKING_ERROR', + code: 'CURRENT_CATALOG_MISMATCH', + message: '현재 카탈로그 미리검증과 저장된 검증 결과가 다릅니다.', + }], })), getCurrentValidations: vi.fn().mockResolvedValue([loadedValidation]), validateStrategy: vi.fn().mockResolvedValue(validation), @@ -462,6 +468,8 @@ describe('Strategy API view', () => { await waitFor(() => expect(save).toBeEnabled()); await waitFor(() => expect(authoringClient.getCurrentValidations).toHaveBeenCalledWith(expect.any(AbortSignal))); await waitFor(() => expect(screen.getByRole('button', { name: '개인 봇 출시' })).toBeEnabled()); + await screen.findByText('출시 가능 · 미리검증 차이 1'); + expect(screen.getByRole('button', { name: '개인 봇 출시' })).toBeEnabled(); const rsiValue = screen.getByRole('spinbutton', { name: 'RSI 반등 값' }); await user.clear(rsiValue); await user.type(rsiValue, '31'); diff --git a/src/views/StrategyViews.tsx b/src/views/StrategyViews.tsx index 1c00ca3..a5c0ab4 100644 --- a/src/views/StrategyViews.tsx +++ b/src/views/StrategyViews.tsx @@ -1845,19 +1845,21 @@ export function BasicEditor({ goBack, openEditor, onLaunchBot, blank = false, st const isLocallyComplete = validationIssues.length === 0 && (!catalogClient || catalogSupportsEditor); const requiresServerValidation = Boolean(strategyId && authoringClient); const isCurrentlyValid = isLocallyComplete && (!requiresServerValidation || serverValidation?.status === 'VALID'); - const isLaunchable = isCurrentlyValid - && savedReadySignature === editorSignature + const isLaunchable = savedReadySignature === editorSignature && (!requiresServerValidation || savedValidation?.status === 'VALID'); + const isValidationDisplayReady = isLaunchable || isCurrentlyValid; const serverErrorFindings = serverValidation?.findings.filter((finding) => ( finding.severity === 'BLOCKING_ERROR' || finding.severity === 'ERROR' )) ?? []; const serverWarningFindings = serverValidation?.findings.filter((finding) => finding.severity === 'WARNING') ?? []; const validationTriggerLabel = validationPending - ? '검증 중…' + ? isLaunchable ? '출시 가능 · 미리검증 중' : '검증 중…' + : isLaunchable + ? serverValidation?.status === 'INVALID' + ? `출시 가능 · 미리검증 차이 ${serverErrorFindings.length}` + : serverWarningFindings.length > 0 ? `출시 가능 · 경고 ${serverWarningFindings.length}` : '출시 가능' : isCurrentlyValid - ? isLaunchable - ? serverWarningFindings.length > 0 ? `출시 가능 · 경고 ${serverWarningFindings.length}` : '출시 가능' - : '유효 · 저장 필요' + ? '유효 · 저장 필요' : validationIssues.length > 0 ? `미완성 · 오류 ${validationIssues.length}` : serverValidation?.status === 'INVALID' @@ -3726,9 +3728,9 @@ export function BasicEditor({ goBack, openEditor, onLaunchBot, blank = false, st : '변경 없음'}
- {isCurrentlyValid - ? serverWarningFindings.length > 0 ? `출시 가능 · 경고 ${serverWarningFindings.length}개` : '모든 필수 설정을 완료했어요' + {isLaunchable + ? serverValidation?.status === 'INVALID' ? '저장된 검증 기준으로 출시 가능' : serverWarningFindings.length > 0 ? `출시 가능 · 경고 ${serverWarningFindings.length}개` : '모든 필수 설정을 완료했어요' + : isCurrentlyValid + ? '현재 구성은 유효하지만 저장이 필요합니다' : validationIssues.length > 0 ? `${validationIssues.length}개 항목을 확인해 주세요` : serverValidation?.status === 'INVALID' ? `서버 검증 오류 ${serverErrorFindings.length}개` : '저장 후 서버 검증이 필요합니다'} - {isCurrentlyValid ? isLaunchable ? '현재 저장된 구성으로 개인 봇을 출시할 수 있습니다.' : '현재 구성은 유효합니다. 저장해야 개인 봇 출시가 활성화됩니다.' : validationIssues.length > 0 ? '항목을 선택하면 수정할 전략 카드로 이동합니다.' : '현재 구성을 서버 기준으로 검증하고 있습니다.'} + {isLaunchable ? serverValidation?.status === 'INVALID' ? '현재 저장된 구성은 출시 가능하며, 아래 미리검증 결과는 새 편집을 위한 참고 정보입니다.' : '현재 저장된 구성으로 개인 봇을 출시할 수 있습니다.' : isCurrentlyValid ? '현재 구성은 유효합니다. 저장해야 개인 봇 출시가 활성화됩니다.' : validationIssues.length > 0 ? '항목을 선택하면 수정할 전략 카드로 이동합니다.' : '현재 구성을 서버 기준으로 검증하고 있습니다.'}
{!isCurrentlyValid &&
{groupedValidationIssues.map((group) =>