refactor: 사용하지 않는 파일 9개 정리
- 코드베이스 분석을 통해 사용되지 않는 파일 식별 및 삭제 - migration, utils, models, screens 등 미사용 파일 제거 - flutter analyze 결과: 63개 → 48개 오류로 개선 - 전체 .dart 파일 수: 365개로 정리 완료 삭제된 파일: - lib/core/migrations/execute_migration.dart - lib/core/utils/login_diagnostics.dart - lib/utils/equipment_display_helper.dart - lib/utils/formatters/business_number_formatter.dart - lib/utils/user_utils.dart - lib/models/user_phone_field.dart - lib/screens/rent/rent_list_screen_simple.dart - lib/screens/common/widgets/category_autocomplete_field.dart - lib/screens/common/widgets/company_branch_dropdown.dart ✅ 빌드 검증 완료: flutter pub get, build_runner 성공 ✅ Phase 10 이후 추가 코드베이스 정리 완료
This commit is contained in:
@@ -1,326 +0,0 @@
|
||||
/// License → Maintenance 마이그레이션 실행 스크립트
|
||||
///
|
||||
/// 사용법:
|
||||
/// dart run lib/core/migrations/execute_migration.dart
|
||||
///
|
||||
/// 옵션:
|
||||
/// --dry-run: 실제 데이터 변경 없이 시뮬레이션만 수행
|
||||
/// --rollback: 이전 백업에서 롤백 실행
|
||||
/// --validate: 마이그레이션 후 데이터 검증만 수행
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'license_to_maintenance_migration.dart';
|
||||
import 'maintenance_data_validator.dart';
|
||||
|
||||
class MigrationExecutor {
|
||||
static const String apiBaseUrl = 'http://43.201.34.104:8080/api/v1';
|
||||
static const String backupPath = './migration_backup.json';
|
||||
|
||||
final Dio _dio;
|
||||
String? _authToken;
|
||||
|
||||
MigrationExecutor() : _dio = Dio(BaseOptions(
|
||||
baseUrl: apiBaseUrl,
|
||||
connectTimeout: const Duration(seconds: 30),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
));
|
||||
|
||||
/// 마이그레이션 실행
|
||||
Future<void> execute({bool isDryRun = false}) async {
|
||||
print('=' * 60);
|
||||
print('License → Maintenance 마이그레이션 시작');
|
||||
print('=' * 60);
|
||||
print('모드: ${isDryRun ? "DRY RUN (시뮬레이션)" : "실제 실행"}');
|
||||
print('시작 시간: ${DateTime.now()}');
|
||||
print('-' * 60);
|
||||
|
||||
try {
|
||||
// 1. 인증
|
||||
print('\n[1/7] API 인증 중...');
|
||||
await _authenticate();
|
||||
|
||||
// 2. 기존 License 데이터 가져오기
|
||||
print('\n[2/7] License 데이터 로딩 중...');
|
||||
final licenseData = await _fetchLicenseData();
|
||||
print(' → ${licenseData.length}개 License 발견');
|
||||
|
||||
// 3. Equipment 및 Equipment History 데이터 가져오기
|
||||
print('\n[3/7] Equipment 관련 데이터 로딩 중...');
|
||||
final equipmentData = await _fetchEquipmentData();
|
||||
final equipmentHistoryData = await _fetchEquipmentHistoryData();
|
||||
print(' → ${equipmentData.length}개 Equipment 발견');
|
||||
print(' → ${equipmentHistoryData.length}개 Equipment History 발견');
|
||||
|
||||
// 4. 마이그레이션 실행
|
||||
print('\n[4/7] 데이터 변환 중...');
|
||||
final result = await LicenseToMaintenanceMigration.migrate(
|
||||
licenseData: licenseData,
|
||||
equipmentData: equipmentData,
|
||||
equipmentHistoryData: equipmentHistoryData,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw Exception('마이그레이션 실패: ${result.error}');
|
||||
}
|
||||
|
||||
// 5. 백업 저장
|
||||
print('\n[5/7] 백업 저장 중...');
|
||||
if (!isDryRun) {
|
||||
await _saveBackup(result.backup!);
|
||||
print(' → 백업 저장 완료: $backupPath');
|
||||
} else {
|
||||
print(' → [DRY RUN] 백업 저장 건너뜀');
|
||||
}
|
||||
|
||||
// 6. Maintenance 데이터 저장
|
||||
print('\n[6/7] Maintenance 데이터 저장 중...');
|
||||
if (!isDryRun) {
|
||||
await _saveMaintenanceData(result.maintenanceData!);
|
||||
print(' → ${result.maintenanceData!.length}개 Maintenance 저장 완료');
|
||||
} else {
|
||||
print(' → [DRY RUN] 실제 저장 건너뜀');
|
||||
_printSampleData(result.maintenanceData!);
|
||||
}
|
||||
|
||||
// 7. 검증
|
||||
print('\n[7/7] 데이터 검증 중...');
|
||||
final validationReport = await MaintenanceDataValidator.validate(
|
||||
maintenanceData: result.maintenanceData!,
|
||||
equipmentHistoryData: [], // TODO: 실제 equipment history 데이터 로드
|
||||
);
|
||||
|
||||
_printValidationReport(validationReport);
|
||||
|
||||
// 완료
|
||||
print('\n${'=' * 60}');
|
||||
print('마이그레이션 ${isDryRun ? "시뮬레이션" : "실행"} 완료!');
|
||||
print('=' * 60);
|
||||
print('통계:');
|
||||
print(' - 총 License: ${result.statistics!.totalCount}개');
|
||||
print(' - 활성: ${result.statistics!.activeCount}개');
|
||||
print(' - 만료 예정: ${result.statistics!.upcomingCount}개');
|
||||
print(' - 만료됨: ${result.statistics!.expiredCount}개');
|
||||
print('종료 시간: ${DateTime.now()}');
|
||||
|
||||
} catch (e) {
|
||||
print('\n❌ 마이그레이션 실패!');
|
||||
print('오류: $e');
|
||||
print('\n롤백이 필요한 경우 다음 명령을 실행하세요:');
|
||||
print('dart run lib/core/migrations/execute_migration.dart --rollback');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// 롤백 실행
|
||||
Future<void> rollback() async {
|
||||
print('=' * 60);
|
||||
print('License → Maintenance 롤백 시작');
|
||||
print('=' * 60);
|
||||
|
||||
try {
|
||||
// 백업 파일 로드
|
||||
final backupFile = File(backupPath);
|
||||
if (!await backupFile.exists()) {
|
||||
throw Exception('백업 파일을 찾을 수 없습니다: $backupPath');
|
||||
}
|
||||
|
||||
final backupContent = await backupFile.readAsString();
|
||||
final backup = jsonDecode(backupContent) as Map<String, dynamic>;
|
||||
|
||||
print('백업 정보:');
|
||||
print(' - 생성 시간: ${backup['timestamp']}');
|
||||
print(' - 버전: ${backup['version']}');
|
||||
print(' - 데이터 수: ${(backup['data'] as List).length}개');
|
||||
|
||||
// 롤백 확인
|
||||
print('\n정말로 롤백하시겠습니까? (y/n)');
|
||||
final confirm = stdin.readLineSync();
|
||||
if (confirm?.toLowerCase() != 'y') {
|
||||
print('롤백 취소됨');
|
||||
return;
|
||||
}
|
||||
|
||||
// 롤백 실행
|
||||
print('\n롤백 실행 중...');
|
||||
final success = await LicenseToMaintenanceMigration.rollback(backup);
|
||||
|
||||
if (success) {
|
||||
print('✅ 롤백 완료!');
|
||||
|
||||
// Maintenance 데이터 삭제
|
||||
print('Maintenance 데이터 정리 중...');
|
||||
await _deleteMaintenanceData();
|
||||
|
||||
// License 데이터 복원
|
||||
print('License 데이터 복원 중...');
|
||||
await _restoreLicenseData(backup['data'] as List);
|
||||
|
||||
print('\n롤백이 성공적으로 완료되었습니다.');
|
||||
} else {
|
||||
throw Exception('롤백 실패');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
print('\n❌ 롤백 실패!');
|
||||
print('오류: $e');
|
||||
print('\n수동 복구가 필요할 수 있습니다.');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// API 인증
|
||||
Future<void> _authenticate() async {
|
||||
try {
|
||||
final response = await _dio.post('/auth/login', data: {
|
||||
'email': 'admin@example.com',
|
||||
'password': 'password123',
|
||||
});
|
||||
|
||||
_authToken = response.data['token'];
|
||||
_dio.options.headers['Authorization'] = 'Bearer $_authToken';
|
||||
} catch (e) {
|
||||
throw Exception('인증 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// License 데이터 가져오기
|
||||
Future<List<Map<String, dynamic>>> _fetchLicenseData() async {
|
||||
try {
|
||||
// 실제 환경에서는 API 호출
|
||||
// 여기서는 더미 데이터 반환 (실제 구현 시 수정 필요)
|
||||
return [
|
||||
{
|
||||
'id': 1,
|
||||
'equipment_id': 1,
|
||||
'license_type': 'O',
|
||||
'period_months': 12,
|
||||
'cost': 1000000,
|
||||
'vendor_name': '삼성전자서비스',
|
||||
'vendor_contact': '1588-3366',
|
||||
'start_date': '2024-01-01T00:00:00Z',
|
||||
'expiry_date': '2024-12-31T23:59:59Z',
|
||||
'created_at': '2024-01-01T00:00:00Z',
|
||||
},
|
||||
// 추가 데이터...
|
||||
];
|
||||
} catch (e) {
|
||||
throw Exception('License 데이터 로딩 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Equipment 데이터 가져오기
|
||||
Future<List<Map<String, dynamic>>> _fetchEquipmentData() async {
|
||||
try {
|
||||
final response = await _dio.get('/equipments');
|
||||
return (response.data['data'] as List)
|
||||
.map((e) => e as Map<String, dynamic>)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw Exception('Equipment 데이터 로딩 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Equipment History 데이터 가져오기
|
||||
Future<List<Map<String, dynamic>>> _fetchEquipmentHistoryData() async {
|
||||
try {
|
||||
final response = await _dio.get('/equipment_history');
|
||||
return (response.data['data'] as List)
|
||||
.map((e) => e as Map<String, dynamic>)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
// Equipment History가 아직 구현되지 않았을 수 있음
|
||||
print(' ⚠️ Equipment History API 미구현, 빈 데이터 사용');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 백업 저장
|
||||
Future<void> _saveBackup(Map<String, dynamic> backup) async {
|
||||
final file = File(backupPath);
|
||||
await file.writeAsString(jsonEncode(backup));
|
||||
}
|
||||
|
||||
/// Maintenance 데이터 저장
|
||||
Future<void> _saveMaintenanceData(List<Map<String, dynamic>> data) async {
|
||||
for (final maintenance in data) {
|
||||
try {
|
||||
await _dio.post('/maintenances', data: maintenance);
|
||||
} catch (e) {
|
||||
print(' ⚠️ Maintenance #${maintenance['id']} 저장 실패: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maintenance 데이터 삭제
|
||||
Future<void> _deleteMaintenanceData() async {
|
||||
try {
|
||||
// 모든 Maintenance 데이터 삭제
|
||||
await _dio.delete('/maintenances/all');
|
||||
} catch (e) {
|
||||
print(' ⚠️ Maintenance 데이터 삭제 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// License 데이터 복원
|
||||
Future<void> _restoreLicenseData(List<dynamic> data) async {
|
||||
for (final license in data) {
|
||||
try {
|
||||
await _dio.post('/licenses', data: license);
|
||||
} catch (e) {
|
||||
print(' ⚠️ License #${license['id']} 복원 실패: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 샘플 데이터 출력 (DRY RUN용)
|
||||
void _printSampleData(List<Map<String, dynamic>> data) {
|
||||
print('\n 샘플 변환 데이터 (처음 3개):');
|
||||
for (var i = 0; i < (data.length > 3 ? 3 : data.length); i++) {
|
||||
final item = data[i];
|
||||
print(' ${i + 1}. Maintenance #${item['id']}');
|
||||
print(' - Equipment History ID: ${item['equipment_history_id']}');
|
||||
print(' - Type: ${item['maintenance_type']}');
|
||||
print(' - Period: ${item['period_months']} months');
|
||||
print(' - Status: ${item['status']}');
|
||||
print(' - Next Date: ${item['next_date']}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 검증 보고서 출력
|
||||
void _printValidationReport(ValidationReport report) {
|
||||
print('\n 검증 결과:');
|
||||
print(' - 전체 검증: ${report.isValid ? "✅ 통과" : "❌ 실패"}');
|
||||
print(' - 데이터 무결성: ${report.dataIntegrity ? "✅" : "❌"}');
|
||||
print(' - FK 관계: ${report.foreignKeyIntegrity ? "✅" : "❌"}');
|
||||
print(' - 비즈니스 규칙: ${report.businessRulesValid ? "✅" : "❌"}');
|
||||
|
||||
if (report.warnings.isNotEmpty) {
|
||||
print('\n 경고:');
|
||||
for (final warning in report.warnings) {
|
||||
print(' ⚠️ $warning');
|
||||
}
|
||||
}
|
||||
|
||||
if (report.errors.isNotEmpty) {
|
||||
print('\n 오류:');
|
||||
for (final error in report.errors) {
|
||||
print(' ❌ $error');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 메인 실행 함수
|
||||
Future<void> main(List<String> args) async {
|
||||
final executor = MigrationExecutor();
|
||||
|
||||
if (args.contains('--rollback')) {
|
||||
await executor.rollback();
|
||||
} else {
|
||||
final isDryRun = args.contains('--dry-run');
|
||||
await executor.execute(isDryRun: isDryRun);
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:superport/core/utils/debug_logger.dart';
|
||||
import 'package:superport/core/config/environment.dart' as env;
|
||||
|
||||
/// 로그인 문제 진단을 위한 유틸리티 클래스
|
||||
class LoginDiagnostics {
|
||||
/// 로그인 프로세스 전체 진단
|
||||
static Future<Map<String, dynamic>> runFullDiagnostics() async {
|
||||
final results = <String, dynamic>{};
|
||||
|
||||
try {
|
||||
// 1. 환경 설정 확인
|
||||
results['environment'] = _checkEnvironment();
|
||||
|
||||
// 2. 네트워크 연결 확인
|
||||
results['network'] = await _checkNetworkConnectivity();
|
||||
|
||||
// 3. API 엔드포인트 확인
|
||||
results['apiEndpoint'] = await _checkApiEndpoint();
|
||||
|
||||
// 4. 모델 직렬화 테스트
|
||||
results['serialization'] = _testSerialization();
|
||||
|
||||
// 5. 저장소 접근 테스트
|
||||
results['storage'] = await _testStorageAccess();
|
||||
|
||||
DebugLogger.log(
|
||||
'로그인 진단 완료',
|
||||
tag: 'DIAGNOSTICS',
|
||||
data: results,
|
||||
);
|
||||
|
||||
return results;
|
||||
} catch (e, stackTrace) {
|
||||
DebugLogger.logError(
|
||||
'진단 중 오류 발생',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return {
|
||||
'error': e.toString(),
|
||||
'stackTrace': stackTrace.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 환경 설정 확인
|
||||
static Map<String, dynamic> _checkEnvironment() {
|
||||
return {
|
||||
'apiBaseUrl': env.Environment.apiBaseUrl,
|
||||
'isDebugMode': kDebugMode,
|
||||
'platform': defaultTargetPlatform.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 네트워크 연결 확인
|
||||
static Future<Map<String, dynamic>> _checkNetworkConnectivity() async {
|
||||
final dio = Dio();
|
||||
final results = <String, dynamic>{};
|
||||
|
||||
try {
|
||||
// Google DNS로 연결 테스트
|
||||
final response = await dio.get('https://dns.google/resolve?name=google.com');
|
||||
results['internetConnection'] = response.statusCode == 200;
|
||||
} catch (e) {
|
||||
results['internetConnection'] = false;
|
||||
results['error'] = e.toString();
|
||||
}
|
||||
|
||||
// API 서버 연결 테스트
|
||||
try {
|
||||
final response = await dio.get(
|
||||
'${env.Environment.apiBaseUrl}/health',
|
||||
options: Options(
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
),
|
||||
);
|
||||
results['apiServerReachable'] = true;
|
||||
results['apiServerStatus'] = response.statusCode;
|
||||
} catch (e) {
|
||||
results['apiServerReachable'] = false;
|
||||
results['apiServerError'] = e.toString();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// API 엔드포인트 확인
|
||||
static Future<Map<String, dynamic>> _checkApiEndpoint() async {
|
||||
|
||||
final dio = Dio();
|
||||
final results = <String, dynamic>{};
|
||||
|
||||
try {
|
||||
// OPTIONS 요청으로 CORS 확인
|
||||
final response = await dio.request(
|
||||
'${env.Environment.apiBaseUrl}/auth/login',
|
||||
options: Options(
|
||||
method: 'OPTIONS',
|
||||
validateStatus: (status) => true,
|
||||
),
|
||||
);
|
||||
|
||||
results['corsEnabled'] = response.statusCode == 200 || response.statusCode == 204;
|
||||
results['allowedMethods'] = response.headers['access-control-allow-methods'];
|
||||
results['allowedHeaders'] = response.headers['access-control-allow-headers'];
|
||||
|
||||
// 실제 로그인 엔드포인트 테스트 (잘못된 자격 증명으로)
|
||||
final loginResponse = await dio.post(
|
||||
'${env.Environment.apiBaseUrl}/auth/login',
|
||||
data: {
|
||||
'email': 'test@test.com',
|
||||
'password': 'test',
|
||||
},
|
||||
options: Options(
|
||||
validateStatus: (status) => true,
|
||||
),
|
||||
);
|
||||
|
||||
results['loginEndpointStatus'] = loginResponse.statusCode;
|
||||
results['loginResponseType'] = loginResponse.data?.runtimeType.toString();
|
||||
|
||||
// 응답 구조 분석
|
||||
if (loginResponse.data is Map) {
|
||||
final data = loginResponse.data as Map;
|
||||
results['responseKeys'] = data.keys.toList();
|
||||
results['hasSuccessField'] = data.containsKey('success');
|
||||
results['hasDataField'] = data.containsKey('data');
|
||||
results['hasAccessToken'] = data.containsKey('accessToken') || data.containsKey('access_token');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
results['error'] = e.toString();
|
||||
if (e is DioException) {
|
||||
results['dioErrorType'] = e.type.toString();
|
||||
results['dioMessage'] = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// 모델 직렬화 테스트
|
||||
static Map<String, dynamic> _testSerialization() {
|
||||
final results = <String, dynamic>{};
|
||||
|
||||
try {
|
||||
// LoginRequest 테스트
|
||||
results['loginRequestValid'] = true;
|
||||
|
||||
// LoginResponse 테스트 (형식 1)
|
||||
final loginResponse1 = {
|
||||
'success': true,
|
||||
'data': {
|
||||
'accessToken': 'test_token',
|
||||
'refreshToken': 'refresh_token',
|
||||
'tokenType': 'Bearer',
|
||||
'expiresIn': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '테스트',
|
||||
'role': 'USER',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
results['format1Valid'] = _validateResponseFormat1(loginResponse1);
|
||||
|
||||
// LoginResponse 테스트 (형식 2)
|
||||
final loginResponse2 = {
|
||||
'accessToken': 'test_token',
|
||||
'refreshToken': 'refresh_token',
|
||||
'tokenType': 'Bearer',
|
||||
'expiresIn': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '테스트',
|
||||
'role': 'USER',
|
||||
},
|
||||
};
|
||||
|
||||
results['format2Valid'] = _validateResponseFormat2(loginResponse2);
|
||||
|
||||
} catch (e) {
|
||||
results['error'] = e.toString();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// 저장소 접근 테스트
|
||||
static Future<Map<String, dynamic>> _testStorageAccess() async {
|
||||
final results = <String, dynamic>{};
|
||||
|
||||
try {
|
||||
// 실제 FlutterSecureStorage 테스트는 의존성 주입이 필요하므로
|
||||
// 여기서는 기본적인 체크만 수행
|
||||
results['platformSupported'] = true;
|
||||
|
||||
// 플랫폼별 특이사항 체크
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS) {
|
||||
results['note'] = 'iOS Keychain 사용';
|
||||
} else if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
results['note'] = 'Android KeyStore 사용';
|
||||
} else {
|
||||
results['note'] = '웹 또는 데스크톱 플랫폼';
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
results['error'] = e.toString();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// 응답 형식 1 검증
|
||||
static bool _validateResponseFormat1(Map<String, dynamic> response) {
|
||||
try {
|
||||
if (!response.containsKey('success') || response['success'] != true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!response.containsKey('data') || response['data'] is! Map) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final data = response['data'] as Map<String, dynamic>;
|
||||
final requiredFields = ['accessToken', 'refreshToken', 'user'];
|
||||
|
||||
for (final field in requiredFields) {
|
||||
if (!data.containsKey(field)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 응답 형식 2 검증
|
||||
static bool _validateResponseFormat2(Map<String, dynamic> response) {
|
||||
try {
|
||||
final requiredFields = ['accessToken', 'refreshToken', 'user'];
|
||||
|
||||
for (final field in requiredFields) {
|
||||
if (!response.containsKey(field)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (response['user'] is! Map) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 진단 결과를 읽기 쉬운 형식으로 포맷
|
||||
static String formatDiagnosticsReport(Map<String, dynamic> diagnostics) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('=== 로그인 진단 보고서 ===\n');
|
||||
|
||||
// 환경 설정
|
||||
if (diagnostics.containsKey('environment')) {
|
||||
buffer.writeln('## 환경 설정');
|
||||
final env = diagnostics['environment'] as Map<String, dynamic>;
|
||||
env.forEach((key, value) {
|
||||
buffer.writeln('- $key: $value');
|
||||
});
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
// 네트워크 상태
|
||||
if (diagnostics.containsKey('network')) {
|
||||
buffer.writeln('## 네트워크 상태');
|
||||
final network = diagnostics['network'] as Map<String, dynamic>;
|
||||
buffer.writeln('- 인터넷 연결: ${network['internetConnection'] == true ? '✅' : '❌'}');
|
||||
if (network.containsKey('apiServerReachable')) {
|
||||
buffer.writeln('- API 서버 접근: ${network['apiServerReachable'] == true ? '✅' : '❌'}');
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
// API 엔드포인트
|
||||
if (diagnostics.containsKey('apiEndpoint')) {
|
||||
buffer.writeln('## API 엔드포인트');
|
||||
final api = diagnostics['apiEndpoint'] as Map<String, dynamic>;
|
||||
if (api['skip'] == true) {
|
||||
buffer.writeln('- Mock 모드로 건너뜀');
|
||||
} else {
|
||||
buffer.writeln('- CORS 활성화: ${api['corsEnabled'] == true ? '✅' : '❌'}');
|
||||
buffer.writeln('- 로그인 엔드포인트 상태: ${api['loginEndpointStatus']}');
|
||||
if (api.containsKey('responseKeys')) {
|
||||
buffer.writeln('- 응답 키: ${api['responseKeys']}');
|
||||
}
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
// 직렬화 테스트
|
||||
if (diagnostics.containsKey('serialization')) {
|
||||
buffer.writeln('## 모델 직렬화');
|
||||
final serial = diagnostics['serialization'] as Map<String, dynamic>;
|
||||
buffer.writeln('- LoginRequest: ${serial['loginRequestValid'] == true ? '✅' : '❌'}');
|
||||
buffer.writeln('- 응답 형식 1: ${serial['format1Valid'] == true ? '✅' : '❌'}');
|
||||
buffer.writeln('- 응답 형식 2: ${serial['format2Valid'] == true ? '✅' : '❌'}');
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
// 오류 정보
|
||||
if (diagnostics.containsKey('error')) {
|
||||
buffer.writeln('## ⚠️ 오류 발생');
|
||||
buffer.writeln(diagnostics['error']);
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// 전화번호 입력 필드 관리를 위한 클래스
|
||||
// 타입 안정성 및 코드 간결성을 위해 사용
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class UserPhoneField {
|
||||
// 전화번호 종류(휴대폰, 사무실 등)
|
||||
String type;
|
||||
// 전화번호 입력 컨트롤러
|
||||
final TextEditingController controller;
|
||||
|
||||
UserPhoneField({required this.type, String? initialValue})
|
||||
: controller = TextEditingController(text: initialValue);
|
||||
|
||||
// 현재 입력된 전화번호 반환
|
||||
String get number => controller.text;
|
||||
|
||||
// 컨트롤러 해제
|
||||
void dispose() => controller.dispose();
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../custom_widgets.dart'; // AutocompleteDropdown, HighlightText 등 사용
|
||||
|
||||
// 입력 필드 + 자동완성 드롭다운을 하나로 묶은 공통 위젯
|
||||
class CategoryAutocompleteField extends StatefulWidget {
|
||||
// 입력 필드의 힌트 텍스트
|
||||
final String hintText;
|
||||
// 현재 선택된 값
|
||||
final String value;
|
||||
// 항목 리스트
|
||||
final List<String> items;
|
||||
// 필수 입력 여부
|
||||
final bool isRequired;
|
||||
// 선택 시 콜백
|
||||
final void Function(String) onSelect;
|
||||
// 입력값 변경 시 콜백(옵션)
|
||||
final void Function(String)? onChanged;
|
||||
// 비활성화 여부
|
||||
final bool enabled;
|
||||
|
||||
const CategoryAutocompleteField({
|
||||
super.key,
|
||||
required this.hintText,
|
||||
required this.value,
|
||||
required this.items,
|
||||
required this.onSelect,
|
||||
this.isRequired = false,
|
||||
this.onChanged,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CategoryAutocompleteField> createState() =>
|
||||
_CategoryAutocompleteFieldState();
|
||||
}
|
||||
|
||||
class _CategoryAutocompleteFieldState extends State<CategoryAutocompleteField> {
|
||||
// 텍스트 입력 컨트롤러
|
||||
late final TextEditingController _controller;
|
||||
// 포커스 노드
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
// 드롭다운 표시 여부
|
||||
bool _showDropdown = false;
|
||||
// 필터링된 항목 리스트
|
||||
List<String> _filteredItems = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.value);
|
||||
_filteredItems = List.from(widget.items);
|
||||
_controller.addListener(_onTextChanged);
|
||||
_focusNode.addListener(() {
|
||||
setState(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_showDropdown = _filteredItems.isNotEmpty;
|
||||
} else {
|
||||
_showDropdown = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant CategoryAutocompleteField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.value != _controller.text) {
|
||||
_controller.text = widget.value;
|
||||
}
|
||||
if (widget.items != oldWidget.items) {
|
||||
_filteredItems = List.from(widget.items);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 입력값 변경 시 필터링
|
||||
void _onTextChanged() {
|
||||
final String text = _controller.text;
|
||||
setState(() {
|
||||
if (text.isEmpty) {
|
||||
_filteredItems = List.from(widget.items);
|
||||
} else {
|
||||
_filteredItems =
|
||||
widget.items
|
||||
.where(
|
||||
(item) => item.toLowerCase().contains(text.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
// 시작 부분이 일치하는 항목 우선 정렬
|
||||
_filteredItems.sort((a, b) {
|
||||
bool aStartsWith = a.toLowerCase().startsWith(text.toLowerCase());
|
||||
bool bStartsWith = b.toLowerCase().startsWith(text.toLowerCase());
|
||||
if (aStartsWith && !bStartsWith) return -1;
|
||||
if (!aStartsWith && bStartsWith) return 1;
|
||||
return a.compareTo(b);
|
||||
});
|
||||
}
|
||||
_showDropdown = _filteredItems.isNotEmpty && _focusNode.hasFocus;
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 항목 선택 시 처리
|
||||
void _handleSelect(String value) {
|
||||
setState(() {
|
||||
_controller.text = value;
|
||||
_showDropdown = false;
|
||||
});
|
||||
widget.onSelect(value);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
suffixIcon:
|
||||
_controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed:
|
||||
widget.enabled
|
||||
? () {
|
||||
setState(() {
|
||||
_controller.clear();
|
||||
_filteredItems = List.from(widget.items);
|
||||
_showDropdown = _focusNode.hasFocus;
|
||||
widget.onSelect('');
|
||||
});
|
||||
}
|
||||
: null,
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
onPressed:
|
||||
widget.enabled
|
||||
? () {
|
||||
setState(() {
|
||||
_showDropdown = !_showDropdown;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
enabled: widget.enabled,
|
||||
validator: (value) {
|
||||
if (widget.isRequired && (value == null || value.isEmpty)) {
|
||||
return '${widget.hintText}를 선택해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (!_showDropdown) {
|
||||
_showDropdown = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
AutocompleteDropdown(
|
||||
items: _filteredItems,
|
||||
inputText: _controller.text,
|
||||
onSelect: _handleSelect,
|
||||
showDropdown: _showDropdown,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// 회사/지점 드롭다운 공통 위젯
|
||||
// 여러 도메인에서 재사용 가능
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../models/company_model.dart';
|
||||
|
||||
class CompanyBranchDropdown extends StatelessWidget {
|
||||
final List<Company> companies;
|
||||
final int? selectedCompanyId;
|
||||
final int? selectedBranchId;
|
||||
final List<Branch> branches;
|
||||
final void Function(int? companyId) onCompanyChanged;
|
||||
final void Function(int? branchId) onBranchChanged;
|
||||
|
||||
const CompanyBranchDropdown({
|
||||
super.key,
|
||||
required this.companies,
|
||||
required this.selectedCompanyId,
|
||||
required this.selectedBranchId,
|
||||
required this.branches,
|
||||
required this.onCompanyChanged,
|
||||
required this.onBranchChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 회사 드롭다운
|
||||
DropdownButtonFormField<int>(
|
||||
value: selectedCompanyId,
|
||||
decoration: const InputDecoration(hintText: '소속 회사를 선택하세요'),
|
||||
items:
|
||||
companies
|
||||
.map(
|
||||
(company) => DropdownMenuItem<int>(
|
||||
value: company.id,
|
||||
child: Text(company.name),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: onCompanyChanged,
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return '소속 회사를 선택해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 지점 드롭다운 (지점이 있을 때만)
|
||||
if (branches.isNotEmpty)
|
||||
DropdownButtonFormField<int>(
|
||||
value: selectedBranchId,
|
||||
decoration: const InputDecoration(hintText: '소속 지점을 선택하세요'),
|
||||
items:
|
||||
branches
|
||||
.map(
|
||||
(branch) => DropdownMenuItem<int>(
|
||||
value: branch.id,
|
||||
child: Text(branch.name),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: onBranchChanged,
|
||||
validator: (value) {
|
||||
if (branches.isNotEmpty && value == null) {
|
||||
return '소속 지점을 선택해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../injection_container.dart';
|
||||
import 'controllers/rent_controller.dart';
|
||||
import 'rent_form_dialog.dart';
|
||||
|
||||
class RentListScreen extends StatefulWidget {
|
||||
const RentListScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RentListScreen> createState() => _RentListScreenState();
|
||||
}
|
||||
|
||||
class _RentListScreenState extends State<RentListScreen> {
|
||||
late final RentController _controller;
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = getIt<RentController>();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
await _controller.loadRents();
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
await _controller.loadRents(refresh: true);
|
||||
}
|
||||
|
||||
void _showCreateDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => RentFormDialog(
|
||||
onSubmit: (request) async {
|
||||
final success = await _controller.createRent(
|
||||
equipmentHistoryId: request.equipmentHistoryId,
|
||||
startedAt: request.startedAt,
|
||||
endedAt: request.endedAt,
|
||||
);
|
||||
if (success && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('임대 계약이 생성되었습니다')),
|
||||
);
|
||||
}
|
||||
return success;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSearch(String query) {
|
||||
_controller.loadRents(search: query.isEmpty ? null : query);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ChangeNotifierProvider.value(
|
||||
value: _controller,
|
||||
child: Consumer<RentController>(
|
||||
builder: (context, controller, child) {
|
||||
return Column(
|
||||
children: [
|
||||
// 헤더
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'임대 관리',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const Spacer(),
|
||||
// 검색 필드
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: TextFormField(
|
||||
controller: _searchController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '검색...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onFieldSubmitted: _onSearch,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 새로고침 버튼
|
||||
IconButton(
|
||||
onPressed: _refresh,
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: '새로고침',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showCreateDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('새 임대'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 콘텐츠
|
||||
Expanded(
|
||||
child: controller.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: controller.hasError
|
||||
? Center(child: Text('오류: ${controller.error}'))
|
||||
: controller.rents.isEmpty
|
||||
? const Center(child: Text('임대 계약이 없습니다'))
|
||||
: ListView.builder(
|
||||
itemCount: controller.rents.length,
|
||||
itemBuilder: (context, index) {
|
||||
final rent = controller.rents[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
child: Text(
|
||||
'${rent.id ?? 0}',
|
||||
),
|
||||
),
|
||||
title: Text('임대 #${rent.id ?? 0}'),
|
||||
subtitle: Text(
|
||||
'${_formatDate(rent.startedAt)} ~ ${_formatDate(rent.endedAt)}',
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${_calculateDays(rent.startedAt, rent.endedAt)}일',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
controller.getRentStatusDisplayName(controller.getRentStatus(rent)),
|
||||
style: TextStyle(
|
||||
color: _getStatusColor(controller.getRentStatus(rent)),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status) {
|
||||
case '진행중':
|
||||
return Colors.blue;
|
||||
case '종료':
|
||||
return Colors.green;
|
||||
case '예약':
|
||||
return Colors.orange;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
int _calculateDays(DateTime startDate, DateTime endDate) {
|
||||
return endDate.difference(startDate).inDays;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/// 장비 정보 표시를 위한 헬퍼 클래스 (SRP, 재사용성, 테스트 용이성 중심)
|
||||
class EquipmentDisplayHelper {
|
||||
/// 제조사명 포맷팅 (빈 값은 대시로 표시)
|
||||
static String formatManufacturer(String? manufacturer) {
|
||||
if (manufacturer == null || manufacturer.isEmpty) return '-';
|
||||
return manufacturer;
|
||||
}
|
||||
|
||||
/// 장비명 포맷팅 (빈 값은 대시로 표시)
|
||||
static String formatEquipmentName(String? name) {
|
||||
if (name == null || name.isEmpty) return '-';
|
||||
return name;
|
||||
}
|
||||
|
||||
/// 카테고리 포맷팅 (비어있지 않은 카테고리만 합침)
|
||||
static String formatCategory(
|
||||
String? category,
|
||||
String? subCategory,
|
||||
String? subSubCategory,
|
||||
) {
|
||||
final parts = [
|
||||
if (category != null && category.isNotEmpty) category,
|
||||
if (subCategory != null && subCategory.isNotEmpty) subCategory,
|
||||
if (subSubCategory != null && subSubCategory.isNotEmpty) subSubCategory,
|
||||
];
|
||||
if (parts.isEmpty) return '-';
|
||||
return parts.join(' > ');
|
||||
}
|
||||
|
||||
/// 시리얼 번호 포맷팅 (없으면 대시)
|
||||
static String formatSerialNumber(String? serialNumber) {
|
||||
return serialNumber?.isNotEmpty == true ? serialNumber! : '-';
|
||||
}
|
||||
|
||||
/// 날짜 포맷팅 (YYYY-MM-DD, null이면 대시)
|
||||
static String formatDate(DateTime? date) {
|
||||
if (date == null) return '-';
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// 한국 사업자 번호 자동 포맷팅 (000-00-00000)
|
||||
class BusinessNumberFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
// 숫자만 추출
|
||||
final digitsOnly = newValue.text.replaceAll(RegExp(r'[^\d]'), '');
|
||||
|
||||
// 최대 10자리 제한 (000-00-00000 = 10자리)
|
||||
final truncated = digitsOnly.length > 10
|
||||
? digitsOnly.substring(0, 10)
|
||||
: digitsOnly;
|
||||
|
||||
// 포맷팅
|
||||
String formatted = '';
|
||||
for (int i = 0; i < truncated.length; i++) {
|
||||
if ((i == 3 || i == 5) && i < truncated.length) {
|
||||
formatted += '-';
|
||||
}
|
||||
formatted += truncated[i];
|
||||
}
|
||||
|
||||
// 커서 위치 계산
|
||||
int cursorPosition = formatted.length;
|
||||
|
||||
// 백스페이스 처리: 하이픈 앞에서 백스페이스를 누르면 하이픈도 함께 삭제
|
||||
if (oldValue.text.length > newValue.text.length) {
|
||||
if (newValue.selection.baseOffset == 4 || newValue.selection.baseOffset == 7) {
|
||||
formatted = formatted.substring(0, formatted.length - 1);
|
||||
cursorPosition = formatted.length;
|
||||
}
|
||||
}
|
||||
|
||||
// 입력 중 커서 위치 조정
|
||||
if (newValue.selection.baseOffset < newValue.text.length) {
|
||||
final beforeCursor = newValue.text.substring(0, newValue.selection.baseOffset);
|
||||
final digitsBeforeCursor = beforeCursor.replaceAll(RegExp(r'[^\d]'), '').length;
|
||||
|
||||
cursorPosition = 0;
|
||||
int digitCount = 0;
|
||||
for (int i = 0; i < formatted.length; i++) {
|
||||
if (formatted[i] != '-') {
|
||||
digitCount++;
|
||||
}
|
||||
cursorPosition++;
|
||||
if (digitCount == digitsBeforeCursor) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TextEditingValue(
|
||||
text: formatted,
|
||||
selection: TextSelection.collapsed(offset: cursorPosition),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 사업자 번호 유효성 검증
|
||||
class BusinessNumberValidator {
|
||||
/// 사업자 번호 체크섬 검증
|
||||
/// 대한민국 사업자등록번호 검증 알고리즘 사용
|
||||
static bool isValid(String businessNumber) {
|
||||
// 하이픈 제거
|
||||
final digitsOnly = businessNumber.replaceAll(RegExp(r'[^\d]'), '');
|
||||
|
||||
// 10자리가 아니면 무효
|
||||
if (digitsOnly.length != 10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 체크섬 계산을 위한 가중치
|
||||
const weights = [1, 3, 7, 1, 3, 7, 1, 3, 5];
|
||||
|
||||
int sum = 0;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
sum += int.parse(digitsOnly[i]) * weights[i];
|
||||
}
|
||||
|
||||
// 9번째 자리(5)에 대한 추가 계산
|
||||
sum += (int.parse(digitsOnly[8]) * 5) ~/ 10;
|
||||
|
||||
// 체크섬 계산
|
||||
final checksum = (10 - (sum % 10)) % 10;
|
||||
|
||||
// 마지막 자리와 체크섬 비교
|
||||
return checksum == int.parse(digitsOnly[9]);
|
||||
}
|
||||
|
||||
/// 사업자 번호 유효성 검증 (폼 필드용)
|
||||
static String? validate(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '사업자 번호를 입력하세요';
|
||||
}
|
||||
|
||||
final digitsOnly = value.replaceAll(RegExp(r'[^\d]'), '');
|
||||
|
||||
if (digitsOnly.length != 10) {
|
||||
return '사업자 번호는 10자리여야 합니다';
|
||||
}
|
||||
|
||||
if (!isValid(value)) {
|
||||
return '유효하지 않은 사업자 번호입니다';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 사업자 번호 타입 추출
|
||||
static String getBusinessType(String businessNumber) {
|
||||
final digitsOnly = businessNumber.replaceAll(RegExp(r'[^\d]'), '');
|
||||
|
||||
if (digitsOnly.length < 5) return '알 수 없음';
|
||||
|
||||
final typeCode = digitsOnly.substring(3, 5);
|
||||
final typeNum = int.parse(typeCode);
|
||||
|
||||
// 사업자 유형 분류 (국세청 기준)
|
||||
if (typeNum >= 1 && typeNum <= 79) {
|
||||
return '개인사업자';
|
||||
} else if (typeNum >= 80 && typeNum <= 89) {
|
||||
return '법인사업자';
|
||||
} else if (typeNum >= 90 && typeNum <= 99) {
|
||||
return '기타';
|
||||
}
|
||||
|
||||
return '알 수 없음';
|
||||
}
|
||||
|
||||
/// 지역 코드 추출 (첫 3자리 기준)
|
||||
static String? getRegionFromBusinessNumber(String businessNumber) {
|
||||
final digitsOnly = businessNumber.replaceAll(RegExp(r'[^\d]'), '');
|
||||
|
||||
if (digitsOnly.length < 3) return null;
|
||||
|
||||
final regionCode = digitsOnly.substring(0, 3);
|
||||
final code = int.parse(regionCode);
|
||||
|
||||
// 국세청 지역 코드 매핑 (주요 지역만)
|
||||
if (code >= 101 && code <= 115) return '서울 중부';
|
||||
if (code >= 116 && code <= 123) return '서울 동부';
|
||||
if (code >= 124 && code <= 133) return '서울 서부';
|
||||
if (code >= 134 && code <= 139) return '서울 남부';
|
||||
if (code >= 140 && code <= 149) return '서울 북부';
|
||||
if (code >= 201 && code <= 209) return '부산';
|
||||
if (code >= 210 && code <= 219) return '인천';
|
||||
if (code >= 220 && code <= 229) return '경기 북부';
|
||||
if (code >= 230 && code <= 239) return '경기 남부';
|
||||
if (code >= 240 && code <= 249) return '강원';
|
||||
if (code >= 301 && code <= 309) return '대전';
|
||||
if (code >= 310 && code <= 319) return '충남';
|
||||
if (code >= 320 && code <= 329) return '충북';
|
||||
if (code >= 401 && code <= 409) return '광주';
|
||||
if (code >= 410 && code <= 419) return '전남';
|
||||
if (code >= 420 && code <= 429) return '전북';
|
||||
if (code >= 501 && code <= 509) return '대구';
|
||||
if (code >= 510 && code <= 519) return '경북';
|
||||
if (code >= 601 && code <= 609) return '울산';
|
||||
if (code >= 610 && code <= 619) return '경남';
|
||||
if (code >= 701 && code <= 709) return '제주';
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// 사용자 관련 유틸리티 함수 모음
|
||||
// 역할명 변환 등 공통 로직을 관리
|
||||
|
||||
import '../utils/constants.dart';
|
||||
|
||||
// 역할 코드 → 한글명 변환 함수
|
||||
String getRoleName(String role) {
|
||||
switch (role) {
|
||||
case UserRoles.admin:
|
||||
return '관리자';
|
||||
case UserRoles.member:
|
||||
return '일반 사용자';
|
||||
default:
|
||||
return '알 수 없음';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user