fix: API 응답 파싱 오류 수정 및 에러 처리 개선

주요 변경사항:
- 창고 관리 API 응답 구조와 DTO 불일치 수정
  - WarehouseLocationDto에 code, manager_phone 필드 추가
  - RemoteDataSource에서 API 응답을 DTO 구조에 맞게 변환
- 회사 관리 API 응답 파싱 오류 수정
  - CompanyResponse의 필수 필드를 nullable로 변경
  - PaginatedResponse 구조 매핑 로직 개선
- 에러 처리 및 로깅 개선
  - Service Layer에 상세 에러 로깅 추가
  - Controller에서 에러 타입별 처리
- 새로운 유틸리티 추가
  - ResponseInterceptor: API 응답 정규화
  - DebugLogger: 디버깅 도구
  - HealthCheckService: 서버 상태 확인
- 문서화
  - API 통합 테스트 가이드
  - 에러 분석 보고서
  - 리팩토링 계획서
This commit is contained in:
JiWoong Sul
2025-07-31 19:15:39 +09:00
parent ad2c699ff7
commit f08b7fec79
89 changed files with 10521 additions and 892 deletions

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:dartz/dartz.dart';
import 'package:superport/core/errors/failures.dart';
import 'package:superport/data/models/auth/login_request.dart';
import 'package:superport/di/injection_container.dart';
import 'package:superport/services/auth_service.dart';
import 'package:superport/services/health_test_service.dart';
/// 로그인 화면의 상태 및 비즈니스 로직을 담당하는 ChangeNotifier 기반 컨트롤러
class LoginController extends ChangeNotifier {
@@ -40,7 +42,7 @@ class LoginController extends ChangeNotifier {
Future<bool> login() async {
// 입력값 검증
if (idController.text.trim().isEmpty) {
_errorMessage = '이메일을 입력해주세요.';
_errorMessage = '아이디 또는 이메일을 입력해주세요.';
notifyListeners();
return false;
}
@@ -51,13 +53,10 @@ class LoginController extends ChangeNotifier {
return false;
}
// 이메일 형식 검증
// 입력값이 이메일인지 username인지 판단
final inputValue = idController.text.trim();
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(idController.text.trim())) {
_errorMessage = '올바른 이메일 형식이 아닙니다.';
notifyListeners();
return false;
}
final isEmail = emailRegex.hasMatch(inputValue);
// 로딩 시작
_isLoading = true;
@@ -65,29 +64,103 @@ class LoginController extends ChangeNotifier {
notifyListeners();
try {
// 로그인 요청
// 로그인 요청 (이메일 또는 username으로)
final request = LoginRequest(
email: idController.text.trim(),
email: isEmail ? inputValue : null,
username: !isEmail ? inputValue : null,
password: pwController.text,
);
final result = await _authService.login(request);
print('[LoginController] 로그인 요청 시작: ${isEmail ? 'email: ${request.email}' : 'username: ${request.username}'}');
print('[LoginController] 요청 데이터: ${request.toJson()}');
final result = await _authService.login(request).timeout(
const Duration(seconds: 10),
onTimeout: () async {
print('[LoginController] 로그인 요청 타임아웃 (10초)');
return Left(NetworkFailure(message: '요청 시간이 초과되었습니다. 네트워크 연결을 확인해주세요.'));
},
);
print('[LoginController] 로그인 결과 수신: ${result.isRight() ? '성공' : '실패'}');
return result.fold(
(failure) {
print('[LoginController] 로그인 실패: ${failure.message}');
_errorMessage = failure.message;
_isLoading = false;
notifyListeners();
return false;
},
(loginResponse) {
(loginResponse) async {
print('[LoginController] 로그인 성공: ${loginResponse.user.email}');
// Health Test 실행
try {
print('[LoginController] ========== Health Test 시작 ==========');
final healthTestService = HealthTestService();
final testResults = await healthTestService.checkAllEndpoints();
// 상세한 결과 출력
print('\n[LoginController] === 인증 상태 ===');
print('인증됨: ${testResults['auth']?['success']}');
print('Access Token: ${testResults['auth']?['accessToken'] == true ? '있음' : '없음'}');
print('Refresh Token: ${testResults['auth']?['refreshToken'] == true ? '있음' : '없음'}');
print('\n[LoginController] === 대시보드 API ===');
print('Overview Stats: ${testResults['dashboard_stats']?['success'] == true ? '✅ 성공' : '❌ 실패'}');
if (testResults['dashboard_stats']?['error'] != null) {
print(' 에러: ${testResults['dashboard_stats']['error']}');
}
if (testResults['dashboard_stats']?['data'] != null) {
print(' 데이터: ${testResults['dashboard_stats']['data']}');
}
print('\n[LoginController] === 장비 상태 분포 ===');
print('Equipment Status: ${testResults['equipment_status_distribution']?['success'] == true ? '✅ 성공' : '❌ 실패'}');
if (testResults['equipment_status_distribution']?['error'] != null) {
print(' 에러: ${testResults['equipment_status_distribution']['error']}');
}
if (testResults['equipment_status_distribution']?['data'] != null) {
print(' 데이터: ${testResults['equipment_status_distribution']['data']}');
}
print('\n[LoginController] === 장비 목록 ===');
print('Equipments: ${testResults['equipments']?['success'] == true ? '✅ 성공' : '❌ 실패'}');
if (testResults['equipments']?['error'] != null) {
print(' 에러: ${testResults['equipments']['error']}');
}
if (testResults['equipments']?['sample'] != null) {
print(' 샘플: ${testResults['equipments']['sample']}');
}
print('\n[LoginController] === 입고지 ===');
print('Warehouses: ${testResults['warehouses']?['success'] == true ? '✅ 성공' : '❌ 실패'}');
if (testResults['warehouses']?['error'] != null) {
print(' 에러: ${testResults['warehouses']['error']}');
}
print('\n[LoginController] === 회사 ===');
print('Companies: ${testResults['companies']?['success'] == true ? '✅ 성공' : '❌ 실패'}');
if (testResults['companies']?['error'] != null) {
print(' 에러: ${testResults['companies']['error']}');
}
print('\n[LoginController] ========== Health Test 완료 ==========\n');
} catch (e, stackTrace) {
print('[LoginController] Health Test 오류: $e');
print('[LoginController] Stack Trace: $stackTrace');
}
_isLoading = false;
notifyListeners();
return true;
},
);
} catch (e) {
_errorMessage = '로그인 중 오류가 발생했습니다.';
} catch (e, stackTrace) {
print('[LoginController] 로그인 예외 발생: $e');
print('[LoginController] 스택 트레이스: $stackTrace');
_errorMessage = '로그인 중 오류가 발생했습니다: ${e.toString()}';
_isLoading = false;
notifyListeners();
return false;

View File

@@ -130,7 +130,7 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
),
boxShadow: [
BoxShadow(
color: ShadcnTheme.gradient1.withOpacity(0.3),
color: ShadcnTheme.gradient1.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
@@ -186,10 +186,10 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
),
const SizedBox(height: ShadcnTheme.spacing8),
// 사용자명 입력
// 아이디/이메일 입력
ShadcnInput(
label: '사용자명',
placeholder: '사용자명을 입력하세요',
label: '아이디/이메일',
placeholder: '아이디 또는 이메일을 입력하세요',
controller: controller.idController,
prefixIcon: const Icon(Icons.person_outline),
keyboardType: TextInputType.text,
@@ -229,10 +229,10 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
padding: const EdgeInsets.all(ShadcnTheme.spacing3),
margin: const EdgeInsets.only(bottom: ShadcnTheme.spacing4),
decoration: BoxDecoration(
color: ShadcnTheme.destructive.withOpacity(0.1),
color: ShadcnTheme.destructive.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
border: Border.all(
color: ShadcnTheme.destructive.withOpacity(0.3),
color: ShadcnTheme.destructive.withValues(alpha: 0.3),
),
),
child: Row(
@@ -270,8 +270,13 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
// 테스트 로그인 버튼
ShadcnButton(
text: '테스트 로그인',
onPressed: () {
widget.onLoginSuccess();
onPressed: () async {
// 테스트 계정 정보 자동 입력
widget.controller.idController.text = 'admin@superport.kr';
widget.controller.pwController.text = 'admin123!';
// 실제 로그인 프로세스 실행
await _handleLogin();
},
variant: ShadcnButtonVariant.secondary,
size: ShadcnButtonSize.medium,
@@ -298,7 +303,7 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
Container(
padding: const EdgeInsets.all(ShadcnTheme.spacing2),
decoration: BoxDecoration(
color: ShadcnTheme.info.withOpacity(0.1),
color: ShadcnTheme.info.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
),
child: Icon(
@@ -323,7 +328,7 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
Text(
'Copyright 2025 NatureBridgeAI. All rights reserved.',
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.foreground.withOpacity(0.7),
color: ShadcnTheme.foreground.withValues(alpha: 0.7),
fontWeight: FontWeight.w500,
),
),