- 환경/라우터 모듈에 approval_flow_v2 토글을 추가하고 FeatureFlags 초기화를 연결 (.env*, lib/core/**) - ApiClient 빌더·ApiRoutes 확장과 ApprovalRepositoryRemote 리팩터링으로 include·액션 시그니처를 정합화 - ApprovalFlow·ApprovalDraft 엔티티/레포/유즈케이스를 도입해 서버 초안과 단계 액션(승인·회수·재상신)을 지원 - Approval 컨트롤러·히스토리·템플릿 페이지와 공유 위젯을 재작성해 감사 로그·회수 UX·템플릿 CRUD를 반영 - Inbound/Outbound/Rental 컨트롤러·페이지에 결재 섹션을 삽입하고 대시보드 pending 카드 요약을 갱신 - SuperportDialog·FormField 등 공통 위젯을 보강하고 승인 위젯 가이드를 추가해 UI 가이드를 정리 - 결재/재고 테스트 픽스처와 단위·위젯·통합 테스트를 확장하고 flutter_test_config로 스테이징 호스트를 허용 - Approval Flow 레포트/플랜 문서를 업데이트하고 ApprovalFlow_System_Integration_and_ChangePlan.md를 추가 - 실행: flutter analyze, flutter test
147 lines
3.6 KiB
Dart
147 lines
3.6 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
/// 스테이징 결재 더블(approval double) 연결 시 인증서 우회를 위한 HttpOverrides.
|
|
class _ApprovalStagingHttpOverrides extends HttpOverrides {
|
|
_ApprovalStagingHttpOverrides(this._patterns);
|
|
|
|
final List<_HostPattern> _patterns;
|
|
|
|
bool _isAllowedHost(String host) {
|
|
final normalized = host.trim().toLowerCase();
|
|
if (normalized.isEmpty) {
|
|
return false;
|
|
}
|
|
for (final pattern in _patterns) {
|
|
if (pattern.matches(normalized)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
HttpClient createHttpClient(SecurityContext? context) {
|
|
final client = super.createHttpClient(context);
|
|
client.badCertificateCallback = (cert, host, port) {
|
|
// 허용된 호스트에 대해서만 자기서명 인증서를 허용한다.
|
|
return _patterns.isNotEmpty && _isAllowedHost(host);
|
|
};
|
|
return client;
|
|
}
|
|
}
|
|
|
|
/// 호스트 패턴(정확 일치 또는 서픽스 매칭)을 표현한다.
|
|
class _HostPattern {
|
|
_HostPattern._(this.value, this.isSuffix);
|
|
|
|
factory _HostPattern.parse(String raw) {
|
|
final trimmed = raw.trim().toLowerCase();
|
|
if (trimmed.startsWith('*.')) {
|
|
return _HostPattern._(trimmed.substring(2), true);
|
|
}
|
|
if (trimmed.startsWith('.')) {
|
|
return _HostPattern._(trimmed.substring(1), true);
|
|
}
|
|
return _HostPattern._(trimmed, false);
|
|
}
|
|
|
|
final String value;
|
|
final bool isSuffix;
|
|
|
|
bool get isValid => value.isNotEmpty;
|
|
|
|
bool matches(String host) {
|
|
if (!isValid) {
|
|
return false;
|
|
}
|
|
if (isSuffix) {
|
|
return host == value || host.endsWith('.$value');
|
|
}
|
|
return host == value;
|
|
}
|
|
}
|
|
|
|
bool _parseBoolDynamic(String? raw) {
|
|
if (raw == null) {
|
|
return false;
|
|
}
|
|
switch (raw.trim().toLowerCase()) {
|
|
case '1':
|
|
case 'y':
|
|
case 'yes':
|
|
case 'true':
|
|
case 'on':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
List<_HostPattern> _collectAllowedPatterns() {
|
|
final env = Platform.environment;
|
|
final tokens = <String>[];
|
|
|
|
void addTokens(String? source) {
|
|
if (source == null || source.trim().isEmpty) {
|
|
return;
|
|
}
|
|
tokens.addAll(source.split(','));
|
|
}
|
|
|
|
addTokens(env['APPROVAL_DOUBLE_ALLOWED_HOSTS']);
|
|
addTokens(
|
|
const String.fromEnvironment(
|
|
'APPROVAL_DOUBLE_ALLOWED_HOSTS',
|
|
defaultValue: '',
|
|
),
|
|
);
|
|
|
|
final baseUrl =
|
|
env['API_BASE_URL'] ??
|
|
const String.fromEnvironment('API_BASE_URL', defaultValue: '');
|
|
final uri = Uri.tryParse(baseUrl);
|
|
if (uri != null && uri.host.isNotEmpty) {
|
|
tokens.add(uri.host);
|
|
}
|
|
|
|
final patterns = <_HostPattern>[];
|
|
for (final token in tokens) {
|
|
final pattern = _HostPattern.parse(token);
|
|
if (pattern.isValid) {
|
|
patterns.add(pattern);
|
|
}
|
|
}
|
|
return patterns;
|
|
}
|
|
|
|
/// Flutter 테스트 진입점.
|
|
///
|
|
/// - `USE_APPROVAL_STAGING_DOUBLE` 토글이 true일 때 HttpOverrides를 등록한다.
|
|
/// - `APPROVAL_DOUBLE_ALLOWED_HOSTS` 또는 `API_BASE_URL` 호스트를 인증 허용 목록에 추가한다.
|
|
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
|
|
final runOverride =
|
|
_parseBoolDynamic(Platform.environment['USE_APPROVAL_STAGING_DOUBLE']) ||
|
|
const bool.fromEnvironment(
|
|
'USE_APPROVAL_STAGING_DOUBLE',
|
|
defaultValue: false,
|
|
);
|
|
|
|
HttpOverrides? previous;
|
|
if (runOverride) {
|
|
final patterns = _collectAllowedPatterns();
|
|
if (patterns.isNotEmpty) {
|
|
previous = HttpOverrides.current;
|
|
HttpOverrides.global = _ApprovalStagingHttpOverrides(patterns);
|
|
}
|
|
}
|
|
|
|
try {
|
|
await testMain();
|
|
} finally {
|
|
if (runOverride) {
|
|
HttpOverrides.global = previous;
|
|
}
|
|
}
|
|
}
|