generated from ttipo/project-base
feat(flutter): AI 코스 입력·만드는 중·결과 화면(8–10) 추가
백엔드 AI 코스 API(4–6단계)를 앱에서 쓸 수 있게 화면 8–10 을 붙였다(설계 ai-course §14 7단계). - 기반: 첫 실행 때 만든 기기 ID 를 안전 저장소에 두고 X-Device-Id 로 보낸다(비로그인 하루 횟수 기준). 앱 언어를 Accept-Language 로 보내 AI 문구 언어를 맞춘다 - 홈 'AI에게 코스 맡기기' → 입력(8): 문장을 1초 멈추면 조건을 해석해 줄마다 채우고 고칠 수 있다. 서버와 같은 조건 규칙을 앱에서도 먼저 본다. 하루 횟수를 다 썼으면 만들지 않고 안내한다 - 만드는 중(9): 1.5초마다 진행을 조회한다. 나가면 취소하고(차감 없음), 만들던 생성 id 를 남겨 앱을 다시 켜거나 AI_004 를 받으면 이어 본다 - 결과(10): 사실 값 · 경고 · 이동 · 바꾸기 후보(한 번에 한 곳, 횟수 안 듦) · 평가 · 다시 만들기(확인 뒤 차감) · 출처와 AI 생성 표시. 요청 칸과 다시 만들기는 이 기기에서 입력한 생성일 때만 보인다 - 디자인 캔버스와 맞춤: 결과의 코스 제목은 빼고 'AI 추천 코스'와 요약으로 시작한다(사용자 결정). 마스코트 그림을 홈 · 입력 · 만드는 중 · 결과 · 횟수 소진에 넣었다 - 화면 전체를 못 불러왔을 때의 공통 오류 화면(그림 · 다시 시도 · 오류 코드와 추적 ID)을 만들어 만드는 중 화면에 쓴다 코스로 저장 · 지도 미리보기 · 홈의 비 소식 줄 · 횟수 소진의 로그인 버튼은 그 기능이 생기면 붙인다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wandergo/core/api/api_exception.dart';
|
||||
import 'package:wandergo/core/l10n/l10n.dart';
|
||||
import 'package:wandergo/core/theme/app_theme.dart';
|
||||
import 'package:wandergo/shared/widgets/app_switch.dart';
|
||||
import 'package:wandergo/shared/widgets/load_error.dart';
|
||||
|
||||
Widget _wrap(Widget child) => MaterialApp(
|
||||
locale: const Locale('ko'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(body: Center(child: child)),
|
||||
);
|
||||
|
||||
/// 인터셉터가 실어 주는 모양 그대로 — DioException.error 에 ApiException
|
||||
DioException _failure({String? traceId}) => DioException(
|
||||
requestOptions: RequestOptions(path: '/places'),
|
||||
error: ApiException.fromCode('SYS_001', traceId: traceId),
|
||||
);
|
||||
|
||||
/// keepAll 이 넣은 단어 잇기 문자를 빼고 글자를 비교한다
|
||||
Finder _text(String text) => find.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is Text &&
|
||||
(w.data ?? w.textSpan?.toPlainText() ?? '').replaceAll('', '') == text,
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('LoadErrorBlock', () {
|
||||
testWidgets('사전 문구 · 다시 시도와 오류 코드 · 추적 ID 앞 8자를 보인다', (tester) async {
|
||||
var retried = 0;
|
||||
await tester.pumpWidget(
|
||||
_wrap(
|
||||
LoadErrorBlock(
|
||||
error: _failure(traceId: '3f1c0a2e-0f1b-4f2a-9a1e-6c9f0a1b2c3d'),
|
||||
onRetry: () => retried++,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(_text('일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'), findsOneWidget);
|
||||
expect(find.text('오류 코드 SYS_001 · 추적 ID 3f1c0a2e'), findsOneWidget);
|
||||
await tester.tap(find.text('다시 시도'));
|
||||
expect(retried, 1);
|
||||
});
|
||||
|
||||
testWidgets('응답이 없던 실패(추적 ID 없음)는 오류 코드만 보인다', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(LoadErrorBlock(error: _failure(), onRetry: () {})),
|
||||
);
|
||||
expect(find.text('오류 코드 SYS_001'), findsOneWidget);
|
||||
});
|
||||
|
||||
test('추적 ID 는 ApiException 에서만 꺼낸다', () {
|
||||
expect(traceIdOf(_failure(traceId: 'abc')), 'abc');
|
||||
expect(traceIdOf(StateError('x')), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('AppSwitch', () {
|
||||
testWidgets('누르면 반대 값을 알리고, 화면 읽기에는 켜짐 상태와 이름을 준다', (tester) async {
|
||||
final handle = tester.ensureSemantics();
|
||||
bool? changed;
|
||||
await tester.pumpWidget(
|
||||
_wrap(
|
||||
AppSwitch(
|
||||
value: true,
|
||||
onChanged: (value) => changed = value,
|
||||
semanticLabel: '비가 오면 실내 위주로',
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byType(AppSwitch));
|
||||
expect(changed, isFalse);
|
||||
expect(
|
||||
tester.getSemantics(find.byType(AppSwitch)),
|
||||
matchesSemantics(
|
||||
label: '비가 오면 실내 위주로',
|
||||
hasToggledState: true,
|
||||
isToggled: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: true,
|
||||
hasTapAction: true,
|
||||
),
|
||||
);
|
||||
handle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('onChanged 가 없으면 누를 수 없다', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(
|
||||
const AppSwitch(
|
||||
value: false,
|
||||
onChanged: null,
|
||||
semanticLabel: '비가 오면 실내 위주로',
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byType(AppSwitch));
|
||||
await tester.pump();
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user