전역 구조 리팩터링 및 테스트 확장

This commit is contained in:
JiWoong Sul
2025-09-29 01:51:47 +09:00
parent c00c0c9ab2
commit fef7108479
70 changed files with 7709 additions and 3185 deletions

View File

@@ -0,0 +1,274 @@
import 'package:superport_v2/core/common/models/paginated_result.dart';
import 'package:superport_v2/core/common/utils/json_utils.dart';
import '../../domain/entities/stock_transaction.dart';
/// 재고 트랜잭션 DTO
///
/// - API 응답(JSON)을 도메인 엔티티로 변환하고, 요청 페이로드를 구성한다.
class StockTransactionDto {
StockTransactionDto({
this.id,
required this.transactionNo,
required this.transactionDate,
required this.type,
required this.status,
required this.warehouse,
required this.createdBy,
this.note,
this.isActive = true,
this.createdAt,
this.updatedAt,
this.lines = const [],
this.customers = const [],
this.approval,
this.expectedReturnDate,
});
final int? id;
final String transactionNo;
final DateTime transactionDate;
final StockTransactionType type;
final StockTransactionStatus status;
final StockTransactionWarehouse warehouse;
final StockTransactionEmployee createdBy;
final String? note;
final bool isActive;
final DateTime? createdAt;
final DateTime? updatedAt;
final List<StockTransactionLine> lines;
final List<StockTransactionCustomer> customers;
final StockTransactionApprovalSummary? approval;
final DateTime? expectedReturnDate;
/// JSON 객체를 DTO로 변환한다.
factory StockTransactionDto.fromJson(Map<String, dynamic> json) {
final typeJson = json['transaction_type'] as Map<String, dynamic>?;
final statusJson = json['transaction_status'] as Map<String, dynamic>?;
final warehouseJson = json['warehouse'] as Map<String, dynamic>?;
final createdByJson = json['created_by'] as Map<String, dynamic>?;
return StockTransactionDto(
id: json['id'] as int?,
transactionNo: json['transaction_no'] as String? ?? '',
transactionDate: _parseDate(json['transaction_date']) ?? DateTime.now(),
type: _parseType(typeJson),
status: _parseStatus(statusJson),
warehouse: _parseWarehouse(warehouseJson),
createdBy: _parseEmployee(createdByJson),
note: json['note'] as String?,
isActive: (json['is_active'] as bool?) ?? true,
createdAt: _parseDateTime(json['created_at']),
updatedAt: _parseDateTime(json['updated_at']),
lines: _parseLines(json),
customers: _parseCustomers(json),
approval: _parseApproval(json['approval']),
expectedReturnDate:
_parseDate(json['expected_return_date']) ??
_parseDate(json['planned_return_date']) ??
_parseDate(json['return_due_date']),
);
}
/// 도메인 엔티티로 변환한다.
StockTransaction toEntity() {
return StockTransaction(
id: id,
transactionNo: transactionNo,
transactionDate: transactionDate,
type: type,
status: status,
warehouse: warehouse,
createdBy: createdBy,
note: note,
isActive: isActive,
createdAt: createdAt,
updatedAt: updatedAt,
lines: lines,
customers: customers,
approval: approval,
expectedReturnDate: expectedReturnDate,
);
}
/// 페이지네이션 응답을 파싱한다.
static PaginatedResult<StockTransaction> parsePaginated(dynamic json) {
final raw = JsonUtils.extractList(json, keys: const ['items']);
final items = raw
.map(StockTransactionDto.fromJson)
.map((dto) => dto.toEntity())
.toList(growable: false);
final map = json is Map<String, dynamic> ? json : <String, dynamic>{};
return PaginatedResult<StockTransaction>(
items: items,
page: JsonUtils.readInt(map, 'page', fallback: 1),
pageSize: JsonUtils.readInt(map, 'page_size', fallback: items.length),
total: JsonUtils.readInt(map, 'total', fallback: items.length),
);
}
/// 단건 응답을 파싱한다.
static StockTransaction? parseSingle(dynamic json) {
final map = JsonUtils.extractMap(json, keys: const ['data']);
if (map.isEmpty) {
return null;
}
return StockTransactionDto.fromJson(map).toEntity();
}
}
StockTransactionType _parseType(Map<String, dynamic>? json) {
return StockTransactionType(
id: json?['id'] as int? ?? 0,
name: json?['type_name'] as String? ?? '',
);
}
StockTransactionStatus _parseStatus(Map<String, dynamic>? json) {
return StockTransactionStatus(
id: json?['id'] as int? ?? 0,
name: json?['status_name'] as String? ?? '',
);
}
StockTransactionWarehouse _parseWarehouse(Map<String, dynamic>? json) {
final zipcode = json?['zipcode'] as Map<String, dynamic>?;
return StockTransactionWarehouse(
id: json?['id'] as int? ?? 0,
code: json?['warehouse_code'] as String? ?? '',
name: json?['warehouse_name'] as String? ?? '',
zipcode: zipcode?['zipcode'] as String?,
addressLine: zipcode?['road_name'] as String?,
);
}
StockTransactionEmployee _parseEmployee(Map<String, dynamic>? json) {
return StockTransactionEmployee(
id: json?['id'] as int? ?? 0,
employeeNo: json?['employee_no'] as String? ?? '',
name: json?['employee_name'] as String? ?? '',
);
}
List<StockTransactionLine> _parseLines(Map<String, dynamic> json) {
final raw = JsonUtils.extractList(json, keys: const ['lines']);
return [
for (final item in raw)
StockTransactionLine(
id: item['id'] as int?,
lineNo: JsonUtils.readInt(item, 'line_no', fallback: 1),
product: _parseProduct(item['product'] as Map<String, dynamic>?),
quantity: JsonUtils.readInt(item, 'quantity', fallback: 0),
unitPrice: _readDouble(item['unit_price']),
note: item['note'] as String?,
),
];
}
StockTransactionProduct _parseProduct(Map<String, dynamic>? json) {
final vendorJson = json?['vendor'] as Map<String, dynamic>?;
final uomJson = json?['uom'] as Map<String, dynamic>?;
return StockTransactionProduct(
id: json?['id'] as int? ?? 0,
code: json?['product_code'] as String? ?? json?['code'] as String? ?? '',
name: json?['product_name'] as String? ?? json?['name'] as String? ?? '',
vendor: vendorJson == null
? null
: StockTransactionVendorSummary(
id: vendorJson['id'] as int? ?? 0,
name:
vendorJson['vendor_name'] as String? ??
vendorJson['name'] as String? ??
'',
),
uom: uomJson == null
? null
: StockTransactionUomSummary(
id: uomJson['id'] as int? ?? 0,
name:
uomJson['uom_name'] as String? ??
uomJson['name'] as String? ??
'',
),
);
}
List<StockTransactionCustomer> _parseCustomers(Map<String, dynamic> json) {
final raw = JsonUtils.extractList(json, keys: const ['customers']);
return [
for (final item in raw)
StockTransactionCustomer(
id: item['id'] as int?,
customer: _parseCustomer(item['customer'] as Map<String, dynamic>?),
note: item['note'] as String?,
),
];
}
StockTransactionCustomerSummary _parseCustomer(Map<String, dynamic>? json) {
return StockTransactionCustomerSummary(
id: json?['id'] as int? ?? 0,
code: json?['customer_code'] as String? ?? json?['code'] as String? ?? '',
name: json?['customer_name'] as String? ?? json?['name'] as String? ?? '',
);
}
StockTransactionApprovalSummary? _parseApproval(dynamic raw) {
if (raw is! Map<String, dynamic>) {
return null;
}
final status = raw['approval_status'] as Map<String, dynamic>?;
return StockTransactionApprovalSummary(
id: raw['id'] as int? ?? 0,
approvalNo: raw['approval_no'] as String? ?? '',
status: status == null
? null
: StockTransactionApprovalStatusSummary(
id: status['id'] as int? ?? 0,
name:
status['status_name'] as String? ??
status['name'] as String? ??
'',
isBlocking: status['is_blocking_next'] as bool?,
),
);
}
DateTime? _parseDate(Object? value) {
if (value == null) {
return null;
}
if (value is DateTime) {
return value;
}
if (value is String && value.isNotEmpty) {
return DateTime.tryParse(value);
}
return null;
}
DateTime? _parseDateTime(Object? value) {
if (value == null) {
return null;
}
if (value is DateTime) {
return value;
}
if (value is String && value.isNotEmpty) {
return DateTime.tryParse(value);
}
return null;
}
double _readDouble(Object? value) {
if (value is double) {
return value;
}
if (value is int) {
return value.toDouble();
}
if (value is String) {
return double.tryParse(value) ?? 0;
}
return 0;
}

View File

@@ -0,0 +1,233 @@
/// 재고 트랜잭션 도메인 엔티티
///
/// - 입고/출고/대여 공통으로 사용되는 헤더와 라인, 고객 연결 정보를 포함한다.
class StockTransaction {
StockTransaction({
this.id,
required this.transactionNo,
required this.transactionDate,
required this.type,
required this.status,
required this.warehouse,
required this.createdBy,
this.note,
this.isActive = true,
this.createdAt,
this.updatedAt,
this.lines = const [],
this.customers = const [],
this.approval,
this.expectedReturnDate,
});
final int? id;
final String transactionNo;
final DateTime transactionDate;
final StockTransactionType type;
final StockTransactionStatus status;
final StockTransactionWarehouse warehouse;
final StockTransactionEmployee createdBy;
final String? note;
final bool isActive;
final DateTime? createdAt;
final DateTime? updatedAt;
final List<StockTransactionLine> lines;
final List<StockTransactionCustomer> customers;
final StockTransactionApprovalSummary? approval;
final DateTime? expectedReturnDate;
int get itemCount => lines.length;
int get totalQuantity => lines.fold<int>(
0,
(previousValue, line) => previousValue + line.quantity,
);
StockTransaction copyWith({
int? id,
String? transactionNo,
DateTime? transactionDate,
StockTransactionType? type,
StockTransactionStatus? status,
StockTransactionWarehouse? warehouse,
StockTransactionEmployee? createdBy,
String? note,
bool? isActive,
DateTime? createdAt,
DateTime? updatedAt,
List<StockTransactionLine>? lines,
List<StockTransactionCustomer>? customers,
StockTransactionApprovalSummary? approval,
DateTime? expectedReturnDate,
}) {
return StockTransaction(
id: id ?? this.id,
transactionNo: transactionNo ?? this.transactionNo,
transactionDate: transactionDate ?? this.transactionDate,
type: type ?? this.type,
status: status ?? this.status,
warehouse: warehouse ?? this.warehouse,
createdBy: createdBy ?? this.createdBy,
note: note ?? this.note,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
lines: lines ?? this.lines,
customers: customers ?? this.customers,
approval: approval ?? this.approval,
expectedReturnDate: expectedReturnDate ?? this.expectedReturnDate,
);
}
}
/// 재고 트랜잭션 유형 요약 정보
class StockTransactionType {
StockTransactionType({required this.id, required this.name});
final int id;
final String name;
}
/// 재고 트랜잭션 상태 요약 정보
class StockTransactionStatus {
StockTransactionStatus({required this.id, required this.name});
final int id;
final String name;
}
/// 재고 트랜잭션 작성자 정보
class StockTransactionEmployee {
StockTransactionEmployee({
required this.id,
required this.employeeNo,
required this.name,
});
final int id;
final String employeeNo;
final String name;
}
/// 재고 트랜잭션 창고 정보 요약
class StockTransactionWarehouse {
StockTransactionWarehouse({
required this.id,
required this.code,
required this.name,
this.zipcode,
this.addressLine,
});
final int id;
final String code;
final String name;
final String? zipcode;
final String? addressLine;
}
/// 재고 트랜잭션 품목(라인)
class StockTransactionLine {
StockTransactionLine({
this.id,
required this.lineNo,
required this.product,
required this.quantity,
required this.unitPrice,
this.note,
});
final int? id;
final int lineNo;
final StockTransactionProduct product;
final int quantity;
final double unitPrice;
final String? note;
}
/// 재고 트랜잭션 품목의 제품 정보 요약
class StockTransactionProduct {
StockTransactionProduct({
required this.id,
required this.code,
required this.name,
this.vendor,
this.uom,
});
final int id;
final String code;
final String name;
final StockTransactionVendorSummary? vendor;
final StockTransactionUomSummary? uom;
}
/// 재고 트랜잭션에 연결된 고객 정보
class StockTransactionCustomer {
StockTransactionCustomer({this.id, required this.customer, this.note});
final int? id;
final StockTransactionCustomerSummary customer;
final String? note;
}
/// 고객 요약 정보
class StockTransactionCustomerSummary {
StockTransactionCustomerSummary({
required this.id,
required this.code,
required this.name,
});
final int id;
final String code;
final String name;
}
/// 제품의 공급사 요약 정보
class StockTransactionVendorSummary {
StockTransactionVendorSummary({required this.id, required this.name});
final int id;
final String name;
}
/// 제품 단위 요약 정보
class StockTransactionUomSummary {
StockTransactionUomSummary({required this.id, required this.name});
final int id;
final String name;
}
/// 결재 요약 정보
class StockTransactionApprovalSummary {
StockTransactionApprovalSummary({
required this.id,
required this.approvalNo,
this.status,
});
final int id;
final String approvalNo;
final StockTransactionApprovalStatusSummary? status;
}
/// 결재 상태 요약 정보
class StockTransactionApprovalStatusSummary {
StockTransactionApprovalStatusSummary({
required this.id,
required this.name,
this.isBlocking,
});
final int id;
final String name;
final bool? isBlocking;
}
extension StockTransactionLineX on List<StockTransactionLine> {
/// 라인 품목 가격 총액을 계산한다.
double get totalAmount =>
fold<double>(0, (sum, line) => sum + (line.quantity * line.unitPrice));
}