chore: 프로젝트 정리 및 문서 업데이트
- 창고 위치 폼 UI 개선 - 테스트 리포트 업데이트 - API 이슈 문서 추가 - 폼 레이아웃 템플릿 추가 - main.dart 정리 - 상수 업데이트 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
92
API_ISSUES.md
Normal file
92
API_ISSUES.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Superport API 개선 사항
|
||||||
|
|
||||||
|
## 1. 회사 목록 API에 company_types 필드 누락
|
||||||
|
|
||||||
|
### 현재 상황
|
||||||
|
- **문제 엔드포인트**:
|
||||||
|
- `GET /companies`
|
||||||
|
- `GET /companies/branches`
|
||||||
|
|
||||||
|
- **현재 응답**: company_types 필드 없음
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 95,
|
||||||
|
"name": "한국물류창고(주)",
|
||||||
|
"address": "경기도 용인시",
|
||||||
|
"contact_name": "박물류",
|
||||||
|
"contact_phone": "010-89208920",
|
||||||
|
"contact_email": "contact@naver.com",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-08-08T09:31:04.661079Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **상세 API 응답** (`GET /companies/{id}`): company_types 포함
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 86,
|
||||||
|
"name": "아이스 맨",
|
||||||
|
"company_types": ["customer", "partner"],
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 필요한 개선
|
||||||
|
1. `/companies` 엔드포인트에 company_types 필드 추가
|
||||||
|
2. `/companies/branches` 엔드포인트에 company_types 필드 추가
|
||||||
|
|
||||||
|
### 영향
|
||||||
|
- 회사 목록 화면에서 유형(고객사/파트너사) 표시 불가
|
||||||
|
- 수정 후에도 목록에 반영되지 않음
|
||||||
|
- N+1 쿼리 문제 발생 (각 회사마다 상세 API 호출 필요)
|
||||||
|
|
||||||
|
### 임시 해결책
|
||||||
|
현재 프론트엔드에서 아래와 같이 처리 중:
|
||||||
|
- 회사 목록에서는 유형을 표시하지 않거나 기본값(고객사)으로 표시
|
||||||
|
- 수정/상세 화면에서만 정확한 유형 표시
|
||||||
|
|
||||||
|
### 제안하는 API 응답 형식
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 95,
|
||||||
|
"name": "한국물류창고(주)",
|
||||||
|
"address": "경기도 용인시",
|
||||||
|
"contact_name": "박물류",
|
||||||
|
"contact_phone": "010-89208920",
|
||||||
|
"contact_email": "contact@naver.com",
|
||||||
|
"company_types": ["customer", "partner"], // 추가 필요
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-08-08T09:31:04.661079Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 장비 상태 관련 데이터베이스 오류
|
||||||
|
|
||||||
|
### 현재 상황
|
||||||
|
- **문제 엔드포인트**:
|
||||||
|
- `GET /overview/stats`
|
||||||
|
- `GET /overview/equipment-status`
|
||||||
|
|
||||||
|
- **오류 메시지**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "DATABASE_ERROR",
|
||||||
|
"message": "Database error: Query Error: error returned from database: operator does not exist: character varying = equipment_status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 원인 추정
|
||||||
|
- equipment_status 타입과 character varying 타입 간 비교 연산자 문제
|
||||||
|
- PostgreSQL에서 enum 타입 처리 오류
|
||||||
|
|
||||||
|
### 필요한 개선
|
||||||
|
- 데이터베이스 쿼리 수정 또는 타입 캐스팅 추가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**작성일**: 2025-01-09
|
||||||
|
**작성자**: Claude Code
|
||||||
|
**우선순위**: 높음 (회사 유형 표시는 핵심 기능)
|
||||||
@@ -7,6 +7,7 @@ import 'package:superport/screens/company/company_form.dart';
|
|||||||
import 'package:superport/screens/equipment/equipment_in_form.dart';
|
import 'package:superport/screens/equipment/equipment_in_form.dart';
|
||||||
import 'package:superport/screens/equipment/equipment_out_form.dart';
|
import 'package:superport/screens/equipment/equipment_out_form.dart';
|
||||||
import 'package:superport/screens/equipment/equipment_history_screen.dart';
|
import 'package:superport/screens/equipment/equipment_history_screen.dart';
|
||||||
|
import 'package:superport/screens/equipment/test_history_screen.dart';
|
||||||
import 'package:superport/screens/license/license_form.dart'; // MaintenanceFormScreen으로 사용
|
import 'package:superport/screens/license/license_form.dart'; // MaintenanceFormScreen으로 사용
|
||||||
import 'package:superport/screens/user/user_form.dart';
|
import 'package:superport/screens/user/user_form.dart';
|
||||||
import 'package:superport/screens/warehouse_location/warehouse_location_form.dart';
|
import 'package:superport/screens/warehouse_location/warehouse_location_form.dart';
|
||||||
@@ -172,6 +173,12 @@ class SuperportApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 테스트 이력 화면
|
||||||
|
case Routes.testHistory:
|
||||||
|
return MaterialPageRoute(
|
||||||
|
builder: (context) => const TestHistoryScreen(),
|
||||||
|
);
|
||||||
|
|
||||||
// 회사 관련 라우트
|
// 회사 관련 라우트
|
||||||
case Routes.companyAdd:
|
case Routes.companyAdd:
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
@@ -208,8 +215,12 @@ class SuperportApp extends StatelessWidget {
|
|||||||
|
|
||||||
// 라이센스 관련 라우트
|
// 라이센스 관련 라우트
|
||||||
case Routes.licenseAdd:
|
case Routes.licenseAdd:
|
||||||
|
final licenseId = settings.arguments as int?;
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
builder: (context) => const MaintenanceFormScreen(),
|
builder: (context) => MaintenanceFormScreen(
|
||||||
|
maintenanceId: licenseId,
|
||||||
|
isExtension: licenseId != null, // 라이선스 ID가 있으면 연장 모드
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case Routes.licenseEdit:
|
case Routes.licenseEdit:
|
||||||
final id = settings.arguments as int;
|
final id = settings.arguments as int;
|
||||||
|
|||||||
243
lib/screens/common/templates/form_layout_template.dart
Normal file
243
lib/screens/common/templates/form_layout_template.dart
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||||
|
|
||||||
|
/// 폼 화면의 일관된 레이아웃을 제공하는 템플릿 위젯
|
||||||
|
class FormLayoutTemplate extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final Widget child;
|
||||||
|
final VoidCallback? onSave;
|
||||||
|
final VoidCallback? onCancel;
|
||||||
|
final String saveButtonText;
|
||||||
|
final bool isLoading;
|
||||||
|
final bool showBottomButtons;
|
||||||
|
final Widget? customActions;
|
||||||
|
|
||||||
|
const FormLayoutTemplate({
|
||||||
|
Key? key,
|
||||||
|
required this.title,
|
||||||
|
required this.child,
|
||||||
|
this.onSave,
|
||||||
|
this.onCancel,
|
||||||
|
this.saveButtonText = '저장',
|
||||||
|
this.isLoading = false,
|
||||||
|
this.showBottomButtons = true,
|
||||||
|
this.customActions,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Color(0xFFF5F7FA),
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF1A1F36),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back_ios, color: Color(0xFF6B7280), size: 20),
|
||||||
|
onPressed: onCancel ?? () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
actions: customActions != null ? [customActions!] : null,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: Size.fromHeight(1),
|
||||||
|
child: Container(
|
||||||
|
color: Color(0xFFE5E7EB),
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: child,
|
||||||
|
bottomNavigationBar: showBottomButtons ? _buildBottomBar(context) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBottomBar(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: Color(0xFFE5E7EB), width: 1),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
|
offset: Offset(0, -2),
|
||||||
|
blurRadius: 4,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.fromLTRB(24, 16, 24, 24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ShadcnButton(
|
||||||
|
text: '취소',
|
||||||
|
onPressed: isLoading ? null : (onCancel ?? () => Navigator.of(context).pop()),
|
||||||
|
variant: ShadcnButtonVariant.secondary,
|
||||||
|
size: ShadcnButtonSize.large,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: ShadcnButton(
|
||||||
|
text: saveButtonText,
|
||||||
|
onPressed: isLoading ? null : onSave,
|
||||||
|
variant: ShadcnButtonVariant.primary,
|
||||||
|
size: ShadcnButtonSize.large,
|
||||||
|
loading: isLoading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 폼 필드를 감싸는 일관된 카드 레이아웃
|
||||||
|
class FormSection extends StatelessWidget {
|
||||||
|
final String? title;
|
||||||
|
final String? subtitle;
|
||||||
|
final List<Widget> children;
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
|
||||||
|
const FormSection({
|
||||||
|
Key? key,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
required this.children,
|
||||||
|
this.padding,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ShadcnCard(
|
||||||
|
padding: padding ?? EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (title != null) ...[
|
||||||
|
Text(
|
||||||
|
title!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF1A1F36),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (subtitle != null) ...[
|
||||||
|
SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
subtitle!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF6B7280),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Divider(color: Color(0xFFE5E7EB), height: 1),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
if (children.isNotEmpty)
|
||||||
|
...children.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final child = entry.value;
|
||||||
|
if (index < children.length - 1) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 16),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}).toList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 일관된 폼 필드 스타일
|
||||||
|
class FormFieldWrapper extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String? hint;
|
||||||
|
final bool required;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const FormFieldWrapper({
|
||||||
|
Key? key,
|
||||||
|
required this.label,
|
||||||
|
this.hint,
|
||||||
|
this.required = false,
|
||||||
|
required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color(0xFF374151),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (required)
|
||||||
|
Text(
|
||||||
|
' *',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFFEF4444),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (hint != null) ...[
|
||||||
|
SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
hint!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF6B7280),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
SizedBox(height: 8),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UI 상수 정의
|
||||||
|
class UIConstants {
|
||||||
|
static const double formPadding = 24.0;
|
||||||
|
static const double buttonHeight = 48.0;
|
||||||
|
static const double borderRadius = 8.0;
|
||||||
|
static const double cardSpacing = 16.0;
|
||||||
|
|
||||||
|
// 테이블 컬럼 너비
|
||||||
|
static const double columnWidthSmall = 100.0; // 구분, 유형
|
||||||
|
static const double columnWidthMedium = 150.0; // 일반 필드
|
||||||
|
static const double columnWidthLarge = 200.0; // 긴 텍스트
|
||||||
|
|
||||||
|
// 색상
|
||||||
|
static const Color backgroundColor = Color(0xFFF5F7FA);
|
||||||
|
static const Color cardBackground = Colors.white;
|
||||||
|
static const Color borderColor = Color(0xFFE5E7EB);
|
||||||
|
static const Color textPrimary = Color(0xFF1A1F36);
|
||||||
|
static const Color textSecondary = Color(0xFF6B7280);
|
||||||
|
static const Color textMuted = Color(0xFF9CA3AF);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'package:superport/models/address_model.dart';
|
|||||||
import 'package:superport/screens/common/widgets/address_input.dart';
|
import 'package:superport/screens/common/widgets/address_input.dart';
|
||||||
import 'package:superport/screens/common/widgets/remark_input.dart';
|
import 'package:superport/screens/common/widgets/remark_input.dart';
|
||||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||||
|
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||||
import 'controllers/warehouse_location_form_controller.dart';
|
import 'controllers/warehouse_location_form_controller.dart';
|
||||||
|
|
||||||
/// 입고지 추가/수정 폼 화면 (SRP 적용, 상태/로직 분리)
|
/// 입고지 추가/수정 폼 화면 (SRP 적용, 상태/로직 분리)
|
||||||
@@ -37,30 +38,61 @@ class _WarehouseLocationFormScreenState
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 저장 메소드
|
||||||
|
Future<void> _onSave() async {
|
||||||
|
setState(() {}); // 저장 중 상태 갱신
|
||||||
|
final success = await _controller.save();
|
||||||
|
setState(() {}); // 저장 완료 후 상태 갱신
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// 성공 메시지 표시
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(_controller.isEditMode ? '입고지가 수정되었습니다' : '입고지가 추가되었습니다'),
|
||||||
|
backgroundColor: AppThemeTailwind.success,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// 리스트 화면으로 돌아가기
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 실패 메시지 표시
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(_controller.error ?? '저장에 실패했습니다'),
|
||||||
|
backgroundColor: AppThemeTailwind.danger,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return FormLayoutTemplate(
|
||||||
appBar: AppBar(
|
title: _controller.isEditMode ? '입고지 수정' : '입고지 추가',
|
||||||
title: Text(_controller.isEditMode ? '입고지 수정' : '입고지 추가'),
|
onSave: _controller.isSaving ? null : _onSave,
|
||||||
leading: IconButton(
|
saveButtonText: '저장',
|
||||||
icon: const Icon(Icons.arrow_back),
|
isLoading: _controller.isSaving,
|
||||||
onPressed: () => Navigator.of(context).maybePop(),
|
child: Form(
|
||||||
),
|
key: _controller.formKey,
|
||||||
),
|
child: SingleChildScrollView(
|
||||||
body: SafeArea(
|
padding: const EdgeInsets.all(UIConstants.formPadding),
|
||||||
child: Form(
|
child: FormSection(
|
||||||
key: _controller.formKey,
|
title: '입고지 정보',
|
||||||
child: SingleChildScrollView(
|
subtitle: '입고지의 기본 정보를 입력하세요',
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
|
children: [
|
||||||
child: Column(
|
// 입고지명 입력
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
FormFieldWrapper(
|
||||||
children: [
|
label: '입고지명',
|
||||||
// 입고지명 입력
|
required: true,
|
||||||
TextFormField(
|
child: TextFormField(
|
||||||
controller: _controller.nameController,
|
controller: _controller.nameController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: '입고지명',
|
|
||||||
hintText: '입고지명을 입력하세요',
|
hintText: '입고지명을 입력하세요',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
@@ -69,9 +101,12 @@ class _WarehouseLocationFormScreenState
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
),
|
||||||
// 주소 입력 (공통 위젯)
|
// 주소 입력 (공통 위젯)
|
||||||
AddressInput(
|
FormFieldWrapper(
|
||||||
|
label: '주소',
|
||||||
|
required: true,
|
||||||
|
child: AddressInput(
|
||||||
initialZipCode: _controller.address.zipCode,
|
initialZipCode: _controller.address.zipCode,
|
||||||
initialRegion: _controller.address.region,
|
initialRegion: _controller.address.region,
|
||||||
initialDetailAddress: _controller.address.detailAddress,
|
initialDetailAddress: _controller.address.detailAddress,
|
||||||
@@ -88,74 +123,13 @@ class _WarehouseLocationFormScreenState
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
|
||||||
// 비고 입력
|
|
||||||
RemarkInput(controller: _controller.remarkController),
|
|
||||||
const SizedBox(height: 80), // 하단 버튼 여백 확보
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 48,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed:
|
|
||||||
_controller.isSaving
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
setState(() {}); // 저장 중 상태 갱신
|
|
||||||
final success = await _controller.save();
|
|
||||||
setState(() {}); // 저장 완료 후 상태 갱신
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// 성공 메시지 표시
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(_controller.isEditMode ? '입고지가 수정되었습니다' : '입고지가 추가되었습니다'),
|
|
||||||
backgroundColor: AppThemeTailwind.success,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// 리스트 화면으로 돌아가기
|
|
||||||
Navigator.of(context).pop(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 실패 메시지 표시
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(_controller.error ?? '저장에 실패했습니다'),
|
|
||||||
backgroundColor: AppThemeTailwind.danger,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppThemeTailwind.primary,
|
|
||||||
minimumSize: const Size.fromHeight(48),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
),
|
||||||
),
|
// 비고 입력
|
||||||
child:
|
FormFieldWrapper(
|
||||||
_controller.isSaving
|
label: '비고',
|
||||||
? const SizedBox(
|
child: RemarkInput(controller: _controller.remarkController),
|
||||||
width: 18,
|
),
|
||||||
height: 18,
|
],
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Text(
|
|
||||||
'저장',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class Routes {
|
|||||||
static const String equipmentOut = '/equipment-out'; // 출고 목록(미사용)
|
static const String equipmentOut = '/equipment-out'; // 출고 목록(미사용)
|
||||||
static const String equipmentOutAdd = '/equipment-out/add'; // 장비 출고 폼
|
static const String equipmentOutAdd = '/equipment-out/add'; // 장비 출고 폼
|
||||||
static const String equipmentHistory = '/equipment/history'; // 장비 이력 조회
|
static const String equipmentHistory = '/equipment/history'; // 장비 이력 조회
|
||||||
|
static const String testHistory = '/test/history'; // 테스트 이력 화면
|
||||||
static const String equipmentOutEdit = '/equipment-out/edit'; // 장비 출고 편집
|
static const String equipmentOutEdit = '/equipment-out/edit'; // 장비 출고 편집
|
||||||
static const String equipmentInList = '/equipment/in'; // 입고 장비 목록
|
static const String equipmentInList = '/equipment/in'; // 입고 장비 목록
|
||||||
static const String equipmentOutList = '/equipment/out'; // 출고 장비 목록
|
static const String equipmentOutList = '/equipment/out'; // 출고 장비 목록
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"testName": "장비 입고 화면 전체 기능 테스트",
|
||||||
|
"timestamp": "2025-08-08T18:31:01.566734",
|
||||||
|
"duration": 6507,
|
||||||
|
"results": {
|
||||||
|
"totalTests": 10,
|
||||||
|
"passedTests": 5,
|
||||||
|
"failedTests": 5,
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"testName": "장비 목록 조회",
|
||||||
|
"passed": true,
|
||||||
|
"error": null,
|
||||||
|
"retryCount": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"testName": "장비 검색 및 필터링",
|
||||||
|
"passed": false,
|
||||||
|
"error": "DioException [bad response]: null\nError: ServerException: 서버 오류가 발생했습니다. (code: 500)",
|
||||||
|
"retryCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"testName": "새 장비 등록",
|
||||||
|
"passed": true,
|
||||||
|
"error": null,
|
||||||
|
"retryCount": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"testName": "장비 정보 수정",
|
||||||
|
"passed": true,
|
||||||
|
"error": null,
|
||||||
|
"retryCount": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"testName": "장비 삭제",
|
||||||
|
"passed": false,
|
||||||
|
"error": "Exception: Assertion failed: 삭제된 장비가 여전히 조회됨",
|
||||||
|
"retryCount": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"testName": "장비 상태 변경",
|
||||||
|
"passed": false,
|
||||||
|
"error": "Exception: Assertion failed: 변경된 상태가 일치해야 합니다",
|
||||||
|
"retryCount": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"testName": "장비 이력 추가",
|
||||||
|
"passed": false,
|
||||||
|
"error": "Exception: Assertion failed: 이력 추가 응답 코드가 201이어야 합니다",
|
||||||
|
"retryCount": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"testName": "이미지 업로드",
|
||||||
|
"passed": true,
|
||||||
|
"error": null,
|
||||||
|
"retryCount": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"testName": "바코드 스캔 시뮬레이션",
|
||||||
|
"passed": true,
|
||||||
|
"error": null,
|
||||||
|
"retryCount": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"testName": "입고 완료 처리",
|
||||||
|
"passed": false,
|
||||||
|
"error": "Exception: Assertion failed: 입고 이력 추가 응답 코드가 201이어야 합니다",
|
||||||
|
"retryCount": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 장비 입고 화면 전체 기능 테스트 리포트
|
||||||
|
|
||||||
|
## 테스트 개요
|
||||||
|
- **실행 일시**: 2025-08-08 18:31:01.570595
|
||||||
|
- **소요 시간**: 6초
|
||||||
|
- **환경**: Production API (https://api-dev.beavercompany.co.kr)
|
||||||
|
|
||||||
|
## 테스트 결과
|
||||||
|
| 항목 | 결과 |
|
||||||
|
|------|------|
|
||||||
|
| 총 테스트 | 10개 |
|
||||||
|
| ✅ 성공 | 5개 |
|
||||||
|
| ❌ 실패 | 5개 |
|
||||||
|
| 📊 성공률 | 50.0% |
|
||||||
|
|
||||||
|
## 개별 테스트 상세
|
||||||
|
|
||||||
|
### 1. 장비 목록 조회
|
||||||
|
- **상태**: ✅ 성공
|
||||||
|
|
||||||
|
### 2. 장비 검색 및 필터링
|
||||||
|
- **상태**: ❌ 실패
|
||||||
|
- **재시도**: 3회
|
||||||
|
- **에러**: `DioException [bad response]: null
|
||||||
|
Error: ServerException: 서버 오류가 발생했습니다. (code: 500)`
|
||||||
|
|
||||||
|
### 3. 새 장비 등록
|
||||||
|
- **상태**: ✅ 성공
|
||||||
|
|
||||||
|
### 4. 장비 정보 수정
|
||||||
|
- **상태**: ✅ 성공
|
||||||
|
|
||||||
|
### 5. 장비 삭제
|
||||||
|
- **상태**: ❌ 실패
|
||||||
|
- **재시도**: 1회
|
||||||
|
- **에러**: `Exception: Assertion failed: 삭제된 장비가 여전히 조회됨`
|
||||||
|
|
||||||
|
### 6. 장비 상태 변경
|
||||||
|
- **상태**: ❌ 실패
|
||||||
|
- **재시도**: 1회
|
||||||
|
- **에러**: `Exception: Assertion failed: 변경된 상태가 일치해야 합니다`
|
||||||
|
|
||||||
|
### 7. 장비 이력 추가
|
||||||
|
- **상태**: ❌ 실패
|
||||||
|
- **재시도**: 1회
|
||||||
|
- **에러**: `Exception: Assertion failed: 이력 추가 응답 코드가 201이어야 합니다`
|
||||||
|
|
||||||
|
### 8. 이미지 업로드
|
||||||
|
- **상태**: ✅ 성공
|
||||||
|
|
||||||
|
### 9. 바코드 스캔 시뮬레이션
|
||||||
|
- **상태**: ✅ 성공
|
||||||
|
|
||||||
|
### 10. 입고 완료 처리
|
||||||
|
- **상태**: ❌ 실패
|
||||||
|
- **재시도**: 1회
|
||||||
|
- **에러**: `Exception: Assertion failed: 입고 이력 추가 응답 코드가 201이어야 합니다`
|
||||||
|
|
||||||
|
## 자동 수정 내역
|
||||||
|
|
||||||
|
이 테스트는 다음과 같은 자동 수정 기능을 포함합니다:
|
||||||
|
- 인증 토큰 만료 시 자동 재로그인
|
||||||
|
- 필수 필드 누락 시 기본값 자동 생성
|
||||||
|
- API 응답 형식 변경 감지 및 대응
|
||||||
|
- 검증 에러 발생 시 데이터 자동 수정
|
||||||
|
|
||||||
|
---
|
||||||
|
*이 리포트는 자동으로 생성되었습니다.*
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
<header class="report-header">
|
<header class="report-header">
|
||||||
<h1>🚀 Automated Test Suite</h1>
|
<h1>🚀 Automated Test Suite</h1>
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<span class="date">생성 시간: 2025-08-05 18:06:29.842386</span>
|
<span class="date">생성 시간: 2025-08-08 18:30:49.349597</span>
|
||||||
<span class="duration">소요 시간: 0초</span>
|
<span class="duration">소요 시간: 0초</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
</section>
|
</section>
|
||||||
<footer class="report-footer">
|
<footer class="report-footer">
|
||||||
<p>이 리포트는 SUPERPORT 자동화 테스트 시스템에 의해 생성되었습니다.</p>
|
<p>이 리포트는 SUPERPORT 자동화 테스트 시스템에 의해 생성되었습니다.</p>
|
||||||
<p>생성 시간: 2025-08-05 18:06:29.844246</p>
|
<p>생성 시간: 2025-08-08 18:30:49.351431</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -222,7 +222,7 @@
|
|||||||
<header class="report-header">
|
<header class="report-header">
|
||||||
<h1>🚀 Automated Test Suite</h1>
|
<h1>🚀 Automated Test Suite</h1>
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<span class="date">생성 시간: 2025-08-05 18:06:30.183513</span>
|
<span class="date">생성 시간: 2025-08-08 18:30:44.508951</span>
|
||||||
<span class="duration">소요 시간: 0초</span>
|
<span class="duration">소요 시간: 0초</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
</section>
|
</section>
|
||||||
<footer class="report-footer">
|
<footer class="report-footer">
|
||||||
<p>이 리포트는 SUPERPORT 자동화 테스트 시스템에 의해 생성되었습니다.</p>
|
<p>이 리포트는 SUPERPORT 자동화 테스트 시스템에 의해 생성되었습니다.</p>
|
||||||
<p>생성 시간: 2025-08-05 18:06:30.185789</p>
|
<p>생성 시간: 2025-08-08 18:30:44.510771</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"reportId": "TEST-1754384789851",
|
"reportId": "TEST-1754645449358",
|
||||||
"testName": "Automated Test Suite",
|
"testName": "Automated Test Suite",
|
||||||
"timestamp": "2025-08-05T18:06:29.851718",
|
"timestamp": "2025-08-08T18:30:49.359223",
|
||||||
"duration": 23,
|
"duration": 22,
|
||||||
"environment": {
|
"environment": {
|
||||||
"platform": "Flutter",
|
"platform": "Flutter",
|
||||||
"dartVersion": "3.0",
|
"dartVersion": "3.0",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"reportId": "TEST-1754384790191",
|
"reportId": "TEST-1754645444520",
|
||||||
"testName": "Automated Test Suite",
|
"testName": "Automated Test Suite",
|
||||||
"timestamp": "2025-08-05T18:06:30.192105",
|
"timestamp": "2025-08-08T18:30:44.520887",
|
||||||
"duration": 19,
|
"duration": 23,
|
||||||
"environment": {
|
"environment": {
|
||||||
"platform": "Flutter",
|
"platform": "Flutter",
|
||||||
"dartVersion": "3.0",
|
"dartVersion": "3.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 📊 테스트 실행 결과
|
## 📊 테스트 실행 결과
|
||||||
|
|
||||||
- **실행 시간**: 2025-08-05 18:06:29.828327 ~ 2025-08-05 18:06:29.850204
|
- **실행 시간**: 2025-08-08 18:30:49.336922 ~ 2025-08-08 18:30:49.357706
|
||||||
- **소요 시간**: 0초
|
- **소요 시간**: 0초
|
||||||
- **환경**: Flutter (null)
|
- **환경**: Flutter (null)
|
||||||
|
|
||||||
@@ -17,4 +17,4 @@
|
|||||||
| 성공률 | 0.0% |
|
| 성공률 | 0.0% |
|
||||||
|
|
||||||
---
|
---
|
||||||
*이 리포트는 2025-08-05 18:06:29.850411에 자동 생성되었습니다.*
|
*이 리포트는 2025-08-08 18:30:49.357945에 자동 생성되었습니다.*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 📊 테스트 실행 결과
|
## 📊 테스트 실행 결과
|
||||||
|
|
||||||
- **실행 시간**: 2025-08-05 18:06:30.172786 ~ 2025-08-05 18:06:30.191012
|
- **실행 시간**: 2025-08-08 18:30:44.497438 ~ 2025-08-08 18:30:44.518672
|
||||||
- **소요 시간**: 0초
|
- **소요 시간**: 0초
|
||||||
- **환경**: Flutter (null)
|
- **환경**: Flutter (null)
|
||||||
|
|
||||||
@@ -17,4 +17,4 @@
|
|||||||
| 성공률 | 0.0% |
|
| 성공률 | 0.0% |
|
||||||
|
|
||||||
---
|
---
|
||||||
*이 리포트는 2025-08-05 18:06:30.191191에 자동 생성되었습니다.*
|
*이 리포트는 2025-08-08 18:30:44.518887에 자동 생성되었습니다.*
|
||||||
|
|||||||
Reference in New Issue
Block a user