사용하지 않는 파일 정리 전 백업 (Phase 10 완료 후 상태)

This commit is contained in:
JiWoong Sul
2025-08-29 15:11:59 +09:00
parent a740ff10c8
commit d916b281a7
333 changed files with 53617 additions and 22574 deletions

View File

@@ -0,0 +1,146 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'company_hierarchy.freezed.dart';
/// Company 계층 구조 도메인 엔티티
@freezed
class CompanyHierarchy with _$CompanyHierarchy {
const CompanyHierarchy._();
const factory CompanyHierarchy({
required String id,
required String name,
String? parentId,
String? parentName,
@Default([]) List<CompanyHierarchy> children,
@Default(0) int level,
@Default('') String fullPath,
@Default(false) bool isExpanded,
@Default(0) int totalDescendants,
}) = _CompanyHierarchy;
/// 계층 구조에서 특정 회사 찾기
CompanyHierarchy? findCompany(String companyId) {
if (id == companyId) {
return this;
}
for (final child in children) {
final found = child.findCompany(companyId);
if (found != null) {
return found;
}
}
return null;
}
/// 모든 자손 회사 ID 목록 가져오기
List<String> getAllDescendantIds() {
final ids = <String>[];
for (final child in children) {
ids.add(child.id);
ids.addAll(child.getAllDescendantIds());
}
return ids;
}
/// 특정 회사가 자손인지 확인
bool hasDescendant(String companyId) {
return getAllDescendantIds().contains(companyId);
}
/// 계층 구조의 최대 깊이 계산
int getMaxDepth() {
if (children.isEmpty) {
return level;
}
return children
.map((child) => child.getMaxDepth())
.reduce((max, depth) => depth > max ? depth : max);
}
/// 순환 참조 검증
bool wouldCreateCycle(String newParentId) {
// 자기 자신을 부모로 설정하려는 경우
if (id == newParentId) {
return true;
}
// 자손을 부모로 설정하려는 경우
if (hasDescendant(newParentId)) {
return true;
}
return false;
}
/// 계층 경로 생성
String buildPath(String separator) {
final parts = fullPath.split('/').where((p) => p.isNotEmpty).toList();
return parts.join(separator);
}
/// 평면 리스트로 변환 (트리 구조 → 플랫 리스트)
List<CompanyHierarchy> flatten() {
final flatList = <CompanyHierarchy>[this];
for (final child in children) {
flatList.addAll(child.flatten());
}
return flatList;
}
/// 계층 구조 통계
Map<String, dynamic> getStatistics() {
final flat = flatten();
return {
'totalCompanies': flat.length,
'maxDepth': getMaxDepth(),
'directChildren': children.length,
'totalDescendants': totalDescendants,
'levels': _getLevelDistribution(flat),
};
}
Map<int, int> _getLevelDistribution(List<CompanyHierarchy> companies) {
final distribution = <int, int>{};
for (final company in companies) {
distribution[company.level] = (distribution[company.level] ?? 0) + 1;
}
return distribution;
}
}
/// 계층 구조 검증 결과
@freezed
class HierarchyValidationResult with _$HierarchyValidationResult {
const factory HierarchyValidationResult({
required bool isValid,
@Default('') String message,
@Default([]) List<String> errors,
@Default([]) List<String> warnings,
}) = _HierarchyValidationResult;
factory HierarchyValidationResult.valid() => const HierarchyValidationResult(
isValid: true,
message: 'Hierarchy is valid',
);
factory HierarchyValidationResult.invalid({
required String message,
List<String> errors = const [],
List<String> warnings = const [],
}) => HierarchyValidationResult(
isValid: false,
message: message,
errors: errors,
warnings: warnings,
);
}

View File

@@ -0,0 +1,555 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'company_hierarchy.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$CompanyHierarchy {
String get id => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
String? get parentId => throw _privateConstructorUsedError;
String? get parentName => throw _privateConstructorUsedError;
List<CompanyHierarchy> get children => throw _privateConstructorUsedError;
int get level => throw _privateConstructorUsedError;
String get fullPath => throw _privateConstructorUsedError;
bool get isExpanded => throw _privateConstructorUsedError;
int get totalDescendants => throw _privateConstructorUsedError;
/// Create a copy of CompanyHierarchy
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$CompanyHierarchyCopyWith<CompanyHierarchy> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CompanyHierarchyCopyWith<$Res> {
factory $CompanyHierarchyCopyWith(
CompanyHierarchy value, $Res Function(CompanyHierarchy) then) =
_$CompanyHierarchyCopyWithImpl<$Res, CompanyHierarchy>;
@useResult
$Res call(
{String id,
String name,
String? parentId,
String? parentName,
List<CompanyHierarchy> children,
int level,
String fullPath,
bool isExpanded,
int totalDescendants});
}
/// @nodoc
class _$CompanyHierarchyCopyWithImpl<$Res, $Val extends CompanyHierarchy>
implements $CompanyHierarchyCopyWith<$Res> {
_$CompanyHierarchyCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CompanyHierarchy
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? parentId = freezed,
Object? parentName = freezed,
Object? children = null,
Object? level = null,
Object? fullPath = null,
Object? isExpanded = null,
Object? totalDescendants = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
parentId: freezed == parentId
? _value.parentId
: parentId // ignore: cast_nullable_to_non_nullable
as String?,
parentName: freezed == parentName
? _value.parentName
: parentName // ignore: cast_nullable_to_non_nullable
as String?,
children: null == children
? _value.children
: children // ignore: cast_nullable_to_non_nullable
as List<CompanyHierarchy>,
level: null == level
? _value.level
: level // ignore: cast_nullable_to_non_nullable
as int,
fullPath: null == fullPath
? _value.fullPath
: fullPath // ignore: cast_nullable_to_non_nullable
as String,
isExpanded: null == isExpanded
? _value.isExpanded
: isExpanded // ignore: cast_nullable_to_non_nullable
as bool,
totalDescendants: null == totalDescendants
? _value.totalDescendants
: totalDescendants // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$CompanyHierarchyImplCopyWith<$Res>
implements $CompanyHierarchyCopyWith<$Res> {
factory _$$CompanyHierarchyImplCopyWith(_$CompanyHierarchyImpl value,
$Res Function(_$CompanyHierarchyImpl) then) =
__$$CompanyHierarchyImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String id,
String name,
String? parentId,
String? parentName,
List<CompanyHierarchy> children,
int level,
String fullPath,
bool isExpanded,
int totalDescendants});
}
/// @nodoc
class __$$CompanyHierarchyImplCopyWithImpl<$Res>
extends _$CompanyHierarchyCopyWithImpl<$Res, _$CompanyHierarchyImpl>
implements _$$CompanyHierarchyImplCopyWith<$Res> {
__$$CompanyHierarchyImplCopyWithImpl(_$CompanyHierarchyImpl _value,
$Res Function(_$CompanyHierarchyImpl) _then)
: super(_value, _then);
/// Create a copy of CompanyHierarchy
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? parentId = freezed,
Object? parentName = freezed,
Object? children = null,
Object? level = null,
Object? fullPath = null,
Object? isExpanded = null,
Object? totalDescendants = null,
}) {
return _then(_$CompanyHierarchyImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
parentId: freezed == parentId
? _value.parentId
: parentId // ignore: cast_nullable_to_non_nullable
as String?,
parentName: freezed == parentName
? _value.parentName
: parentName // ignore: cast_nullable_to_non_nullable
as String?,
children: null == children
? _value._children
: children // ignore: cast_nullable_to_non_nullable
as List<CompanyHierarchy>,
level: null == level
? _value.level
: level // ignore: cast_nullable_to_non_nullable
as int,
fullPath: null == fullPath
? _value.fullPath
: fullPath // ignore: cast_nullable_to_non_nullable
as String,
isExpanded: null == isExpanded
? _value.isExpanded
: isExpanded // ignore: cast_nullable_to_non_nullable
as bool,
totalDescendants: null == totalDescendants
? _value.totalDescendants
: totalDescendants // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
class _$CompanyHierarchyImpl extends _CompanyHierarchy {
const _$CompanyHierarchyImpl(
{required this.id,
required this.name,
this.parentId,
this.parentName,
final List<CompanyHierarchy> children = const [],
this.level = 0,
this.fullPath = '',
this.isExpanded = false,
this.totalDescendants = 0})
: _children = children,
super._();
@override
final String id;
@override
final String name;
@override
final String? parentId;
@override
final String? parentName;
final List<CompanyHierarchy> _children;
@override
@JsonKey()
List<CompanyHierarchy> get children {
if (_children is EqualUnmodifiableListView) return _children;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_children);
}
@override
@JsonKey()
final int level;
@override
@JsonKey()
final String fullPath;
@override
@JsonKey()
final bool isExpanded;
@override
@JsonKey()
final int totalDescendants;
@override
String toString() {
return 'CompanyHierarchy(id: $id, name: $name, parentId: $parentId, parentName: $parentName, children: $children, level: $level, fullPath: $fullPath, isExpanded: $isExpanded, totalDescendants: $totalDescendants)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CompanyHierarchyImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.parentId, parentId) ||
other.parentId == parentId) &&
(identical(other.parentName, parentName) ||
other.parentName == parentName) &&
const DeepCollectionEquality().equals(other._children, _children) &&
(identical(other.level, level) || other.level == level) &&
(identical(other.fullPath, fullPath) ||
other.fullPath == fullPath) &&
(identical(other.isExpanded, isExpanded) ||
other.isExpanded == isExpanded) &&
(identical(other.totalDescendants, totalDescendants) ||
other.totalDescendants == totalDescendants));
}
@override
int get hashCode => Object.hash(
runtimeType,
id,
name,
parentId,
parentName,
const DeepCollectionEquality().hash(_children),
level,
fullPath,
isExpanded,
totalDescendants);
/// Create a copy of CompanyHierarchy
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CompanyHierarchyImplCopyWith<_$CompanyHierarchyImpl> get copyWith =>
__$$CompanyHierarchyImplCopyWithImpl<_$CompanyHierarchyImpl>(
this, _$identity);
}
abstract class _CompanyHierarchy extends CompanyHierarchy {
const factory _CompanyHierarchy(
{required final String id,
required final String name,
final String? parentId,
final String? parentName,
final List<CompanyHierarchy> children,
final int level,
final String fullPath,
final bool isExpanded,
final int totalDescendants}) = _$CompanyHierarchyImpl;
const _CompanyHierarchy._() : super._();
@override
String get id;
@override
String get name;
@override
String? get parentId;
@override
String? get parentName;
@override
List<CompanyHierarchy> get children;
@override
int get level;
@override
String get fullPath;
@override
bool get isExpanded;
@override
int get totalDescendants;
/// Create a copy of CompanyHierarchy
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CompanyHierarchyImplCopyWith<_$CompanyHierarchyImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$HierarchyValidationResult {
bool get isValid => throw _privateConstructorUsedError;
String get message => throw _privateConstructorUsedError;
List<String> get errors => throw _privateConstructorUsedError;
List<String> get warnings => throw _privateConstructorUsedError;
/// Create a copy of HierarchyValidationResult
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$HierarchyValidationResultCopyWith<HierarchyValidationResult> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $HierarchyValidationResultCopyWith<$Res> {
factory $HierarchyValidationResultCopyWith(HierarchyValidationResult value,
$Res Function(HierarchyValidationResult) then) =
_$HierarchyValidationResultCopyWithImpl<$Res, HierarchyValidationResult>;
@useResult
$Res call(
{bool isValid,
String message,
List<String> errors,
List<String> warnings});
}
/// @nodoc
class _$HierarchyValidationResultCopyWithImpl<$Res,
$Val extends HierarchyValidationResult>
implements $HierarchyValidationResultCopyWith<$Res> {
_$HierarchyValidationResultCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of HierarchyValidationResult
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isValid = null,
Object? message = null,
Object? errors = null,
Object? warnings = null,
}) {
return _then(_value.copyWith(
isValid: null == isValid
? _value.isValid
: isValid // ignore: cast_nullable_to_non_nullable
as bool,
message: null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
errors: null == errors
? _value.errors
: errors // ignore: cast_nullable_to_non_nullable
as List<String>,
warnings: null == warnings
? _value.warnings
: warnings // ignore: cast_nullable_to_non_nullable
as List<String>,
) as $Val);
}
}
/// @nodoc
abstract class _$$HierarchyValidationResultImplCopyWith<$Res>
implements $HierarchyValidationResultCopyWith<$Res> {
factory _$$HierarchyValidationResultImplCopyWith(
_$HierarchyValidationResultImpl value,
$Res Function(_$HierarchyValidationResultImpl) then) =
__$$HierarchyValidationResultImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{bool isValid,
String message,
List<String> errors,
List<String> warnings});
}
/// @nodoc
class __$$HierarchyValidationResultImplCopyWithImpl<$Res>
extends _$HierarchyValidationResultCopyWithImpl<$Res,
_$HierarchyValidationResultImpl>
implements _$$HierarchyValidationResultImplCopyWith<$Res> {
__$$HierarchyValidationResultImplCopyWithImpl(
_$HierarchyValidationResultImpl _value,
$Res Function(_$HierarchyValidationResultImpl) _then)
: super(_value, _then);
/// Create a copy of HierarchyValidationResult
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isValid = null,
Object? message = null,
Object? errors = null,
Object? warnings = null,
}) {
return _then(_$HierarchyValidationResultImpl(
isValid: null == isValid
? _value.isValid
: isValid // ignore: cast_nullable_to_non_nullable
as bool,
message: null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
errors: null == errors
? _value._errors
: errors // ignore: cast_nullable_to_non_nullable
as List<String>,
warnings: null == warnings
? _value._warnings
: warnings // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
/// @nodoc
class _$HierarchyValidationResultImpl implements _HierarchyValidationResult {
const _$HierarchyValidationResultImpl(
{required this.isValid,
this.message = '',
final List<String> errors = const [],
final List<String> warnings = const []})
: _errors = errors,
_warnings = warnings;
@override
final bool isValid;
@override
@JsonKey()
final String message;
final List<String> _errors;
@override
@JsonKey()
List<String> get errors {
if (_errors is EqualUnmodifiableListView) return _errors;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_errors);
}
final List<String> _warnings;
@override
@JsonKey()
List<String> get warnings {
if (_warnings is EqualUnmodifiableListView) return _warnings;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_warnings);
}
@override
String toString() {
return 'HierarchyValidationResult(isValid: $isValid, message: $message, errors: $errors, warnings: $warnings)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$HierarchyValidationResultImpl &&
(identical(other.isValid, isValid) || other.isValid == isValid) &&
(identical(other.message, message) || other.message == message) &&
const DeepCollectionEquality().equals(other._errors, _errors) &&
const DeepCollectionEquality().equals(other._warnings, _warnings));
}
@override
int get hashCode => Object.hash(
runtimeType,
isValid,
message,
const DeepCollectionEquality().hash(_errors),
const DeepCollectionEquality().hash(_warnings));
/// Create a copy of HierarchyValidationResult
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$HierarchyValidationResultImplCopyWith<_$HierarchyValidationResultImpl>
get copyWith => __$$HierarchyValidationResultImplCopyWithImpl<
_$HierarchyValidationResultImpl>(this, _$identity);
}
abstract class _HierarchyValidationResult implements HierarchyValidationResult {
const factory _HierarchyValidationResult(
{required final bool isValid,
final String message,
final List<String> errors,
final List<String> warnings}) = _$HierarchyValidationResultImpl;
@override
bool get isValid;
@override
String get message;
@override
List<String> get errors;
@override
List<String> get warnings;
/// Create a copy of HierarchyValidationResult
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$HierarchyValidationResultImplCopyWith<_$HierarchyValidationResultImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,138 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'maintenance_schedule.freezed.dart';
@freezed
class MaintenanceSchedule with _$MaintenanceSchedule {
const MaintenanceSchedule._();
const factory MaintenanceSchedule({
required int equipmentHistoryId,
required String maintenanceType,
required int periodMonths,
required DateTime startDate,
required DateTime endDate,
DateTime? lastMaintenanceDate,
DateTime? nextMaintenanceDate,
required double cost,
String? description,
@Default([]) List<DateTime> scheduledDates,
@Default(MaintenanceScheduleStatus.active) MaintenanceScheduleStatus status,
}) = _MaintenanceSchedule;
// 다음 유지보수 날짜 계산
DateTime calculateNextMaintenanceDate() {
final lastDate = lastMaintenanceDate ?? startDate;
return lastDate.add(Duration(days: periodMonths * 30));
}
// 남은 일수 계산
int getDaysUntilNextMaintenance() {
final next = nextMaintenanceDate ?? calculateNextMaintenanceDate();
return next.difference(DateTime.now()).inDays;
}
// 만료 여부 확인
bool isOverdue() {
final daysUntil = getDaysUntilNextMaintenance();
return daysUntil < 0;
}
// 곧 만료 예정 여부 확인 (30일 이내)
bool isUpcoming({int daysThreshold = 30}) {
final daysUntil = getDaysUntilNextMaintenance();
return daysUntil >= 0 && daysUntil <= daysThreshold;
}
// 전체 스케줄 생성
List<DateTime> generateFullSchedule() {
final schedules = <DateTime>[];
var currentDate = startDate;
while (currentDate.isBefore(endDate)) {
schedules.add(currentDate);
currentDate = currentDate.add(Duration(days: periodMonths * 30));
}
return schedules;
}
// 남은 유지보수 횟수 계산
int getRemainingMaintenanceCount() {
final now = DateTime.now();
final schedules = generateFullSchedule();
return schedules.where((date) => date.isAfter(now)).length;
}
// 총 비용 계산
double getTotalCost() {
final totalCount = generateFullSchedule().length;
return cost * totalCount;
}
// 남은 비용 계산
double getRemainingCost() {
return cost * getRemainingMaintenanceCount();
}
}
enum MaintenanceScheduleStatus {
active, // 활성
paused, // 일시중지
completed,// 완료
cancelled // 취소
}
// 유지보수 알림 설정
@freezed
class MaintenanceAlert with _$MaintenanceAlert {
const MaintenanceAlert._();
const factory MaintenanceAlert({
required int maintenanceId,
required int equipmentHistoryId,
required String equipmentName,
required DateTime scheduledDate,
required String maintenanceType,
required int daysUntil,
required AlertPriority priority,
String? description,
double? estimatedCost,
}) = _MaintenanceAlert;
// Backward compatibility getter
int get daysUntilDue => daysUntil;
factory MaintenanceAlert.fromSchedule(
MaintenanceSchedule schedule,
String equipmentName,
) {
final daysUntil = schedule.getDaysUntilNextMaintenance();
final priority = _getPriority(daysUntil);
return MaintenanceAlert(
maintenanceId: 0, // Will be set from actual maintenance
equipmentHistoryId: schedule.equipmentHistoryId,
equipmentName: equipmentName,
scheduledDate: schedule.nextMaintenanceDate ?? schedule.calculateNextMaintenanceDate(),
maintenanceType: schedule.maintenanceType,
daysUntil: daysUntil,
priority: priority,
description: schedule.description,
);
}
static AlertPriority _getPriority(int daysUntil) {
if (daysUntil < 0) return AlertPriority.critical;
if (daysUntil <= 7) return AlertPriority.high;
if (daysUntil <= 30) return AlertPriority.medium;
return AlertPriority.low;
}
}
enum AlertPriority {
critical, // 만료됨
high, // 7일 이내
medium, // 30일 이내
low // 30일 초과
}

View File

@@ -0,0 +1,693 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'maintenance_schedule.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$MaintenanceSchedule {
int get equipmentHistoryId => throw _privateConstructorUsedError;
String get maintenanceType => throw _privateConstructorUsedError;
int get periodMonths => throw _privateConstructorUsedError;
DateTime get startDate => throw _privateConstructorUsedError;
DateTime get endDate => throw _privateConstructorUsedError;
DateTime? get lastMaintenanceDate => throw _privateConstructorUsedError;
DateTime? get nextMaintenanceDate => throw _privateConstructorUsedError;
double get cost => throw _privateConstructorUsedError;
String? get description => throw _privateConstructorUsedError;
List<DateTime> get scheduledDates => throw _privateConstructorUsedError;
MaintenanceScheduleStatus get status => throw _privateConstructorUsedError;
/// Create a copy of MaintenanceSchedule
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$MaintenanceScheduleCopyWith<MaintenanceSchedule> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MaintenanceScheduleCopyWith<$Res> {
factory $MaintenanceScheduleCopyWith(
MaintenanceSchedule value, $Res Function(MaintenanceSchedule) then) =
_$MaintenanceScheduleCopyWithImpl<$Res, MaintenanceSchedule>;
@useResult
$Res call(
{int equipmentHistoryId,
String maintenanceType,
int periodMonths,
DateTime startDate,
DateTime endDate,
DateTime? lastMaintenanceDate,
DateTime? nextMaintenanceDate,
double cost,
String? description,
List<DateTime> scheduledDates,
MaintenanceScheduleStatus status});
}
/// @nodoc
class _$MaintenanceScheduleCopyWithImpl<$Res, $Val extends MaintenanceSchedule>
implements $MaintenanceScheduleCopyWith<$Res> {
_$MaintenanceScheduleCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of MaintenanceSchedule
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? equipmentHistoryId = null,
Object? maintenanceType = null,
Object? periodMonths = null,
Object? startDate = null,
Object? endDate = null,
Object? lastMaintenanceDate = freezed,
Object? nextMaintenanceDate = freezed,
Object? cost = null,
Object? description = freezed,
Object? scheduledDates = null,
Object? status = null,
}) {
return _then(_value.copyWith(
equipmentHistoryId: null == equipmentHistoryId
? _value.equipmentHistoryId
: equipmentHistoryId // ignore: cast_nullable_to_non_nullable
as int,
maintenanceType: null == maintenanceType
? _value.maintenanceType
: maintenanceType // ignore: cast_nullable_to_non_nullable
as String,
periodMonths: null == periodMonths
? _value.periodMonths
: periodMonths // ignore: cast_nullable_to_non_nullable
as int,
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate: null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
lastMaintenanceDate: freezed == lastMaintenanceDate
? _value.lastMaintenanceDate
: lastMaintenanceDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
nextMaintenanceDate: freezed == nextMaintenanceDate
? _value.nextMaintenanceDate
: nextMaintenanceDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
cost: null == cost
? _value.cost
: cost // ignore: cast_nullable_to_non_nullable
as double,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
scheduledDates: null == scheduledDates
? _value.scheduledDates
: scheduledDates // ignore: cast_nullable_to_non_nullable
as List<DateTime>,
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as MaintenanceScheduleStatus,
) as $Val);
}
}
/// @nodoc
abstract class _$$MaintenanceScheduleImplCopyWith<$Res>
implements $MaintenanceScheduleCopyWith<$Res> {
factory _$$MaintenanceScheduleImplCopyWith(_$MaintenanceScheduleImpl value,
$Res Function(_$MaintenanceScheduleImpl) then) =
__$$MaintenanceScheduleImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int equipmentHistoryId,
String maintenanceType,
int periodMonths,
DateTime startDate,
DateTime endDate,
DateTime? lastMaintenanceDate,
DateTime? nextMaintenanceDate,
double cost,
String? description,
List<DateTime> scheduledDates,
MaintenanceScheduleStatus status});
}
/// @nodoc
class __$$MaintenanceScheduleImplCopyWithImpl<$Res>
extends _$MaintenanceScheduleCopyWithImpl<$Res, _$MaintenanceScheduleImpl>
implements _$$MaintenanceScheduleImplCopyWith<$Res> {
__$$MaintenanceScheduleImplCopyWithImpl(_$MaintenanceScheduleImpl _value,
$Res Function(_$MaintenanceScheduleImpl) _then)
: super(_value, _then);
/// Create a copy of MaintenanceSchedule
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? equipmentHistoryId = null,
Object? maintenanceType = null,
Object? periodMonths = null,
Object? startDate = null,
Object? endDate = null,
Object? lastMaintenanceDate = freezed,
Object? nextMaintenanceDate = freezed,
Object? cost = null,
Object? description = freezed,
Object? scheduledDates = null,
Object? status = null,
}) {
return _then(_$MaintenanceScheduleImpl(
equipmentHistoryId: null == equipmentHistoryId
? _value.equipmentHistoryId
: equipmentHistoryId // ignore: cast_nullable_to_non_nullable
as int,
maintenanceType: null == maintenanceType
? _value.maintenanceType
: maintenanceType // ignore: cast_nullable_to_non_nullable
as String,
periodMonths: null == periodMonths
? _value.periodMonths
: periodMonths // ignore: cast_nullable_to_non_nullable
as int,
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate: null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
lastMaintenanceDate: freezed == lastMaintenanceDate
? _value.lastMaintenanceDate
: lastMaintenanceDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
nextMaintenanceDate: freezed == nextMaintenanceDate
? _value.nextMaintenanceDate
: nextMaintenanceDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
cost: null == cost
? _value.cost
: cost // ignore: cast_nullable_to_non_nullable
as double,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
scheduledDates: null == scheduledDates
? _value._scheduledDates
: scheduledDates // ignore: cast_nullable_to_non_nullable
as List<DateTime>,
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as MaintenanceScheduleStatus,
));
}
}
/// @nodoc
class _$MaintenanceScheduleImpl extends _MaintenanceSchedule {
const _$MaintenanceScheduleImpl(
{required this.equipmentHistoryId,
required this.maintenanceType,
required this.periodMonths,
required this.startDate,
required this.endDate,
this.lastMaintenanceDate,
this.nextMaintenanceDate,
required this.cost,
this.description,
final List<DateTime> scheduledDates = const [],
this.status = MaintenanceScheduleStatus.active})
: _scheduledDates = scheduledDates,
super._();
@override
final int equipmentHistoryId;
@override
final String maintenanceType;
@override
final int periodMonths;
@override
final DateTime startDate;
@override
final DateTime endDate;
@override
final DateTime? lastMaintenanceDate;
@override
final DateTime? nextMaintenanceDate;
@override
final double cost;
@override
final String? description;
final List<DateTime> _scheduledDates;
@override
@JsonKey()
List<DateTime> get scheduledDates {
if (_scheduledDates is EqualUnmodifiableListView) return _scheduledDates;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_scheduledDates);
}
@override
@JsonKey()
final MaintenanceScheduleStatus status;
@override
String toString() {
return 'MaintenanceSchedule(equipmentHistoryId: $equipmentHistoryId, maintenanceType: $maintenanceType, periodMonths: $periodMonths, startDate: $startDate, endDate: $endDate, lastMaintenanceDate: $lastMaintenanceDate, nextMaintenanceDate: $nextMaintenanceDate, cost: $cost, description: $description, scheduledDates: $scheduledDates, status: $status)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$MaintenanceScheduleImpl &&
(identical(other.equipmentHistoryId, equipmentHistoryId) ||
other.equipmentHistoryId == equipmentHistoryId) &&
(identical(other.maintenanceType, maintenanceType) ||
other.maintenanceType == maintenanceType) &&
(identical(other.periodMonths, periodMonths) ||
other.periodMonths == periodMonths) &&
(identical(other.startDate, startDate) ||
other.startDate == startDate) &&
(identical(other.endDate, endDate) || other.endDate == endDate) &&
(identical(other.lastMaintenanceDate, lastMaintenanceDate) ||
other.lastMaintenanceDate == lastMaintenanceDate) &&
(identical(other.nextMaintenanceDate, nextMaintenanceDate) ||
other.nextMaintenanceDate == nextMaintenanceDate) &&
(identical(other.cost, cost) || other.cost == cost) &&
(identical(other.description, description) ||
other.description == description) &&
const DeepCollectionEquality()
.equals(other._scheduledDates, _scheduledDates) &&
(identical(other.status, status) || other.status == status));
}
@override
int get hashCode => Object.hash(
runtimeType,
equipmentHistoryId,
maintenanceType,
periodMonths,
startDate,
endDate,
lastMaintenanceDate,
nextMaintenanceDate,
cost,
description,
const DeepCollectionEquality().hash(_scheduledDates),
status);
/// Create a copy of MaintenanceSchedule
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$MaintenanceScheduleImplCopyWith<_$MaintenanceScheduleImpl> get copyWith =>
__$$MaintenanceScheduleImplCopyWithImpl<_$MaintenanceScheduleImpl>(
this, _$identity);
}
abstract class _MaintenanceSchedule extends MaintenanceSchedule {
const factory _MaintenanceSchedule(
{required final int equipmentHistoryId,
required final String maintenanceType,
required final int periodMonths,
required final DateTime startDate,
required final DateTime endDate,
final DateTime? lastMaintenanceDate,
final DateTime? nextMaintenanceDate,
required final double cost,
final String? description,
final List<DateTime> scheduledDates,
final MaintenanceScheduleStatus status}) = _$MaintenanceScheduleImpl;
const _MaintenanceSchedule._() : super._();
@override
int get equipmentHistoryId;
@override
String get maintenanceType;
@override
int get periodMonths;
@override
DateTime get startDate;
@override
DateTime get endDate;
@override
DateTime? get lastMaintenanceDate;
@override
DateTime? get nextMaintenanceDate;
@override
double get cost;
@override
String? get description;
@override
List<DateTime> get scheduledDates;
@override
MaintenanceScheduleStatus get status;
/// Create a copy of MaintenanceSchedule
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$MaintenanceScheduleImplCopyWith<_$MaintenanceScheduleImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$MaintenanceAlert {
int get maintenanceId => throw _privateConstructorUsedError;
int get equipmentHistoryId => throw _privateConstructorUsedError;
String get equipmentName => throw _privateConstructorUsedError;
DateTime get scheduledDate => throw _privateConstructorUsedError;
String get maintenanceType => throw _privateConstructorUsedError;
int get daysUntil => throw _privateConstructorUsedError;
AlertPriority get priority => throw _privateConstructorUsedError;
String? get description => throw _privateConstructorUsedError;
double? get estimatedCost => throw _privateConstructorUsedError;
/// Create a copy of MaintenanceAlert
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$MaintenanceAlertCopyWith<MaintenanceAlert> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MaintenanceAlertCopyWith<$Res> {
factory $MaintenanceAlertCopyWith(
MaintenanceAlert value, $Res Function(MaintenanceAlert) then) =
_$MaintenanceAlertCopyWithImpl<$Res, MaintenanceAlert>;
@useResult
$Res call(
{int maintenanceId,
int equipmentHistoryId,
String equipmentName,
DateTime scheduledDate,
String maintenanceType,
int daysUntil,
AlertPriority priority,
String? description,
double? estimatedCost});
}
/// @nodoc
class _$MaintenanceAlertCopyWithImpl<$Res, $Val extends MaintenanceAlert>
implements $MaintenanceAlertCopyWith<$Res> {
_$MaintenanceAlertCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of MaintenanceAlert
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? maintenanceId = null,
Object? equipmentHistoryId = null,
Object? equipmentName = null,
Object? scheduledDate = null,
Object? maintenanceType = null,
Object? daysUntil = null,
Object? priority = null,
Object? description = freezed,
Object? estimatedCost = freezed,
}) {
return _then(_value.copyWith(
maintenanceId: null == maintenanceId
? _value.maintenanceId
: maintenanceId // ignore: cast_nullable_to_non_nullable
as int,
equipmentHistoryId: null == equipmentHistoryId
? _value.equipmentHistoryId
: equipmentHistoryId // ignore: cast_nullable_to_non_nullable
as int,
equipmentName: null == equipmentName
? _value.equipmentName
: equipmentName // ignore: cast_nullable_to_non_nullable
as String,
scheduledDate: null == scheduledDate
? _value.scheduledDate
: scheduledDate // ignore: cast_nullable_to_non_nullable
as DateTime,
maintenanceType: null == maintenanceType
? _value.maintenanceType
: maintenanceType // ignore: cast_nullable_to_non_nullable
as String,
daysUntil: null == daysUntil
? _value.daysUntil
: daysUntil // ignore: cast_nullable_to_non_nullable
as int,
priority: null == priority
? _value.priority
: priority // ignore: cast_nullable_to_non_nullable
as AlertPriority,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
estimatedCost: freezed == estimatedCost
? _value.estimatedCost
: estimatedCost // ignore: cast_nullable_to_non_nullable
as double?,
) as $Val);
}
}
/// @nodoc
abstract class _$$MaintenanceAlertImplCopyWith<$Res>
implements $MaintenanceAlertCopyWith<$Res> {
factory _$$MaintenanceAlertImplCopyWith(_$MaintenanceAlertImpl value,
$Res Function(_$MaintenanceAlertImpl) then) =
__$$MaintenanceAlertImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int maintenanceId,
int equipmentHistoryId,
String equipmentName,
DateTime scheduledDate,
String maintenanceType,
int daysUntil,
AlertPriority priority,
String? description,
double? estimatedCost});
}
/// @nodoc
class __$$MaintenanceAlertImplCopyWithImpl<$Res>
extends _$MaintenanceAlertCopyWithImpl<$Res, _$MaintenanceAlertImpl>
implements _$$MaintenanceAlertImplCopyWith<$Res> {
__$$MaintenanceAlertImplCopyWithImpl(_$MaintenanceAlertImpl _value,
$Res Function(_$MaintenanceAlertImpl) _then)
: super(_value, _then);
/// Create a copy of MaintenanceAlert
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? maintenanceId = null,
Object? equipmentHistoryId = null,
Object? equipmentName = null,
Object? scheduledDate = null,
Object? maintenanceType = null,
Object? daysUntil = null,
Object? priority = null,
Object? description = freezed,
Object? estimatedCost = freezed,
}) {
return _then(_$MaintenanceAlertImpl(
maintenanceId: null == maintenanceId
? _value.maintenanceId
: maintenanceId // ignore: cast_nullable_to_non_nullable
as int,
equipmentHistoryId: null == equipmentHistoryId
? _value.equipmentHistoryId
: equipmentHistoryId // ignore: cast_nullable_to_non_nullable
as int,
equipmentName: null == equipmentName
? _value.equipmentName
: equipmentName // ignore: cast_nullable_to_non_nullable
as String,
scheduledDate: null == scheduledDate
? _value.scheduledDate
: scheduledDate // ignore: cast_nullable_to_non_nullable
as DateTime,
maintenanceType: null == maintenanceType
? _value.maintenanceType
: maintenanceType // ignore: cast_nullable_to_non_nullable
as String,
daysUntil: null == daysUntil
? _value.daysUntil
: daysUntil // ignore: cast_nullable_to_non_nullable
as int,
priority: null == priority
? _value.priority
: priority // ignore: cast_nullable_to_non_nullable
as AlertPriority,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
estimatedCost: freezed == estimatedCost
? _value.estimatedCost
: estimatedCost // ignore: cast_nullable_to_non_nullable
as double?,
));
}
}
/// @nodoc
class _$MaintenanceAlertImpl extends _MaintenanceAlert {
const _$MaintenanceAlertImpl(
{required this.maintenanceId,
required this.equipmentHistoryId,
required this.equipmentName,
required this.scheduledDate,
required this.maintenanceType,
required this.daysUntil,
required this.priority,
this.description,
this.estimatedCost})
: super._();
@override
final int maintenanceId;
@override
final int equipmentHistoryId;
@override
final String equipmentName;
@override
final DateTime scheduledDate;
@override
final String maintenanceType;
@override
final int daysUntil;
@override
final AlertPriority priority;
@override
final String? description;
@override
final double? estimatedCost;
@override
String toString() {
return 'MaintenanceAlert(maintenanceId: $maintenanceId, equipmentHistoryId: $equipmentHistoryId, equipmentName: $equipmentName, scheduledDate: $scheduledDate, maintenanceType: $maintenanceType, daysUntil: $daysUntil, priority: $priority, description: $description, estimatedCost: $estimatedCost)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$MaintenanceAlertImpl &&
(identical(other.maintenanceId, maintenanceId) ||
other.maintenanceId == maintenanceId) &&
(identical(other.equipmentHistoryId, equipmentHistoryId) ||
other.equipmentHistoryId == equipmentHistoryId) &&
(identical(other.equipmentName, equipmentName) ||
other.equipmentName == equipmentName) &&
(identical(other.scheduledDate, scheduledDate) ||
other.scheduledDate == scheduledDate) &&
(identical(other.maintenanceType, maintenanceType) ||
other.maintenanceType == maintenanceType) &&
(identical(other.daysUntil, daysUntil) ||
other.daysUntil == daysUntil) &&
(identical(other.priority, priority) ||
other.priority == priority) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.estimatedCost, estimatedCost) ||
other.estimatedCost == estimatedCost));
}
@override
int get hashCode => Object.hash(
runtimeType,
maintenanceId,
equipmentHistoryId,
equipmentName,
scheduledDate,
maintenanceType,
daysUntil,
priority,
description,
estimatedCost);
/// Create a copy of MaintenanceAlert
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$MaintenanceAlertImplCopyWith<_$MaintenanceAlertImpl> get copyWith =>
__$$MaintenanceAlertImplCopyWithImpl<_$MaintenanceAlertImpl>(
this, _$identity);
}
abstract class _MaintenanceAlert extends MaintenanceAlert {
const factory _MaintenanceAlert(
{required final int maintenanceId,
required final int equipmentHistoryId,
required final String equipmentName,
required final DateTime scheduledDate,
required final String maintenanceType,
required final int daysUntil,
required final AlertPriority priority,
final String? description,
final double? estimatedCost}) = _$MaintenanceAlertImpl;
const _MaintenanceAlert._() : super._();
@override
int get maintenanceId;
@override
int get equipmentHistoryId;
@override
String get equipmentName;
@override
DateTime get scheduledDate;
@override
String get maintenanceType;
@override
int get daysUntil;
@override
AlertPriority get priority;
@override
String? get description;
@override
double? get estimatedCost;
/// Create a copy of MaintenanceAlert
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$MaintenanceAlertImplCopyWith<_$MaintenanceAlertImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,59 @@
import 'package:dartz/dartz.dart';
import '../../core/errors/failures.dart';
import '../../data/models/administrator_dto.dart';
/// 관리자 관리 Repository 인터페이스 (백엔드 API Administrator 테이블)
/// Clean Architecture Domain Layer - Repository 계약
abstract class AdministratorRepository {
/// 관리자 목록 조회 (페이지네이션 지원)
/// [page] 페이지 번호 (기본값: 1)
/// [pageSize] 페이지당 항목 수 (기본값: 20)
/// [search] 검색어 (이름, 이메일, 전화번호)
/// Returns: 페이지네이션된 관리자 목록
Future<Either<Failure, AdministratorListResponse>> getAdministrators({
int? page,
int? pageSize,
String? search,
});
/// 단일 관리자 조회
/// [id] 관리자 고유 식별자
/// Returns: 관리자 상세 정보
Future<Either<Failure, AdministratorDto>> getAdministratorById(int id);
/// 관리자 계정 생성
/// [administrator] 생성할 관리자 정보
/// Returns: 생성된 관리자 정보 (ID 포함)
Future<Either<Failure, AdministratorDto>> createAdministrator(
AdministratorRequestDto administrator,
);
/// 관리자 정보 수정
/// [id] 수정할 관리자 고유 식별자
/// [administrator] 수정할 관리자 정보
/// Returns: 수정된 관리자 정보
Future<Either<Failure, AdministratorDto>> updateAdministrator(
int id,
AdministratorUpdateRequestDto administrator,
);
/// 관리자 계정 삭제
/// [id] 삭제할 관리자 고유 식별자
/// Returns: 삭제 성공/실패 여부
Future<Either<Failure, void>> deleteAdministrator(int id);
/// 이메일 중복 확인
/// [email] 체크할 이메일 주소
/// [excludeId] 체크에서 제외할 관리자 ID (수정 시 현재 관리자 제외용)
/// Returns: 중복 여부 (true: 중복됨, false: 중복되지 않음)
Future<Either<Failure, bool>> isDuplicateEmail(String email, {int? excludeId});
/// 관리자 인증 (로그인용)
/// [email] 이메일 주소
/// [password] 비밀번호
/// Returns: 인증된 관리자 정보
Future<Either<Failure, AdministratorDto>> authenticateAdministrator(
String email,
String password,
);
}

View File

@@ -93,4 +93,50 @@ abstract class CompanyRepository {
/// [excludeId] 체크에서 제외할 회사 ID (수정 시 현재 회사 제외용)
/// Returns: 중복 여부 (true: 중복됨, false: 중복되지 않음)
Future<Either<Failure, bool>> isDuplicateCompanyName(String name, {int? excludeId});
// 계층 구조 관련 메서드들
/// 전체 회사 계층 구조 조회
/// [includeInactive] 비활성 회사 포함 여부
/// Returns: 전체 계층 구조 트리
Future<Either<Failure, List<Company>>> getCompanyHierarchy({
bool includeInactive = false,
});
/// 특정 회사의 자식 회사 목록 조회
/// [companyId] 부모 회사 ID
/// [recursive] 재귀적으로 모든 자손 포함 여부
/// Returns: 자식 회사 목록
Future<Either<Failure, List<Company>>> getChildrenCompanies(
int companyId, {
bool recursive = false,
});
/// 특정 회사의 부모 경로 조회
/// [companyId] 회사 ID
/// Returns: 루트부터 현재 회사까지의 경로
Future<Either<Failure, List<Company>>> getAncestorPath(int companyId);
/// 회사의 부모 변경
/// [companyId] 변경할 회사 ID
/// [newParentId] 새로운 부모 회사 ID (null이면 루트로 변경)
/// Returns: 업데이트된 회사 정보
Future<Either<Failure, Company>> updateParentCompany(
int companyId,
int? newParentId,
);
/// 회사가 자식을 가지고 있는지 확인
/// [companyId] 확인할 회사 ID
/// Returns: 자식 회사 존재 여부
Future<Either<Failure, bool>> hasChildrenCompanies(int companyId);
/// 계층 구조 유효성 검증
/// [companyId] 검증할 회사 ID
/// [newParentId] 새로운 부모 회사 ID
/// Returns: 유효성 검증 결과 (순환 참조, 깊이 제한 등)
Future<Either<Failure, bool>> validateHierarchyChange(
int companyId,
int? newParentId,
);
}

View File

@@ -1,68 +1,26 @@
import 'package:dartz/dartz.dart';
import '../../core/errors/failures.dart';
import '../../models/equipment_unified_model.dart';
import '../../data/models/equipment/equipment_dto.dart';
import '../../data/models/common/paginated_response.dart';
/// 장비 관리 Repository 인터페이스
abstract class EquipmentRepository {
/// 장비 입고 목록 조회
Future<Either<Failure, List<EquipmentIn>>> getEquipmentIns({
/// 장비 목록 조회 (페이지네이션 지원)
Future<Either<Failure, PaginatedResponse<EquipmentDto>>> getEquipments({
int? page,
int? limit,
String? search,
String? sortBy,
String? sortOrder,
});
/// 장비 입고 상세 조회
Future<Either<Failure, EquipmentIn>> getEquipmentInById(int id);
/// 장비 상세 조회
Future<Either<Failure, EquipmentDto>> getEquipmentDetail(int id);
/// 장비 입고 생성
Future<Either<Failure, EquipmentIn>> createEquipmentIn(EquipmentIn equipmentIn);
/// 장비 생성
Future<Either<Failure, EquipmentDto>> createEquipment(EquipmentRequestDto request);
/// 장비 입고 수정
Future<Either<Failure, EquipmentIn>> updateEquipmentIn(int id, EquipmentIn equipmentIn);
/// 장비 수정
Future<Either<Failure, EquipmentDto>> updateEquipment(int id, EquipmentUpdateRequestDto request);
/// 장비 입고 삭제
Future<Either<Failure, void>> deleteEquipmentIn(int id);
/// 장비 출고 목록 조회
Future<Either<Failure, List<EquipmentOut>>> getEquipmentOuts({
int? page,
int? limit,
String? search,
String? sortBy,
String? sortOrder,
});
/// 장비 출고 상세 조회
Future<Either<Failure, EquipmentOut>> getEquipmentOutById(int id);
/// 장비 출고 생성
Future<Either<Failure, EquipmentOut>> createEquipmentOut(EquipmentOut equipmentOut);
/// 장비 출고 수정
Future<Either<Failure, EquipmentOut>> updateEquipmentOut(int id, EquipmentOut equipmentOut);
/// 장비 출고 삭제
Future<Either<Failure, void>> deleteEquipmentOut(int id);
/// 장비 일괄 출고
Future<Either<Failure, List<EquipmentOut>>> createBatchEquipmentOut(List<EquipmentOut> equipmentOuts);
/// 제조사 목록 조회
Future<Either<Failure, List<String>>> getManufacturers();
/// 장비명 목록 조회
Future<Either<Failure, List<String>>> getEquipmentNames();
/// 장비 이력 조회
Future<Either<Failure, List<dynamic>>> getEquipmentHistory(int equipmentId);
/// 장비 검색
Future<Either<Failure, List<Equipment>>> searchEquipment({
String? manufacturer,
String? name,
String? category,
String? serialNumber,
});
/// 장비 삭제 (소프트 삭제)
Future<Either<Failure, void>> deleteEquipment(int id);
}

View File

@@ -1,101 +0,0 @@
import 'package:dartz/dartz.dart';
import '../../core/errors/failures.dart';
import '../../models/license_model.dart';
import '../../data/models/common/paginated_response.dart';
import '../../data/models/dashboard/license_expiry_summary.dart';
/// 라이선스 관리 Repository 인터페이스
/// 장비 라이선스 및 유지보수 계약 정보 관리를 담당
abstract class LicenseRepository {
/// 라이선스 목록 조회
/// [page] 페이지 번호 (기본값: 1)
/// [limit] 페이지당 항목 수 (기본값: 20)
/// [search] 검색어 (라이선스명, 회사명, 장비명 등)
/// [companyId] 회사 ID 필터
/// [equipmentType] 장비 유형 필터
/// [expiryStatus] 만료 상태 필터 ('expired', 'expiring', 'active')
/// [sortBy] 정렬 기준 ('name', 'expiryDate', 'createdAt' 등)
/// [sortOrder] 정렬 순서 ('asc', 'desc')
/// Returns: 페이지네이션된 라이선스 목록
Future<Either<Failure, PaginatedResponse<License>>> getLicenses({
int? page,
int? limit,
String? search,
int? companyId,
String? equipmentType,
String? expiryStatus,
String? sortBy,
String? sortOrder,
});
/// 라이선스 상세 정보 조회
/// [id] 라이선스 고유 식별자
/// Returns: 라이선스 상세 정보 (회사, 장비 정보 포함)
Future<Either<Failure, License>> getLicenseById(int id);
/// 라이선스 생성
/// [license] 생성할 라이선스 정보
/// Returns: 생성된 라이선스 정보 (ID 포함)
Future<Either<Failure, License>> createLicense(License license);
/// 라이선스 정보 수정
/// [id] 수정할 라이선스 고유 식별자
/// [license] 수정할 라이선스 정보
/// Returns: 수정된 라이선스 정보
Future<Either<Failure, License>> updateLicense(int id, License license);
/// 라이선스 삭제
/// [id] 삭제할 라이선스 고유 식별자
/// Returns: 삭제 성공/실패 여부
Future<Either<Failure, void>> deleteLicense(int id);
/// 만료 예정 라이선스 조회
/// [days] 앞으로 N일 내 만료 예정 (기본값: 30일)
/// [companyId] 회사 ID 필터 (선택적)
/// Returns: 만료 예정 라이선스 목록
Future<Either<Failure, List<License>>> getExpiringLicenses({int days = 30, int? companyId});
/// 만료된 라이선스 조회
/// [companyId] 회사 ID 필터 (선택적)
/// Returns: 이미 만료된 라이선스 목록
Future<Either<Failure, List<License>>> getExpiredLicenses({int? companyId});
/// 라이선스 만료 요약 정보 조회
/// 대시보드용 30일/60일/90일 내 만료 예정 요약 정보
/// Returns: 만료 예정 요약 정보
Future<Either<Failure, LicenseExpirySummary>> getLicenseExpirySummary();
/// 라이선스 갱신
/// [id] 갱신할 라이선스 ID
/// [newExpiryDate] 새로운 만료일
/// [renewalCost] 갱신 비용 (선택적)
/// [renewalNote] 갱신 비고 (선택적)
/// Returns: 갱신된 라이선스 정보
Future<Either<Failure, License>> renewLicense(
int id,
DateTime newExpiryDate,
{double? renewalCost, String? renewalNote}
);
/// 회사별 라이선스 통계
/// [companyId] 회사 ID
/// Returns: 해당 회사의 라이선스 통계 정보 (전체, 활성, 만료, 만료예정)
Future<Either<Failure, Map<String, int>>> getLicenseStatsByCompany(int companyId);
/// 라이선스 유형별 통계
/// Returns: 라이선스 유형별 개수 (소프트웨어, 하드웨어 등)
Future<Either<Failure, Map<String, int>>> getLicenseCountByType();
/// 라이선스 만료일 사전 알림 설정
/// [licenseId] 라이선스 ID
/// [notifyDays] 만료 N일 전 알림 (기본값: 30일)
/// Returns: 알림 설정 성공/실패 여부
Future<Either<Failure, void>> setExpiryNotification(int licenseId, {int notifyDays = 30});
/// 라이선스 검색 (자동완성용)
/// [query] 검색 쿼리
/// [companyId] 회사 ID 필터 (선택적)
/// [limit] 결과 제한 수 (기본값: 10)
/// Returns: 일치하는 라이선스 목록
Future<Either<Failure, List<License>>> searchLicenses(String query, {int? companyId, int? limit});
}

View File

@@ -0,0 +1,42 @@
import '../../data/models/rent_dto.dart';
abstract class RentRepository {
/// 임대 목록 조회
Future<RentListResponse> getRents({
int page = 1,
int pageSize = 10,
String? search,
String? status,
int? equipmentHistoryId,
});
/// 임대 상세 조회
Future<RentDto> getRent(int id);
/// 임대 생성
Future<RentDto> createRent(RentRequestDto request);
/// 임대 수정
Future<RentDto> updateRent(int id, RentUpdateRequestDto request);
/// 임대 삭제
Future<void> deleteRent(int id);
/// 진행 중인 임대 목록
Future<RentListResponse> getActiveRents({
int page = 1,
int pageSize = 10,
});
/// 연체된 임대 목록
Future<RentListResponse> getOverdueRents({
int page = 1,
int pageSize = 10,
});
/// 임대 통계
Future<Map<String, dynamic>> getRentStats();
/// 장비 반납 처리
Future<RentDto> returnRent(int id, String returnDate);
}

View File

@@ -24,17 +24,17 @@ abstract class UserRepository {
/// Returns: 사용자 상세 정보
Future<Either<Failure, User>> getUserById(int id);
/// 사용자 계정 생성
/// [user] 생성할 사용자 정보 (username, email, name, role 필수)
/// [password] 초기 비밀번호 (필수, 6자 이상)
/// 사용자 계정 생성 (백엔드 API v1 준수)
/// [name] 사용자 이름 (필수)
/// [email] 이메일 (선택적)
/// [phone] 전화번호 (선택적)
/// [companiesId] 회사 ID (필수)
/// Returns: 생성된 사용자 정보 (ID 포함)
Future<Either<Failure, User>> createUser({
required String username,
required String email,
required String password,
required String name,
String? email,
String? phone,
required UserRole role,
required int companiesId,
});
/// 사용자 정보 수정
@@ -49,8 +49,8 @@ abstract class UserRepository {
/// Returns: 삭제 성공/실패 여부
Future<Either<Failure, void>> deleteUser(int id);
/// 사용자명 사용 가능 여부 확인
/// [username] 체크할 사용자
/// Returns: 사용 가능 여부 응답
Future<Either<Failure, bool>> checkUsernameAvailability(String username);
/// 사용자 이름 중복 확인 (백엔드 API v1에서는 미지원)
/// [name] 체크할 사용자 이름
/// Returns: 사용 가능 여부 응답 (항상 true 반환)
Future<Either<Failure, bool>> checkUsernameAvailability(String name);
}

View File

@@ -0,0 +1,328 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import 'package:superport/core/errors/failures.dart';
import 'package:superport/data/models/administrator_dto.dart';
import 'package:superport/domain/repositories/administrator_repository.dart';
import 'package:superport/utils/constants.dart';
/// 관리자 UseCase 인터페이스 (비즈니스 로직)
abstract class AdministratorUseCase {
Future<Either<Failure, AdministratorListResponse>> getAdministrators({
int page = 1,
int pageSize = PaginationConstants.defaultPageSize,
String? search,
});
Future<Either<Failure, AdministratorDto>> getAdministratorById(int id);
Future<Either<Failure, AdministratorDto>> createAdministrator(
AdministratorRequestDto administrator,
);
Future<Either<Failure, AdministratorDto>> updateAdministrator(
int id,
AdministratorUpdateRequestDto administrator,
);
Future<Either<Failure, void>> deleteAdministrator(int id);
Future<Either<Failure, bool>> checkEmailDuplicate(String email, {int? excludeId});
Future<Either<Failure, AdministratorDto>> authenticateAdministrator(
String email,
String password,
);
Future<Either<Failure, bool>> validateAdministratorData(
String name,
String email,
String phone,
String mobile,
);
}
/// 관리자 UseCase 구현체
@Injectable(as: AdministratorUseCase)
class AdministratorUseCaseImpl implements AdministratorUseCase {
final AdministratorRepository _repository;
AdministratorUseCaseImpl(this._repository);
@override
Future<Either<Failure, AdministratorListResponse>> getAdministrators({
int page = 1,
int pageSize = PaginationConstants.defaultPageSize,
String? search,
}) async {
// 비즈니스 로직: 페이지네이션 유효성 검사
if (page < 1) page = 1;
if (pageSize < 1 || pageSize > 100) pageSize = 20;
return await _repository.getAdministrators(
page: page,
pageSize: pageSize,
search: search,
);
}
@override
Future<Either<Failure, AdministratorDto>> getAdministratorById(int id) async {
// 비즈니스 규칙: ID 유효성 검사
if (id <= 0) {
return Left(ValidationFailure(
message: '유효하지 않은 관리자 ID입니다.',
));
}
return await _repository.getAdministratorById(id);
}
@override
Future<Either<Failure, AdministratorDto>> createAdministrator(
AdministratorRequestDto administrator,
) async {
// 비즈니스 규칙 검증
final validationResult = await validateAdministratorData(
administrator.name,
administrator.email,
administrator.phone,
administrator.mobile,
);
return validationResult.fold(
(failure) => Left(failure),
(isValid) async {
if (!isValid) {
return Left(ValidationFailure(
message: '관리자 정보가 유효하지 않습니다.',
));
}
// 이메일 중복 검사
final duplicateResult = await checkEmailDuplicate(administrator.email);
return duplicateResult.fold(
(failure) => Left(failure),
(isDuplicate) async {
if (isDuplicate) {
return Left(DuplicateFailure(
message: '이미 사용 중인 이메일 주소입니다.',
));
}
return await _repository.createAdministrator(administrator);
},
);
},
);
}
@override
Future<Either<Failure, AdministratorDto>> updateAdministrator(
int id,
AdministratorUpdateRequestDto administrator,
) async {
// 비즈니스 규칙: ID 유효성 검사
if (id <= 0) {
return Left(ValidationFailure(
message: '유효하지 않은 관리자 ID입니다.',
));
}
// 수정할 데이터가 있는 경우에만 검증
if (administrator.name != null ||
administrator.email != null ||
administrator.phone != null ||
administrator.mobile != null) {
// 현재 데이터 조회 (검증용)
final currentResult = await _repository.getAdministratorById(id);
return currentResult.fold(
(failure) => Left(failure),
(current) async {
// 새로운 값들로 검증
final newName = administrator.name ?? current.name;
final newEmail = administrator.email ?? current.email;
final newPhone = administrator.phone ?? current.phone;
final newMobile = administrator.mobile ?? current.mobile;
final validationResult = await validateAdministratorData(
newName,
newEmail,
newPhone,
newMobile,
);
return validationResult.fold(
(failure) => Left(failure),
(isValid) async {
if (!isValid) {
return Left(ValidationFailure(
message: '관리자 정보가 유효하지 않습니다.',
));
}
// 이메일이 변경된 경우 중복 검사
if (administrator.email != null && administrator.email != current.email) {
final duplicateResult = await checkEmailDuplicate(
administrator.email!,
excludeId: id,
);
return duplicateResult.fold(
(failure) => Left(failure),
(isDuplicate) async {
if (isDuplicate) {
return Left(DuplicateFailure(
message: '이미 사용 중인 이메일 주소입니다.',
));
}
return await _repository.updateAdministrator(id, administrator);
},
);
}
return await _repository.updateAdministrator(id, administrator);
},
);
},
);
}
return await _repository.updateAdministrator(id, administrator);
}
@override
Future<Either<Failure, void>> deleteAdministrator(int id) async {
// 비즈니스 규칙: ID 유효성 검사
if (id <= 0) {
return Left(ValidationFailure(
message: '유효하지 않은 관리자 ID입니다.',
));
}
// 비즈니스 로직: 관리자가 최소 1명은 남아있어야 함
final administratorsResult = await _repository.getAdministrators(
page: 1,
pageSize: 5,
);
return administratorsResult.fold(
(failure) => Left(failure),
(administrators) async {
if (administrators.totalCount <= 1) {
return Left(ValidationFailure(
message: '최소 1명의 관리자가 존재해야 합니다.',
));
}
return await _repository.deleteAdministrator(id);
},
);
}
@override
Future<Either<Failure, bool>> checkEmailDuplicate(String email, {int? excludeId}) async {
// 이메일 형식 검증
if (!_isValidEmail(email)) {
return Left(ValidationFailure(
message: '유효하지 않은 이메일 형식입니다.',
));
}
return await _repository.isDuplicateEmail(email, excludeId: excludeId);
}
@override
Future<Either<Failure, AdministratorDto>> authenticateAdministrator(
String email,
String password,
) async {
// 입력값 검증
if (email.trim().isEmpty || password.isEmpty) {
return Left(ValidationFailure(
message: '이메일과 비밀번호를 입력해주세요.',
));
}
if (!_isValidEmail(email)) {
return Left(ValidationFailure(
message: '유효하지 않은 이메일 형식입니다.',
));
}
return await _repository.authenticateAdministrator(email, password);
}
@override
Future<Either<Failure, bool>> validateAdministratorData(
String name,
String email,
String phone,
String mobile,
) async {
// 필수 필드 검증
if (name.trim().isEmpty) {
return Left(ValidationFailure(
message: '관리자 이름을 입력해주세요.',
));
}
if (email.trim().isEmpty) {
return Left(ValidationFailure(
message: '이메일 주소를 입력해주세요.',
));
}
if (phone.trim().isEmpty) {
return Left(ValidationFailure(
message: '전화번호를 입력해주세요.',
));
}
if (mobile.trim().isEmpty) {
return Left(ValidationFailure(
message: '휴대폰 번호를 입력해주세요.',
));
}
// 이메일 형식 검증
if (!_isValidEmail(email)) {
return Left(ValidationFailure(
message: '유효하지 않은 이메일 형식입니다.',
));
}
// 이름 길이 검증
if (name.length > 100) {
return Left(ValidationFailure(
message: '관리자 이름은 100자 이내로 입력해주세요.',
));
}
// 전화번호 형식 검증 (숫자, 하이픈, 공백만 허용)
if (!_isValidPhoneNumber(phone)) {
return Left(ValidationFailure(
message: '유효하지 않은 전화번호 형식입니다.',
));
}
if (!_isValidPhoneNumber(mobile)) {
return Left(ValidationFailure(
message: '유효하지 않은 휴대폰 번호 형식입니다.',
));
}
return const Right(true);
}
/// 이메일 형식 검증 (간단한 정규식)
bool _isValidEmail(String email) {
return RegExp(r'^[^\s@]+@[^\s@]+\.[^\s@]+$').hasMatch(email.trim());
}
/// 전화번호 형식 검증 (한국 형식 기준)
bool _isValidPhoneNumber(String phone) {
final cleaned = phone.replaceAll(RegExp(r'[\s\-\(\)]'), '');
return RegExp(r'^\d{8,11}$').hasMatch(cleaned);
}
}

View File

@@ -1,4 +1,5 @@
/// Auth 도메인 UseCase 모음
library;
export 'login_usecase.dart';
export 'logout_usecase.dart';
export 'refresh_token_usecase.dart';

View File

@@ -1,7 +1,13 @@
/// Company 도메인 UseCase 모음
library;
export 'get_companies_usecase.dart';
export 'create_company_usecase.dart';
export 'update_company_usecase.dart';
export 'delete_company_usecase.dart';
export 'get_company_detail_usecase.dart';
export 'toggle_company_status_usecase.dart';
export 'toggle_company_status_usecase.dart';
// 계층 구조 관련 UseCase
export 'get_company_hierarchy_usecase.dart';
export 'update_parent_company_usecase.dart';
export 'validate_company_deletion_usecase.dart';

View File

@@ -0,0 +1,128 @@
import 'package:dartz/dartz.dart';
import '../../../core/errors/failures.dart';
import '../../../domain/entities/company_hierarchy.dart';
import '../../../models/company_model.dart';
import '../../../services/company_service.dart';
import '../base_usecase.dart';
/// 회사 계층 구조 조회 파라미터
class GetCompanyHierarchyParams {
final bool includeInactive;
const GetCompanyHierarchyParams({
this.includeInactive = false,
});
}
/// 회사 계층 구조 조회 UseCase
class GetCompanyHierarchyUseCase extends UseCase<CompanyHierarchy, GetCompanyHierarchyParams> {
final CompanyService _companyService;
GetCompanyHierarchyUseCase(this._companyService);
@override
Future<Either<Failure, CompanyHierarchy>> call(GetCompanyHierarchyParams params) async {
try {
// 모든 회사 조회
final response = await _companyService.getCompanies(
page: 1,
perPage: 1000,
includeInactive: params.includeInactive,
);
// 계층 구조로 변환
final hierarchy = _buildHierarchy(response.items);
return Right(hierarchy);
} on ServerFailure catch (e) {
return Left(ServerFailure(
message: e.message,
originalError: e,
));
} catch (e) {
return Left(UnknownFailure(
message: '회사 계층 구조 조회 중 오류가 발생했습니다.',
originalError: e,
));
}
}
/// 회사 목록을 계층 구조로 변환
CompanyHierarchy _buildHierarchy(List<Company> companies) {
// 루트 회사들 찾기 (parent_company_id가 null인 회사들)
final rootCompanies = companies.where((c) => c.parentCompanyId == null).toList();
// 계층 구조 생성
final children = rootCompanies.map((company) =>
_buildCompanyNode(company, companies, 0)
).toList();
return CompanyHierarchy(
id: '0',
name: 'Root',
children: children,
totalDescendants: _countDescendants(children),
);
}
/// 회사 노드 생성 (재귀)
CompanyHierarchy _buildCompanyNode(
Company company,
List<Company> allCompanies,
int level,
) {
// 자식 회사들 찾기
final childCompanies = allCompanies
.where((c) => c.parentCompanyId == company.id)
.toList();
// 자식 노드들 생성
final children = childCompanies
.map((child) => _buildCompanyNode(child, allCompanies, level + 1))
.toList();
return CompanyHierarchy(
id: company.id.toString(),
name: company.name,
parentId: company.parentCompanyId?.toString(),
children: children,
level: level,
fullPath: _buildPath(company, allCompanies),
totalDescendants: _countDescendants(children),
);
}
/// 경로 생성
String _buildPath(Company company, List<Company> allCompanies) {
final path = <String>[company.name];
Company? current = company;
while (current?.parentCompanyId != null) {
final parent = allCompanies.firstWhere(
(c) => c.id == current!.parentCompanyId,
orElse: () => Company(
id: 0,
name: '',
),
);
if (parent.id == 0) break;
path.insert(0, parent.name);
current = parent;
}
return '/${path.join('/')}';
}
/// 자손 수 계산
int _countDescendants(List<CompanyHierarchy> children) {
int count = children.length;
for (final child in children) {
count += child.totalDescendants;
}
return count;
}
}

View File

@@ -0,0 +1,110 @@
import 'package:dartz/dartz.dart';
import '../../../core/errors/failures.dart';
import '../../../core/utils/hierarchy_validator.dart';
import '../../../models/company_model.dart';
import '../../../services/company_service.dart';
import '../base_usecase.dart';
import '../../../data/models/company/company_dto.dart';
/// 부모 회사 변경 파라미터
class UpdateParentCompanyParams {
final int companyId;
final int? newParentId;
const UpdateParentCompanyParams({
required this.companyId,
required this.newParentId,
});
}
/// 부모 회사 변경 UseCase
class UpdateParentCompanyUseCase extends UseCase<Company, UpdateParentCompanyParams> {
final CompanyService _companyService;
UpdateParentCompanyUseCase(this._companyService);
@override
Future<Either<Failure, Company>> call(UpdateParentCompanyParams params) async {
try {
// 1. 모든 회사 조회 (검증용)
final response = await _companyService.getCompanies(
page: 1,
perPage: 1000,
);
// CompanyDto 리스트로 변환 (검증용)
final companyResponses = response.items.map((company) => CompanyDto(
id: company.id ?? 0,
name: company.name,
address: company.address.toString(),
contactName: company.contactName ?? '',
contactPhone: company.contactPhone ?? '',
contactEmail: company.contactEmail ?? '',
isActive: true,
parentCompanyId: company.parentCompanyId,
registeredAt: DateTime.now(),
)).toList();
// 2. 순환 참조 검증
final circularValidation = HierarchyValidator.validateCircularReference(
companyId: params.companyId,
newParentId: params.newParentId,
allCompanies: companyResponses,
);
if (!circularValidation.isValid) {
return Left(ValidationFailure(
message: circularValidation.message,
));
}
// 3. 계층 깊이 검증
final depthValidation = HierarchyValidator.validateDepth(
parentId: params.newParentId,
allCompanies: companyResponses,
);
if (!depthValidation.isValid) {
return Left(ValidationFailure(
message: depthValidation.message,
));
}
// 4. 부모 변경 가능 여부 전체 검증
final changeValidation = HierarchyValidator.validateParentChange(
companyId: params.companyId,
newParentId: params.newParentId,
allCompanies: companyResponses,
);
if (!changeValidation.isValid) {
return Left(ValidationFailure(
message: changeValidation.message,
));
}
// 5. 현재 회사 정보 조회
final currentCompany = await _companyService.getCompanyDetail(params.companyId);
// 6. 부모 회사 ID만 변경
final updatedCompany = currentCompany.copyWith(
parentCompanyId: params.newParentId,
);
// 7. 업데이트 실행
final result = await _companyService.updateCompany(params.companyId, updatedCompany);
return Right(result);
} on ServerFailure catch (e) {
return Left(ServerFailure(
message: e.message,
originalError: e,
));
} catch (e) {
return Left(UnknownFailure(
message: '부모 회사 변경 중 오류가 발생했습니다.',
originalError: e,
));
}
}
}

View File

@@ -0,0 +1,110 @@
import 'package:dartz/dartz.dart';
import '../../../core/errors/failures.dart';
import '../../../core/utils/hierarchy_validator.dart';
import '../../../services/company_service.dart';
import '../base_usecase.dart';
import '../../../data/models/company/company_dto.dart';
/// 회사 삭제 가능 여부 검증 파라미터
class ValidateCompanyDeletionParams {
final int companyId;
const ValidateCompanyDeletionParams({
required this.companyId,
});
}
/// 회사 삭제 가능 여부 검증 결과
class CompanyDeletionValidationResult {
final bool canDelete;
final String message;
final List<String> blockers;
const CompanyDeletionValidationResult({
required this.canDelete,
required this.message,
this.blockers = const [],
});
}
/// 회사 삭제 가능 여부 검증 UseCase
class ValidateCompanyDeletionUseCase extends UseCase<CompanyDeletionValidationResult, ValidateCompanyDeletionParams> {
final CompanyService _companyService;
ValidateCompanyDeletionUseCase(this._companyService);
@override
Future<Either<Failure, CompanyDeletionValidationResult>> call(ValidateCompanyDeletionParams params) async {
try {
final blockers = <String>[];
// 1. 자식 회사 존재 여부 확인
final response = await _companyService.getCompanies(
page: 1,
perPage: 1000,
);
// CompanyDto 리스트로 변환 (검증용)
final companyResponses = response.items.map((company) => CompanyDto(
id: company.id ?? 0,
name: company.name,
address: company.address.toString(),
contactName: company.contactName ?? '',
contactPhone: company.contactPhone ?? '',
contactEmail: company.contactEmail ?? '',
isActive: true,
parentCompanyId: company.parentCompanyId,
registeredAt: DateTime.now(),
)).toList();
// HierarchyValidator를 사용한 삭제 가능 여부 검증
final deletionValidation = HierarchyValidator.validateDeletion(
companyId: params.companyId,
allCompanies: companyResponses,
);
if (!deletionValidation.isValid) {
blockers.add(deletionValidation.message);
blockers.addAll(deletionValidation.errors);
}
// 2. 연결된 사용자 존재 여부 확인
// TODO: CompanyService에 hasLinkedUsers 메서드 추가 후 활성화
// final hasUsers = await _companyService.hasLinkedUsers(params.companyId);
// if (hasUsers) {
// blockers.add('이 회사에 연결된 사용자가 존재합니다.');
// }
// 3. 연결된 장비 존재 여부 확인
// TODO: CompanyService에 hasLinkedEquipment 메서드 추가 후 활성화
// final hasEquipment = await _companyService.hasLinkedEquipment(params.companyId);
// if (hasEquipment) {
// blockers.add('이 회사에 연결된 장비가 존재합니다.');
// }
// 결과 생성
if (blockers.isEmpty) {
return const Right(CompanyDeletionValidationResult(
canDelete: true,
message: '이 회사를 삭제할 수 있습니다.',
));
} else {
return Right(CompanyDeletionValidationResult(
canDelete: false,
message: '이 회사를 삭제할 수 없습니다.',
blockers: blockers,
));
}
} on ServerFailure catch (e) {
return Left(ServerFailure(
message: e.message,
originalError: e,
));
} catch (e) {
return Left(UnknownFailure(
message: '회사 삭제 가능 여부 검증 중 오류가 발생했습니다.',
originalError: e,
));
}
}
}

View File

@@ -0,0 +1,54 @@
import 'package:dartz/dartz.dart';
import '../../repositories/equipment_repository.dart';
import '../../../data/models/equipment/equipment_dto.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 장비 생성 UseCase
class CreateEquipmentUseCase extends UseCase<EquipmentDto, EquipmentRequestDto> {
final EquipmentRepository _equipmentRepository;
CreateEquipmentUseCase(this._equipmentRepository);
@override
Future<Either<Failure, EquipmentDto>> call(EquipmentRequestDto params) async {
// 입력 검증
if (params.companiesId <= 0) {
return Left(ValidationFailure(
message: '회사를 선택해주세요.',
errors: {'companiesId': '회사는 필수 선택 항목입니다.'},
));
}
if (params.modelsId <= 0) {
return Left(ValidationFailure(
message: '모델을 선택해주세요.',
errors: {'modelsId': '모델은 필수 선택 항목입니다.'},
));
}
if (params.serialNumber.trim().isEmpty) {
return Left(ValidationFailure(
message: '시리얼 번호를 입력해주세요.',
errors: {'serialNumber': '시리얼 번호는 필수 입력 항목입니다.'},
));
}
if (params.warrantyNumber.trim().isEmpty) {
return Left(ValidationFailure(
message: '워런티 번호를 입력해주세요.',
errors: {'warrantyNumber': '워런티 번호는 필수 입력 항목입니다.'},
));
}
// 워런티 기간 검증
if (params.warrantyStartedAt.isAfter(params.warrantyEndedAt)) {
return Left(ValidationFailure(
message: '워런티 시작일이 종료일보다 늦을 수 없습니다.',
errors: {'warrantyPeriod': '워런티 기간을 올바르게 설정해주세요.'},
));
}
return await _equipmentRepository.createEquipment(params);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:dartz/dartz.dart';
import '../../repositories/equipment_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 장비 삭제 UseCase
class DeleteEquipmentUseCase extends UseCase<void, int> {
final EquipmentRepository _equipmentRepository;
DeleteEquipmentUseCase(this._equipmentRepository);
@override
Future<Either<Failure, void>> call(int equipmentId) async {
if (equipmentId <= 0) {
return Left(ValidationFailure(
message: '올바르지 않은 장비 ID입니다.',
errors: {'equipmentId': '장비 ID는 0보다 커야 합니다.'},
));
}
return await _equipmentRepository.deleteEquipment(equipmentId);
}
}

View File

@@ -1,39 +1,35 @@
import 'package:dartz/dartz.dart';
import '../../../services/equipment_service.dart';
import '../../../data/models/equipment/equipment_io_response.dart';
import '../../../data/models/equipment_history_dto.dart';
import '../../../core/errors/failures.dart';
import '../equipment_history_usecase.dart';
import '../base_usecase.dart';
/// 장비 입고 파라미터
/// 장비 입고 파라미터 (백엔드 스키마 기반)
class EquipmentInParams {
final int equipmentId;
final int warehouseLocationId;
final int equipmentsId;
final int warehousesId;
final int quantity;
final String serialNumber;
final DateTime? transactedAt;
final String? remark;
final DateTime? purchaseDate;
final double? purchasePrice;
const EquipmentInParams({
required this.equipmentId,
required this.warehouseLocationId,
required this.equipmentsId,
required this.warehousesId,
required this.quantity,
required this.serialNumber,
this.transactedAt,
this.remark,
this.purchaseDate,
this.purchasePrice,
});
}
/// 장비 입고 UseCase
/// 장비 입고 UseCase (백엔드 스키마 기반)
/// 새로운 장비를 창고에 입고 처리
class EquipmentInUseCase extends UseCase<EquipmentIoResponse, EquipmentInParams> {
final EquipmentService _equipmentService;
class EquipmentInUseCase extends UseCase<EquipmentHistoryDto, EquipmentInParams> {
final EquipmentHistoryUseCase _equipmentHistoryUseCase;
EquipmentInUseCase(this._equipmentService);
EquipmentInUseCase(this._equipmentHistoryUseCase);
@override
Future<Either<Failure, EquipmentIoResponse>> call(EquipmentInParams params) async {
Future<Either<Failure, EquipmentHistoryDto>> call(EquipmentInParams params) async {
try {
// 유효성 검증
final validationResult = _validateInput(params);
@@ -41,29 +37,21 @@ class EquipmentInUseCase extends UseCase<EquipmentIoResponse, EquipmentInParams>
return Left(validationResult);
}
// 시리얼 번호 중복 체크 (프론트엔드 임시 로직)
// TODO: 백엔드 API 구현 후 제거
final response = await _equipmentService.equipmentIn(
equipmentId: params.equipmentId,
// 백엔드 EquipmentHistoryUseCase를 통한 입고 처리
final response = await _equipmentHistoryUseCase.createStockIn(
equipmentsId: params.equipmentsId,
warehousesId: params.warehousesId,
quantity: params.quantity,
warehouseLocationId: params.warehouseLocationId,
notes: params.remark,
transactedAt: params.transactedAt,
remark: params.remark,
);
return Right(response);
} catch (e) {
if (e.toString().contains('시리얼')) {
if (e.toString().contains('수량')) {
return Left(ValidationFailure(
message: '이미 등록된 시리얼 번호입니다.',
code: 'DUPLICATE_SERIAL',
errors: {'serialNumber': '중복된 시리얼 번호입니다.'},
originalError: e,
));
} else if (e.toString().contains('재고')) {
return Left(ValidationFailure(
message: '재고 수량이 부족합니다.',
code: 'INSUFFICIENT_STOCK',
message: '입고 수량을 확인해주세요.',
code: 'INVALID_QUANTITY',
originalError: e,
));
} else if (e.toString().contains('권한')) {
@@ -92,22 +80,9 @@ class EquipmentInUseCase extends UseCase<EquipmentIoResponse, EquipmentInParams>
errors['quantity'] = '한 번에 입고 가능한 최대 수량은 999개입니다.';
}
// 시리얼 번호 검증
if (params.serialNumber.isEmpty) {
errors['serialNumber'] = '시리얼 번호를 입력해주세요.';
}
if (!RegExp(r'^[A-Za-z0-9-]+$').hasMatch(params.serialNumber)) {
errors['serialNumber'] = '시리얼 번호는 영문, 숫자, 하이픈만 사용 가능합니다.';
}
// 구매 가격 검증 (선택사항)
if (params.purchasePrice != null && params.purchasePrice! < 0) {
errors['purchasePrice'] = '구매 가격은 0 이상이어야 합니다.';
}
// 구매 날짜 검증 (선택사항)
if (params.purchaseDate != null && params.purchaseDate!.isAfter(DateTime.now())) {
errors['purchaseDate'] = '구매 날짜는 미래 날짜일 수 없습니다.';
// 날짜 검증 (선택사항)
if (params.transactedAt != null && params.transactedAt!.isAfter(DateTime.now())) {
errors['transactedAt'] = '입고 날짜는 미래 날짜일 수 없습니다.';
}
if (errors.isNotEmpty) {

View File

@@ -1,39 +1,35 @@
import 'package:dartz/dartz.dart';
import '../../../services/equipment_service.dart';
import '../../../data/models/equipment/equipment_io_response.dart';
import '../../../data/models/equipment_history_dto.dart';
import '../../../core/errors/failures.dart';
import '../equipment_history_usecase.dart';
import '../base_usecase.dart';
/// 장비 출고 파라미터
/// 장비 출고 파라미터 (백엔드 스키마 기반)
class EquipmentOutParams {
final int equipmentInId;
final int companyId;
final int equipmentsId;
final int warehousesId;
final int quantity;
final DateTime? transactedAt;
final String? remark;
final String? recipientName;
final String? recipientPhone;
final DateTime? deliveryDate;
const EquipmentOutParams({
required this.equipmentInId,
required this.companyId,
required this.equipmentsId,
required this.warehousesId,
required this.quantity,
this.transactedAt,
this.remark,
this.recipientName,
this.recipientPhone,
this.deliveryDate,
});
}
/// 장비 출고 UseCase
/// 창고에서 회사로 장비 출고 처리
class EquipmentOutUseCase extends UseCase<EquipmentIoResponse, EquipmentOutParams> {
final EquipmentService _equipmentService;
/// 장비 출고 UseCase (백엔드 스키마 기반)
/// 창고에서 장비 출고 처리
class EquipmentOutUseCase extends UseCase<EquipmentHistoryDto, EquipmentOutParams> {
final EquipmentHistoryUseCase _equipmentHistoryUseCase;
EquipmentOutUseCase(this._equipmentService);
EquipmentOutUseCase(this._equipmentHistoryUseCase);
@override
Future<Either<Failure, EquipmentIoResponse>> call(EquipmentOutParams params) async {
Future<Either<Failure, EquipmentHistoryDto>> call(EquipmentOutParams params) async {
try {
// 유효성 검증
final validationResult = _validateInput(params);
@@ -41,11 +37,13 @@ class EquipmentOutUseCase extends UseCase<EquipmentIoResponse, EquipmentOutParam
return Left(validationResult);
}
final response = await _equipmentService.equipmentOut(
equipmentId: params.equipmentInId, // equipmentInId를 equipmentId로 사용
// 백엔드 EquipmentHistoryUseCase를 통한 출고 처리
final response = await _equipmentHistoryUseCase.createStockOut(
equipmentsId: params.equipmentsId,
warehousesId: params.warehousesId,
quantity: params.quantity,
companyId: params.companyId,
notes: params.remark,
transactedAt: params.transactedAt,
remark: params.remark,
);
return Right(response);
@@ -56,10 +54,10 @@ class EquipmentOutUseCase extends UseCase<EquipmentIoResponse, EquipmentOutParam
code: 'INSUFFICIENT_STOCK',
originalError: e,
));
} else if (e.toString().contains('찾을 수 없')) {
} else if (e.toString().contains('수량')) {
return Left(ValidationFailure(
message: '장비 정보를 찾을 수 없습니다.',
code: 'EQUIPMENT_NOT_FOUND',
message: '출고 수량을 확인해주세요.',
code: 'INVALID_QUANTITY',
originalError: e,
));
} else if (e.toString().contains('권한')) {
@@ -88,20 +86,9 @@ class EquipmentOutUseCase extends UseCase<EquipmentIoResponse, EquipmentOutParam
errors['quantity'] = '한 번에 출고 가능한 최대 수량은 999개입니다.';
}
// 수령자 정보 검증 (선택사항이지만 제공된 경우)
if (params.recipientName != null && params.recipientName!.isEmpty) {
errors['recipientName'] = '수령자 이름을 입력해주세요.';
}
if (params.recipientPhone != null && params.recipientPhone!.isNotEmpty) {
if (!RegExp(r'^01[0-9]{1}-?[0-9]{4}-?[0-9]{4}$').hasMatch(params.recipientPhone!)) {
errors['recipientPhone'] = '올바른 전화번호 형식이 아닙니다.';
}
}
// 배송 날짜 검증 (선택사항)
if (params.deliveryDate != null && params.deliveryDate!.isBefore(DateTime.now().subtract(Duration(days: 1)))) {
errors['deliveryDate'] = '배송 날짜는 과거 날짜일 수 없습니다.';
// 날짜 검증 (선택사항)
if (params.transactedAt != null && params.transactedAt!.isAfter(DateTime.now())) {
errors['transactedAt'] = '출고 날짜는 미래 날짜일 수 없습니다.';
}
if (errors.isNotEmpty) {

View File

@@ -1,5 +1,7 @@
/// Equipment 도메인 UseCase 모음
library;
export 'get_equipments_usecase.dart';
export 'equipment_in_usecase.dart';
export 'equipment_out_usecase.dart';
export 'get_equipment_history_usecase.dart';
export 'get_equipment_detail_usecase.dart';
export 'create_equipment_usecase.dart';
export 'update_equipment_usecase.dart';
export 'delete_equipment_usecase.dart';

View File

@@ -0,0 +1,24 @@
import 'package:dartz/dartz.dart';
import '../../repositories/equipment_repository.dart';
import '../../../data/models/equipment/equipment_dto.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 장비 상세 조회 UseCase
class GetEquipmentDetailUseCase extends UseCase<EquipmentDto, int> {
final EquipmentRepository _equipmentRepository;
GetEquipmentDetailUseCase(this._equipmentRepository);
@override
Future<Either<Failure, EquipmentDto>> call(int equipmentId) async {
if (equipmentId <= 0) {
return Left(ValidationFailure(
message: '올바르지 않은 장비 ID입니다.',
errors: {'equipmentId': '장비 ID는 0보다 커야 합니다.'},
));
}
return await _equipmentRepository.getEquipmentDetail(equipmentId);
}
}

View File

@@ -1,8 +1,8 @@
import 'package:dartz/dartz.dart';
import '../../../services/equipment_service.dart';
import '../../../data/models/equipment/equipment_history_dto.dart';
import '../../../data/models/equipment_history_dto.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
import '../equipment_history_usecase.dart';
/// 장비 이력 조회 파라미터
class GetEquipmentHistoryParams {
@@ -19,12 +19,12 @@ class GetEquipmentHistoryParams {
});
}
/// 장비 이력 조회 UseCase
/// 특정 장비의 입출고 및 상태 변경 이력 조회
/// 장비 이력 조회 UseCase (백엔드 스키마 기반)
/// 특정 장비의 입출고 이력 조회
class GetEquipmentHistoryUseCase extends UseCase<List<EquipmentHistoryDto>, GetEquipmentHistoryParams> {
final EquipmentService _equipmentService;
final EquipmentHistoryUseCase _equipmentHistoryUseCase;
GetEquipmentHistoryUseCase(this._equipmentService);
GetEquipmentHistoryUseCase(this._equipmentHistoryUseCase);
@override
Future<Either<Failure, List<EquipmentHistoryDto>>> call(GetEquipmentHistoryParams params) async {
@@ -48,7 +48,7 @@ class GetEquipmentHistoryUseCase extends UseCase<List<EquipmentHistoryDto>, GetE
));
}
final history = await _equipmentService.getEquipmentHistory(params.equipmentId);
final history = await _equipmentHistoryUseCase.getEquipmentHistoriesByEquipmentId(params.equipmentId);
// 필터링 적용
List<EquipmentHistoryDto> filteredHistory = history;

View File

@@ -1,6 +1,6 @@
import 'package:dartz/dartz.dart';
import '../../../services/equipment_service.dart';
import '../../../models/equipment_unified_model.dart';
import '../../repositories/equipment_repository.dart';
import '../../../data/models/equipment/equipment_dto.dart';
import '../../../core/errors/failures.dart';
import '../../../data/models/common/paginated_response.dart';
import '../base_usecase.dart';
@@ -9,62 +9,27 @@ import '../base_usecase.dart';
class GetEquipmentsParams {
final int page;
final int perPage;
final String? status;
final int? companyId;
final int? warehouseLocationId;
final String? search;
const GetEquipmentsParams({
this.page = 1,
this.perPage = 20,
this.status,
this.companyId,
this.warehouseLocationId,
this.search,
});
}
/// 장비 목록 조회 UseCase
/// 필터링 및 페이지네이션 지원
class GetEquipmentsUseCase extends UseCase<PaginatedResponse<Equipment>, GetEquipmentsParams> {
final EquipmentService _equipmentService;
class GetEquipmentsUseCase extends UseCase<PaginatedResponse<EquipmentDto>, GetEquipmentsParams> {
final EquipmentRepository _equipmentRepository;
GetEquipmentsUseCase(this._equipmentService);
GetEquipmentsUseCase(this._equipmentRepository);
@override
Future<Either<Failure, PaginatedResponse<Equipment>>> call(GetEquipmentsParams params) async {
try {
// 상태 유효성 검증
if (params.status != null &&
!['available', 'in_use', 'maintenance', 'disposed', 'rented'].contains(params.status)) {
return Left(ValidationFailure(
message: '올바르지 않은 장비 상태입니다.',
errors: {'status': '유효한 상태를 선택해주세요.'},
));
}
final equipments = await _equipmentService.getEquipments(
page: params.page,
perPage: params.perPage,
status: params.status,
companyId: params.companyId,
warehouseLocationId: params.warehouseLocationId,
search: params.search,
);
return Right(equipments);
} catch (e) {
if (e.toString().contains('네트워크')) {
return Left(NetworkFailure(
message: '네트워크 연결을 확인해주세요.',
originalError: e,
));
} else {
return Left(ServerFailure(
message: '장비 목록을 불러오는 중 오류가 발생했습니다.',
originalError: e,
));
}
}
Future<Either<Failure, PaginatedResponse<EquipmentDto>>> call(GetEquipmentsParams params) async {
return await _equipmentRepository.getEquipments(
page: params.page,
limit: params.perPage,
search: params.search,
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:dartz/dartz.dart';
import '../../repositories/equipment_repository.dart';
import '../../../data/models/equipment/equipment_dto.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 장비 수정 파라미터
class UpdateEquipmentParams {
final int id;
final EquipmentUpdateRequestDto request;
const UpdateEquipmentParams({
required this.id,
required this.request,
});
}
/// 장비 수정 UseCase
class UpdateEquipmentUseCase extends UseCase<EquipmentDto, UpdateEquipmentParams> {
final EquipmentRepository _equipmentRepository;
UpdateEquipmentUseCase(this._equipmentRepository);
@override
Future<Either<Failure, EquipmentDto>> call(UpdateEquipmentParams params) async {
// ID 검증
if (params.id <= 0) {
return Left(ValidationFailure(
message: '올바르지 않은 장비 ID입니다.',
errors: {'id': '장비 ID는 0보다 커야 합니다.'},
));
}
// 입력 검증 (값이 제공된 경우만)
if (params.request.companiesId != null && params.request.companiesId! <= 0) {
return Left(ValidationFailure(
message: '회사를 선택해주세요.',
errors: {'companiesId': '올바른 회사를 선택해주세요.'},
));
}
if (params.request.modelsId != null && params.request.modelsId! <= 0) {
return Left(ValidationFailure(
message: '모델을 선택해주세요.',
errors: {'modelsId': '올바른 모델을 선택해주세요.'},
));
}
if (params.request.serialNumber != null && params.request.serialNumber!.trim().isEmpty) {
return Left(ValidationFailure(
message: '시리얼 번호를 입력해주세요.',
errors: {'serialNumber': '시리얼 번호는 비어있을 수 없습니다.'},
));
}
// 워런티 기간 검증
if (params.request.warrantyStartedAt != null &&
params.request.warrantyEndedAt != null &&
params.request.warrantyStartedAt!.isAfter(params.request.warrantyEndedAt!)) {
return Left(ValidationFailure(
message: '워런티 시작일이 종료일보다 늦을 수 없습니다.',
errors: {'warrantyPeriod': '워런티 기간을 올바르게 설정해주세요.'},
));
}
return await _equipmentRepository.updateEquipment(params.id, params.request);
}
}

View File

@@ -0,0 +1,248 @@
import 'package:injectable/injectable.dart';
import 'package:superport/data/models/equipment_history_dto.dart';
import 'package:superport/data/repositories/equipment_history_repository.dart';
@lazySingleton
class EquipmentHistoryUseCase {
final EquipmentHistoryRepository _repository;
EquipmentHistoryUseCase(this._repository);
// 재고 이력 조회
Future<EquipmentHistoryListResponse> getEquipmentHistories({
int? page,
int? pageSize,
int? equipmentsId,
int? warehousesId,
int? companiesId,
String? transactionType,
String? startDate,
String? endDate,
}) async {
return await _repository.getEquipmentHistories(
page: page,
pageSize: pageSize,
equipmentsId: equipmentsId,
warehousesId: warehousesId,
companiesId: companiesId,
transactionType: transactionType,
startDate: startDate,
endDate: endDate,
);
}
// 특정 이력 상세 조회
Future<EquipmentHistoryDto> getEquipmentHistoryById(int id) async {
return await _repository.getEquipmentHistoryById(id);
}
// 장비별 이력 조회
Future<List<EquipmentHistoryDto>> getEquipmentHistoriesByEquipmentId(
int equipmentId,
) async {
return await _repository.getEquipmentHistoriesByEquipmentId(equipmentId);
}
// 창고별 이력 조회
Future<List<EquipmentHistoryDto>> getEquipmentHistoriesByWarehouseId(
int warehouseId,
) async {
return await _repository.getEquipmentHistoriesByWarehouseId(warehouseId);
}
// 장비별 재고 현황 계산 (백엔드 기반)
Future<int> getCurrentStock(int equipmentId, {int? warehouseId}) async {
final histories = await getEquipmentHistoriesByEquipmentId(equipmentId);
int totalStock = 0;
for (final history in histories) {
if (warehouseId != null && history.warehousesId != warehouseId) continue;
if (history.transactionType == TransactionType.input) {
totalStock += history.quantity;
} else if (history.transactionType == TransactionType.output) {
totalStock -= history.quantity;
}
}
return totalStock;
}
// 입고 처리 (백엔드 스키마 기반)
Future<EquipmentHistoryDto> createStockIn({
required int equipmentsId,
required int warehousesId,
required int quantity,
DateTime? transactedAt,
String? remark,
}) async {
// 비즈니스 규칙 검증
if (quantity <= 0) {
throw Exception('입고 수량은 0보다 커야 합니다.');
}
final request = EquipmentHistoryRequestDto(
equipmentsId: equipmentsId,
warehousesId: warehousesId,
transactionType: TransactionType.input,
quantity: quantity,
transactedAt: transactedAt ?? DateTime.now(),
remark: remark,
);
return await _repository.createEquipmentHistory(request);
}
// 출고 처리 (백엔드 스키마 기반)
Future<EquipmentHistoryDto> createStockOut({
required int equipmentsId,
required int warehousesId,
required int quantity,
DateTime? transactedAt,
String? remark,
}) async {
// 비즈니스 규칙 검증
if (quantity <= 0) {
throw Exception('출고 수량은 0보다 커야 합니다.');
}
// 재고 확인
final currentStock = await getCurrentStock(equipmentsId, warehouseId: warehousesId);
if (currentStock < quantity) {
throw Exception('재고가 부족합니다. (현재 재고: $currentStock개)');
}
final request = EquipmentHistoryRequestDto(
equipmentsId: equipmentsId,
warehousesId: warehousesId,
transactionType: TransactionType.output,
quantity: quantity,
transactedAt: transactedAt ?? DateTime.now(),
remark: remark,
);
return await _repository.createEquipmentHistory(request);
}
// 이력 수정
Future<EquipmentHistoryDto> updateEquipmentHistory(
int id,
EquipmentHistoryUpdateRequestDto request,
) async {
// 수정 권한 검증 (필요시 추가)
// 이미 완료된 트랜잭션은 수정 불가 등의 규칙
return await _repository.updateEquipmentHistory(id, request);
}
// 이력 삭제
Future<void> deleteEquipmentHistory(int id) async {
// 삭제 권한 검증 (필요시 추가)
// 감사 추적을 위해 실제로는 soft delete 고려
await _repository.deleteEquipmentHistory(id);
}
// 재고 이동 (창고 간 이동)
Future<void> transferStock({
required int equipmentsId,
required int quantity,
required int fromWarehouseId,
required int toWarehouseId,
String? remark,
}) async {
// 1. 출고 처리 (from warehouse)
await createStockOut(
equipmentsId: equipmentsId,
warehousesId: fromWarehouseId,
quantity: quantity,
remark: remark ?? '창고 이동: $fromWarehouseId$toWarehouseId',
);
// 2. 입고 처리 (to warehouse)
await createStockIn(
equipmentsId: equipmentsId,
warehousesId: toWarehouseId,
quantity: quantity,
remark: remark ?? '창고 이동: $fromWarehouseId$toWarehouseId',
);
}
// 재고 조정 (실사 후 조정)
Future<EquipmentHistoryDto> adjustInventory({
required int equipmentsId,
required int actualQuantity,
required int warehousesId,
String? remark,
}) async {
// 현재 재고 확인
final currentStock = await getCurrentStock(equipmentsId, warehouseId: warehousesId);
final difference = actualQuantity - currentStock;
if (difference == 0) {
throw Exception('재고 조정이 필요하지 않습니다.');
}
if (difference > 0) {
// 재고 증가 - 입고 처리
return await createStockIn(
equipmentsId: equipmentsId,
warehousesId: warehousesId,
quantity: difference,
remark: remark ?? '재고 조정: +$difference개',
);
} else {
// 재고 감소 - 출고 처리
return await createStockOut(
equipmentsId: equipmentsId,
warehousesId: warehousesId,
quantity: -difference,
remark: remark ?? '재고 조정: $difference개',
);
}
}
// 재고 부족 장비 확인 (단순화)
Future<List<int>> checkLowStockEquipments({
int minimumStock = 10,
int? warehouseId,
List<int>? equipmentIds,
}) async {
final lowStockEquipments = <int>[];
// 특정 장비 ID 목록이 제공된 경우, 해당 장비들만 확인
if (equipmentIds != null) {
for (final equipmentId in equipmentIds) {
final currentStock = await getCurrentStock(equipmentId, warehouseId: warehouseId);
if (currentStock < minimumStock) {
lowStockEquipments.add(equipmentId);
}
}
}
return lowStockEquipments;
}
// 이력 생성 (범용)
Future<EquipmentHistoryDto> createEquipmentHistory(
EquipmentHistoryRequestDto request,
) async {
// 비즈니스 규칙 검증
if (request.quantity <= 0) {
throw Exception('수량은 0보다 커야 합니다.');
}
// 출고인 경우 재고 확인
if (request.transactionType == TransactionType.output) {
final currentStock = await getCurrentStock(
request.equipmentsId,
warehouseId: request.warehousesId,
);
if (currentStock < request.quantity) {
throw Exception('재고가 부족합니다. (현재 재고: $currentStock개)');
}
}
return await _repository.createEquipmentHistory(request);
}
}

View File

@@ -1,112 +0,0 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/license/license_dto.dart';
import '../../repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 만료일 체크 UseCase
@injectable
class CheckLicenseExpiryUseCase implements UseCase<LicenseExpiryResult, CheckLicenseExpiryParams> {
final LicenseRepository repository;
CheckLicenseExpiryUseCase(this.repository);
@override
Future<Either<Failure, LicenseExpiryResult>> call(CheckLicenseExpiryParams params) async {
try {
// 모든 라이선스 조회
final allLicensesResult = await repository.getLicenses(
page: 1,
limit: 10000, // 모든 라이선스 조회
);
return allLicensesResult.fold(
(failure) => Left(failure),
(paginatedResponse) {
final now = DateTime.now();
final expiring30Days = <LicenseDto>[];
final expiring60Days = <LicenseDto>[];
final expiring90Days = <LicenseDto>[];
final expired = <LicenseDto>[];
for (final license in paginatedResponse.items) {
final licenseDto = LicenseDto(
id: license.id ?? 0,
licenseKey: license.licenseKey,
productName: license.productName,
vendor: license.vendor,
licenseType: license.licenseType,
userCount: license.userCount,
purchaseDate: license.purchaseDate,
expiryDate: license.expiryDate,
purchasePrice: license.purchasePrice,
companyId: license.companyId,
branchId: license.branchId,
assignedUserId: license.assignedUserId,
remark: license.remark,
isActive: license.isActive ?? true,
createdAt: license.createdAt ?? DateTime.now(),
updatedAt: license.updatedAt ?? DateTime.now(),
companyName: license.companyName,
branchName: license.branchName,
assignedUserName: license.assignedUserName,
);
if (licenseDto.expiryDate == null) continue;
final daysUntilExpiry = licenseDto.expiryDate!.difference(now).inDays;
if (daysUntilExpiry < 0) {
expired.add(licenseDto);
} else if (daysUntilExpiry <= 30) {
expiring30Days.add(licenseDto);
} else if (daysUntilExpiry <= 60) {
expiring60Days.add(licenseDto);
} else if (daysUntilExpiry <= 90) {
expiring90Days.add(licenseDto);
}
}
return Right(LicenseExpiryResult(
expiring30Days: expiring30Days,
expiring60Days: expiring60Days,
expiring90Days: expiring90Days,
expired: expired,
));
},
);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}
/// 라이선스 만료일 체크 파라미터
class CheckLicenseExpiryParams {
final int? companyId;
final String? equipmentType;
CheckLicenseExpiryParams({
this.companyId,
this.equipmentType,
});
}
/// 라이선스 만료일 체크 결과
class LicenseExpiryResult {
final List<LicenseDto> expiring30Days;
final List<LicenseDto> expiring60Days;
final List<LicenseDto> expiring90Days;
final List<LicenseDto> expired;
LicenseExpiryResult({
required this.expiring30Days,
required this.expiring60Days,
required this.expiring90Days,
required this.expired,
});
int get totalExpiring => expiring30Days.length + expiring60Days.length + expiring90Days.length;
int get totalExpired => expired.length;
}

View File

@@ -1,99 +0,0 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/license/license_dto.dart';
import '../../../models/license_model.dart';
import '../../repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 생성 UseCase
@injectable
class CreateLicenseUseCase implements UseCase<LicenseDto, CreateLicenseParams> {
final LicenseRepository repository;
CreateLicenseUseCase(this.repository);
@override
Future<Either<Failure, LicenseDto>> call(CreateLicenseParams params) async {
try {
// 비즈니스 로직: 만료일 검증
if (params.expiryDate.isBefore(params.purchaseDate)) {
return Left(ValidationFailure(message: '만료일은 구매일 이후여야 합니다'));
}
// 비즈니스 로직: 최소 라이선스 기간 검증 (30일)
final duration = params.expiryDate.difference(params.purchaseDate).inDays;
if (duration < 30) {
return Left(ValidationFailure(message: '라이선스 기간은 최소 30일 이상이어야 합니다'));
}
final license = License(
licenseKey: params.licenseKey,
productName: params.productName,
vendor: params.vendor,
licenseType: params.licenseType,
userCount: params.userCount,
purchaseDate: params.purchaseDate,
expiryDate: params.expiryDate,
purchasePrice: params.purchasePrice,
companyId: params.companyId,
branchId: params.branchId,
remark: params.remark,
);
final result = await repository.createLicense(license);
return result.map((createdLicense) => LicenseDto(
id: createdLicense.id!,
licenseKey: createdLicense.licenseKey,
productName: createdLicense.productName,
vendor: createdLicense.vendor,
licenseType: createdLicense.licenseType,
userCount: createdLicense.userCount,
purchaseDate: createdLicense.purchaseDate,
expiryDate: createdLicense.expiryDate,
purchasePrice: createdLicense.purchasePrice,
companyId: createdLicense.companyId,
branchId: createdLicense.branchId,
assignedUserId: createdLicense.assignedUserId,
remark: createdLicense.remark,
isActive: createdLicense.isActive,
createdAt: createdLicense.createdAt ?? DateTime.now(),
updatedAt: createdLicense.updatedAt ?? DateTime.now(),
companyName: createdLicense.companyName,
branchName: createdLicense.branchName,
assignedUserName: createdLicense.assignedUserName,
));
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}
/// 라이선스 생성 파라미터
class CreateLicenseParams {
final String licenseKey;
final String productName;
final String? vendor;
final String? licenseType;
final int? userCount;
final DateTime purchaseDate;
final DateTime expiryDate;
final double? purchasePrice;
final int companyId;
final int? branchId;
final String? remark;
CreateLicenseParams({
required this.licenseKey,
required this.productName,
this.vendor,
this.licenseType,
this.userCount,
required this.purchaseDate,
required this.expiryDate,
this.purchasePrice,
required this.companyId,
this.branchId,
this.remark,
});
}

View File

@@ -1,37 +0,0 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 삭제 UseCase
@injectable
class DeleteLicenseUseCase implements UseCase<bool, int> {
final LicenseRepository repository;
DeleteLicenseUseCase(this.repository);
@override
Future<Either<Failure, bool>> call(int id) async {
try {
// 비즈니스 로직: 활성 라이선스는 삭제 불가
final licenseResult = await repository.getLicenseById(id);
return licenseResult.fold(
(failure) => Left(failure),
(license) async {
if (license.isActive == true) {
return Left(ValidationFailure(message: '활성 라이선스는 삭제할 수 없습니다'));
}
final deleteResult = await repository.deleteLicense(id);
return deleteResult.fold(
(failure) => Left(failure),
(_) => const Right(true),
);
},
);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}

View File

@@ -1,44 +0,0 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/license/license_dto.dart';
import '../../repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 상세 조회 UseCase
@injectable
class GetLicenseDetailUseCase implements UseCase<LicenseDto, int> {
final LicenseRepository repository;
GetLicenseDetailUseCase(this.repository);
@override
Future<Either<Failure, LicenseDto>> call(int id) async {
try {
final result = await repository.getLicenseById(id);
return result.map((license) => LicenseDto(
id: license.id ?? 0,
licenseKey: license.licenseKey,
productName: license.productName,
vendor: license.vendor,
licenseType: license.licenseType,
userCount: license.userCount,
purchaseDate: license.purchaseDate,
expiryDate: license.expiryDate,
purchasePrice: license.purchasePrice,
companyId: license.companyId,
branchId: license.branchId,
assignedUserId: license.assignedUserId,
remark: license.remark,
isActive: license.isActive ?? true,
createdAt: license.createdAt ?? DateTime.now(),
updatedAt: license.updatedAt ?? DateTime.now(),
companyName: license.companyName,
branchName: license.branchName,
assignedUserName: license.assignedUserName,
));
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}

View File

@@ -1,85 +0,0 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/common/pagination_params.dart';
import '../../../data/models/license/license_dto.dart';
import '../../repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 목록 조회 UseCase
@injectable
class GetLicensesUseCase implements UseCase<LicenseListResponseDto, GetLicensesParams> {
final LicenseRepository repository;
GetLicensesUseCase(this.repository);
@override
Future<Either<Failure, LicenseListResponseDto>> call(GetLicensesParams params) async {
try {
final result = await repository.getLicenses(
page: params.page,
limit: params.perPage,
search: params.search,
companyId: params.filters?['companyId'],
equipmentType: params.filters?['equipmentType'],
expiryStatus: params.filters?['expiryStatus'],
sortBy: params.filters?['sortBy'],
sortOrder: params.filters?['sortOrder'],
);
return result.map((paginatedResponse) => LicenseListResponseDto(
items: paginatedResponse.items.map((license) => LicenseDto(
id: license.id ?? 0,
licenseKey: license.licenseKey,
productName: license.productName,
vendor: license.vendor,
licenseType: license.licenseType,
userCount: license.userCount,
purchaseDate: license.purchaseDate,
expiryDate: license.expiryDate,
purchasePrice: license.purchasePrice,
companyId: license.companyId,
branchId: license.branchId,
assignedUserId: license.assignedUserId,
remark: license.remark,
isActive: license.isActive ?? true,
createdAt: license.createdAt ?? DateTime.now(),
updatedAt: license.updatedAt ?? DateTime.now(),
companyName: license.companyName,
branchName: license.branchName,
assignedUserName: license.assignedUserName,
)).toList(),
page: paginatedResponse.page,
perPage: params.perPage,
total: paginatedResponse.totalElements,
totalPages: paginatedResponse.totalPages,
));
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}
/// 라이선스 목록 조회 파라미터
class GetLicensesParams {
final int page;
final int perPage;
final String? search;
final Map<String, dynamic>? filters;
GetLicensesParams({
this.page = 1,
this.perPage = 20,
this.search,
this.filters,
});
/// PaginationParams로부터 변환
factory GetLicensesParams.fromPaginationParams(PaginationParams params) {
return GetLicensesParams(
page: params.page,
perPage: params.perPage,
search: params.search,
filters: params.filters,
);
}
}

View File

@@ -1,7 +0,0 @@
// License UseCase barrel file
export 'get_licenses_usecase.dart';
export 'get_license_detail_usecase.dart';
export 'create_license_usecase.dart';
export 'update_license_usecase.dart';
export 'delete_license_usecase.dart';
export 'check_license_expiry_usecase.dart';

View File

@@ -1,110 +0,0 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/license/license_dto.dart';
import '../../../models/license_model.dart';
import '../../repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 수정 UseCase
@injectable
class UpdateLicenseUseCase implements UseCase<LicenseDto, UpdateLicenseParams> {
final LicenseRepository repository;
UpdateLicenseUseCase(this.repository);
@override
Future<Either<Failure, LicenseDto>> call(UpdateLicenseParams params) async {
try {
// 비즈니스 로직: 만료일 검증
if (params.expiryDate != null && params.purchaseDate != null) {
if (params.expiryDate!.isBefore(params.purchaseDate!)) {
return Left(ValidationFailure(message: '만료일은 구매일 이후여야 합니다'));
}
// 비즈니스 로직: 최소 라이선스 기간 검증 (30일)
final duration = params.expiryDate!.difference(params.purchaseDate!).inDays;
if (duration < 30) {
return Left(ValidationFailure(message: '라이선스 기간은 최소 30일 이상이어야 합니다'));
}
}
final license = License(
id: params.id,
licenseKey: params.licenseKey ?? '',
productName: params.productName ?? '',
vendor: params.vendor,
licenseType: params.licenseType,
userCount: params.userCount,
purchaseDate: params.purchaseDate ?? DateTime.now(),
expiryDate: params.expiryDate ?? DateTime.now(),
purchasePrice: params.purchasePrice,
companyId: params.companyId ?? 0,
branchId: params.branchId,
assignedUserId: params.assignedUserId,
remark: params.remark,
isActive: params.isActive ?? true,
);
final result = await repository.updateLicense(params.id ?? 0, license);
return result.map((updatedLicense) => LicenseDto(
id: updatedLicense.id ?? 0,
licenseKey: updatedLicense.licenseKey,
productName: updatedLicense.productName,
vendor: updatedLicense.vendor,
licenseType: updatedLicense.licenseType,
userCount: updatedLicense.userCount,
purchaseDate: updatedLicense.purchaseDate,
expiryDate: updatedLicense.expiryDate,
purchasePrice: updatedLicense.purchasePrice,
companyId: updatedLicense.companyId,
branchId: updatedLicense.branchId,
assignedUserId: updatedLicense.assignedUserId,
remark: updatedLicense.remark,
isActive: updatedLicense.isActive,
createdAt: updatedLicense.createdAt ?? DateTime.now(),
updatedAt: updatedLicense.updatedAt ?? DateTime.now(),
companyName: updatedLicense.companyName,
branchName: updatedLicense.branchName,
assignedUserName: updatedLicense.assignedUserName,
));
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}
/// 라이선스 수정 파라미터
class UpdateLicenseParams {
final int id;
final String? licenseKey;
final String? productName;
final String? vendor;
final String? licenseType;
final int? userCount;
final DateTime? purchaseDate;
final DateTime? expiryDate;
final double? purchasePrice;
final int? companyId;
final int? branchId;
final int? assignedUserId;
final String? remark;
final bool? isActive;
UpdateLicenseParams({
required this.id,
this.licenseKey,
this.productName,
this.vendor,
this.licenseType,
this.userCount,
this.purchaseDate,
this.expiryDate,
this.purchasePrice,
this.companyId,
this.branchId,
this.assignedUserId,
this.remark,
this.isActive,
});
}

View File

@@ -0,0 +1,170 @@
import '../../data/models/maintenance_dto.dart';
import '../../data/repositories/maintenance_repository.dart';
import '../../utils/constants.dart';
/// 유지보수 UseCase (백엔드 스키마 기반)
/// 백엔드 API와 100% 호환되는 단순한 CRUD 작업만 제공
class MaintenanceUseCase {
final MaintenanceRepository _repository;
MaintenanceUseCase({required MaintenanceRepository repository})
: _repository = repository;
// 유지보수 목록 조회 (백엔드 스키마 기반)
Future<MaintenanceListResponse> getMaintenances({
int page = 1,
int pageSize = PaginationConstants.defaultPageSize,
String? sortBy,
String? sortOrder,
String? search,
int? equipmentHistoryId,
String? maintenanceType,
}) async {
return await _repository.getMaintenances(
page: page,
pageSize: pageSize,
sortBy: sortBy,
sortOrder: sortOrder,
search: search,
equipmentHistoryId: equipmentHistoryId,
maintenanceType: maintenanceType,
);
}
// 특정 유지보수 조회
Future<MaintenanceDto> getMaintenance(int id) async {
return await _repository.getMaintenance(id);
}
// 장비 이력별 유지보수 조회
Future<List<MaintenanceDto>> getMaintenancesByEquipmentHistory(
int equipmentHistoryId) async {
return await _repository.getMaintenancesByEquipmentHistory(equipmentHistoryId);
}
// 유지보수 생성 (백엔드 스키마 기반)
Future<MaintenanceDto> createMaintenance(MaintenanceRequestDto request) async {
// 기본 검증만
_validateMaintenanceRequest(request);
return await _repository.createMaintenance(request);
}
// 유지보수 수정 (백엔드 스키마 기반)
Future<MaintenanceDto> updateMaintenance(
int id, MaintenanceUpdateRequestDto request) async {
// 기본 검증만
if (request.periodMonth != null && request.periodMonth! <= 0) {
throw Exception('유지보수 주기는 1개월 이상이어야 합니다.');
}
// 날짜 검증
if (request.startedAt != null && request.endedAt != null) {
if (request.endedAt!.isBefore(request.startedAt!)) {
throw Exception('종료일은 시작일 이후여야 합니다.');
}
}
return await _repository.updateMaintenance(id, request);
}
// 유지보수 삭제 (백엔드 soft delete)
Future<void> deleteMaintenance(int id) async {
// 기본 검증만
final maintenance = await _repository.getMaintenance(id);
// 진행 중인 유지보수는 삭제 불가 (선택적)
final now = DateTime.now();
if (now.isAfter(maintenance.startedAt) && now.isBefore(maintenance.endedAt)) {
throw Exception('진행 중인 유지보수는 삭제할 수 없습니다.');
}
await _repository.deleteMaintenance(id);
}
// 활성 유지보수만 조회 (백엔드 is_deleted = false)
Future<List<MaintenanceDto>> getActiveMaintenances({
int? equipmentHistoryId,
String? maintenanceType,
}) async {
final response = await getMaintenances(
pageSize: 1000, // 큰 사이즈로 모든 데이터 조회
equipmentHistoryId: equipmentHistoryId,
maintenanceType: maintenanceType,
);
// is_deleted = false인 것만 필터링
return response.items.where((m) => m.isActive).toList();
}
// 특정 기간 내 유지보수 조회 (백엔드 날짜 필터링)
Future<List<MaintenanceDto>> getMaintenancesByDateRange({
required DateTime startDate,
required DateTime endDate,
int? equipmentHistoryId,
}) async {
final response = await getMaintenances(
pageSize: 1000,
equipmentHistoryId: equipmentHistoryId,
);
// 날짜 범위 필터링 (클라이언트 사이드)
return response.items.where((maintenance) {
return maintenance.startedAt.isAfter(startDate) &&
maintenance.endedAt.isBefore(endDate);
}).toList();
}
// Private 헬퍼 메서드
void _validateMaintenanceRequest(MaintenanceRequestDto request) {
// 필수 필드 검증 (백엔드 스키마 기반)
if (request.periodMonth <= 0) {
throw Exception('유지보수 주기는 1개월 이상이어야 합니다.');
}
// 날짜 검증
if (request.endedAt.isBefore(request.startedAt)) {
throw Exception('종료일은 시작일 이후여야 합니다.');
}
// 유지보수 타입 검증 (백엔드 값)
if (request.maintenanceType != MaintenanceType.onsite &&
request.maintenanceType != MaintenanceType.remote) {
throw Exception('유지보수 타입은 방문(O) 또는 원격(R)이어야 합니다.');
}
}
// 통계 조회 (단순화)
Future<MaintenanceStatistics> getMaintenanceStatistics() async {
final response = await getMaintenances(pageSize: 1000);
final maintenances = response.items;
int totalCount = maintenances.length;
int activeCount = maintenances.where((m) => m.isActive).length;
int onsiteCount = maintenances.where((m) => m.maintenanceType == MaintenanceType.onsite).length;
int remoteCount = maintenances.where((m) => m.maintenanceType == MaintenanceType.remote).length;
return MaintenanceStatistics(
totalCount: totalCount,
activeCount: activeCount,
onsiteCount: onsiteCount,
remoteCount: remoteCount,
);
}
}
/// 유지보수 통계 모델 (단순화)
class MaintenanceStatistics {
final int totalCount;
final int activeCount;
final int onsiteCount;
final int remoteCount;
MaintenanceStatistics({
required this.totalCount,
required this.activeCount,
required this.onsiteCount,
required this.remoteCount,
});
}

View File

@@ -0,0 +1,165 @@
import 'package:injectable/injectable.dart';
import 'package:superport/data/models/model_dto.dart';
import 'package:superport/data/repositories/model_repository.dart';
/// Model 비즈니스 로직을 처리하는 UseCase
@lazySingleton
class ModelUseCase {
final ModelRepository _repository;
ModelUseCase(this._repository);
/// 모든 모델 조회 (선택적으로 vendor로 필터링)
Future<List<ModelDto>> getModels({int? vendorId}) async {
try {
final models = await _repository.getModels(vendorId: vendorId);
// 활성 모델만 필터링 (비즈니스 규칙)
return models.where((model) => model.isActive).toList()
..sort((a, b) => a.name.compareTo(b.name));
} catch (e) {
throw Exception('모델 목록을 불러오는데 실패했습니다: $e');
}
}
/// 특정 모델 상세 조회
Future<ModelDto> getModelById(int id) async {
try {
return await _repository.getModelById(id);
} catch (e) {
throw Exception('모델 정보를 불러오는데 실패했습니다: $e');
}
}
/// 새 모델 생성
Future<ModelDto> createModel({
required int vendorsId,
required String name,
}) async {
try {
// 입력값 검증
if (name.trim().isEmpty) {
throw Exception('모델명을 입력해주세요');
}
// 중복 검사 (같은 vendor 내에서)
final existingModels = await _repository.getModels(vendorId: vendorsId);
final isDuplicate = existingModels.any(
(model) => model.name.toLowerCase() == name.toLowerCase(),
);
if (isDuplicate) {
throw Exception('동일한 제조사에 이미 존재하는 모델명입니다');
}
// 모델 생성
final request = ModelRequestDto(
vendorsId: vendorsId,
name: name.trim(),
);
return await _repository.createModel(request);
} catch (e) {
if (e.toString().contains('이미 존재하는')) {
rethrow;
}
throw Exception('모델 생성에 실패했습니다: $e');
}
}
/// 모델 정보 수정
Future<ModelDto> updateModel({
required int id,
required int vendorsId,
required String name,
}) async {
try {
// 입력값 검증
if (name.trim().isEmpty) {
throw Exception('모델명을 입력해주세요');
}
// 중복 검사 (자기 자신 제외)
final existingModels = await _repository.getModels(vendorId: vendorsId);
final isDuplicate = existingModels.any(
(model) => model.id != id &&
model.name.toLowerCase() == name.toLowerCase(),
);
if (isDuplicate) {
throw Exception('동일한 제조사에 이미 존재하는 모델명입니다');
}
// 모델 수정
final request = ModelUpdateRequestDto(
vendorsId: vendorsId,
name: name.trim(),
);
return await _repository.updateModel(id, request);
} catch (e) {
if (e.toString().contains('이미 존재하는')) {
rethrow;
}
throw Exception('모델 수정에 실패했습니다: $e');
}
}
/// 모델 삭제 (Hard Delete - 백엔드 API 사용)
Future<void> deleteModel(int id) async {
try {
await _repository.deleteModel(id);
} catch (e) {
throw Exception('모델 삭제에 실패했습니다: $e');
}
}
/// 모델 검색
Future<List<ModelDto>> searchModels(String query) async {
try {
if (query.trim().isEmpty) {
return await getModels();
}
final models = await _repository.searchModels(query);
// 활성 모델만 필터링
return models.where((model) => model.isActive).toList()
..sort((a, b) => a.name.compareTo(b.name));
} catch (e) {
throw Exception('모델 검색에 실패했습니다: $e');
}
}
/// Vendor별 모델 그룹핑
Future<Map<int, List<ModelDto>>> getModelsByVendorGrouped() async {
try {
final models = await getModels();
final grouped = <int, List<ModelDto>>{};
for (final model in models) {
grouped.putIfAbsent(model.vendorsId, () => []).add(model);
}
return grouped;
} catch (e) {
throw Exception('모델 그룹핑에 실패했습니다: $e');
}
}
/// Vendor 삭제 시 관련 모델 처리
Future<void> handleVendorDeletion(int vendorId) async {
try {
final models = await _repository.getModels(vendorId: vendorId);
// 모든 관련 모델 삭제
for (final model in models) {
if (model.id != null) {
await _repository.deleteModel(model.id!);
}
}
} catch (e) {
throw Exception('Vendor 삭제 처리에 실패했습니다: $e');
}
}
}

View File

@@ -0,0 +1,198 @@
import 'package:injectable/injectable.dart';
import '../../data/models/rent_dto.dart';
import '../repositories/rent_repository.dart';
/// 임대 UseCase (백엔드 스키마 기반)
/// 백엔드 API와 100% 호환되는 단순한 CRUD 작업만 제공
@lazySingleton
class RentUseCase {
final RentRepository _repository;
RentUseCase(this._repository);
/// 임대 목록 조회 (백엔드 스키마 기반)
Future<RentListResponse> getRents({
int page = 1,
int pageSize = 10,
String? search,
int? equipmentHistoryId,
}) async {
return await _repository.getRents(
page: page,
pageSize: pageSize,
search: search,
equipmentHistoryId: equipmentHistoryId,
);
}
/// 임대 상세 조회 (백엔드 스키마 기반)
Future<RentDto> getRent(int id) async {
if (id <= 0) {
throw ArgumentError('유효하지 않은 임대 ID입니다.');
}
return await _repository.getRent(id);
}
/// 임대 생성 (백엔드 스키마 기반)
Future<RentDto> createRent(RentRequestDto request) async {
// 백엔드 스키마 기반 검증만
_validateRentRequest(request);
return await _repository.createRent(request);
}
/// 임대 수정 (백엔드 스키마 기반)
Future<RentDto> updateRent(int id, RentUpdateRequestDto request) async {
if (id <= 0) {
throw ArgumentError('유효하지 않은 임대 ID입니다.');
}
_validateRentUpdateRequest(request);
return await _repository.updateRent(id, request);
}
/// 임대 삭제 (백엔드에서 처리)
Future<void> deleteRent(int id) async {
if (id <= 0) {
throw ArgumentError('유효하지 않은 임대 ID입니다.');
}
return await _repository.deleteRent(id);
}
/// 장비 이력별 임대 조회
Future<List<RentDto>> getRentsByEquipmentHistory(int equipmentHistoryId) async {
final response = await getRents(
pageSize: 1000,
equipmentHistoryId: equipmentHistoryId,
);
return response.items;
}
/// 특정 기간 내 임대 조회 (백엔드 날짜 필터링)
Future<List<RentDto>> getRentsByDateRange({
required DateTime startDate,
required DateTime endDate,
int? equipmentHistoryId,
}) async {
final response = await getRents(
pageSize: 1000,
equipmentHistoryId: equipmentHistoryId,
);
// 날짜 범위 필터링 (클라이언트 사이드)
return response.items.where((rent) {
return rent.startedAt.isAfter(startDate) &&
rent.endedAt.isBefore(endDate);
}).toList();
}
/// 현재 진행 중인 임대 조회
Future<List<RentDto>> getCurrentRents({int? equipmentHistoryId}) async {
final response = await getRents(
pageSize: 1000,
equipmentHistoryId: equipmentHistoryId,
);
final now = DateTime.now();
return response.items.where((rent) {
return rent.startedAt.isBefore(now) && rent.endedAt.isAfter(now);
}).toList();
}
/// 종료된 임대 조회
Future<List<RentDto>> getCompletedRents({int? equipmentHistoryId}) async {
final response = await getRents(
pageSize: 1000,
equipmentHistoryId: equipmentHistoryId,
);
final now = DateTime.now();
return response.items.where((rent) {
return rent.endedAt.isBefore(now);
}).toList();
}
/// 예정된 임대 조회
Future<List<RentDto>> getUpcomingRents({int? equipmentHistoryId}) async {
final response = await getRents(
pageSize: 1000,
equipmentHistoryId: equipmentHistoryId,
);
final now = DateTime.now();
return response.items.where((rent) {
return rent.startedAt.isAfter(now);
}).toList();
}
/// 임대 기간 계산 (일수)
int calculateRentDays(RentDto rent) {
return rent.endedAt.difference(rent.startedAt).inDays;
}
/// 남은 임대 기간 계산 (일수)
int calculateRemainingDays(RentDto rent) {
final now = DateTime.now();
if (rent.endedAt.isBefore(now)) {
return 0; // 이미 종료됨
}
return rent.endedAt.difference(now).inDays;
}
/// 임대 통계 조회 (단순화)
Future<RentStatistics> getRentStatistics() async {
final response = await getRents(pageSize: 1000);
final rents = response.items;
final now = DateTime.now();
int totalCount = rents.length;
int currentCount = rents.where((rent) =>
rent.startedAt.isBefore(now) && rent.endedAt.isAfter(now)
).length;
int completedCount = rents.where((rent) =>
rent.endedAt.isBefore(now)
).length;
int upcomingCount = rents.where((rent) =>
rent.startedAt.isAfter(now)
).length;
return RentStatistics(
totalCount: totalCount,
currentCount: currentCount,
completedCount: completedCount,
upcomingCount: upcomingCount,
);
}
/// Private 헬퍼 메서드
void _validateRentRequest(RentRequestDto request) {
// 날짜 검증 (백엔드 스키마 기반)
if (request.endedAt.isBefore(request.startedAt) ||
request.endedAt.isAtSameMomentAs(request.startedAt)) {
throw ArgumentError('종료일은 시작일보다 이후여야 합니다.');
}
}
void _validateRentUpdateRequest(RentUpdateRequestDto request) {
// 날짜 검증 (선택적 필드)
if (request.startedAt != null && request.endedAt != null) {
if (request.endedAt!.isBefore(request.startedAt!) ||
request.endedAt!.isAtSameMomentAs(request.startedAt!)) {
throw ArgumentError('종료일은 시작일보다 이후여야 합니다.');
}
}
}
}
/// 임대 통계 모델 (단순화)
class RentStatistics {
final int totalCount;
final int currentCount;
final int completedCount;
final int upcomingCount;
RentStatistics({
required this.totalCount,
required this.currentCount,
required this.completedCount,
required this.upcomingCount,
});
}

View File

@@ -5,22 +5,18 @@ import '../../../core/errors/failures.dart';
import '../../repositories/user_repository.dart';
import '../base_usecase.dart';
/// 사용자 생성 파라미터 (서버 API v0.2.1 대응)
/// 사용자 생성 파라미터 (백엔드 API v1 대응)
class CreateUserParams {
final String username;
final String email;
final String password;
final String name;
final UserRole role;
final String? email;
final String? phone;
final int companiesId;
const CreateUserParams({
required this.username,
required this.email,
required this.password,
required this.name,
required this.role,
this.email,
this.phone,
required this.companiesId,
});
}
@@ -41,45 +37,38 @@ class CreateUserUseCase extends UseCase<User, CreateUserParams> {
}
return await _userRepository.createUser(
username: params.username,
email: params.email,
password: params.password,
name: params.name,
email: params.email,
phone: params.phone,
role: params.role,
companiesId: params.companiesId,
);
}
/// 입력값 유효성 검증 (서버 API v0.2.1 규칙 적용)
/// 입력값 유효성 검증 (백엔드 API v1 규칙 적용)
ValidationFailure? _validateUserInput(CreateUserParams params) {
final errors = <String, String>{};
// 사용자명 검증 (3자 이상, 영문/숫자/언더스코어만)
if (params.username.length < 3) {
errors['username'] = '사용자명은 3자 이상이어야 합니다.';
}
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(params.username)) {
errors['username'] = '사용자명은 영문, 숫자, 언더스코어만 사용 가능합니다.';
}
// 이메일 검증 (기본 이메일 형식)
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(params.email)) {
errors['email'] = '올바른 이메일 형식이 아닙니다.';
}
// 비밀번호 검증 (서버 API: 6자 이상)
if (params.password.length < 6) {
errors['password'] = '비밀번호는 6자 이상이어야 합니다.';
}
// 이름 검증 (필수)
if (params.name.trim().isEmpty) {
errors['name'] = '이름을 입력해주세요.';
}
// 이메일 검증 (선택적, 입력시 형식 검증)
if (params.email != null && params.email!.isNotEmpty) {
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(params.email!)) {
errors['email'] = '올바른 이메일 형식이 아닙니다.';
}
}
// 회사 ID 검증 (필수)
if (params.companiesId <= 0) {
errors['companiesId'] = '회사를 선택해주세요.';
}
// 전화번호 검증 (선택적, "010-1234-5678" 형식)
if (params.phone != null && params.phone!.isNotEmpty) {
if (!PhoneNumberUtil.isValidFormat(params.phone!)) {
// PhoneNumberUtil import 필요시 추가
if (!RegExp(r'^010-\d{4}-\d{4}$').hasMatch(params.phone!)) {
errors['phone'] = '전화번호는 "010-1234-5678" 형식으로 입력해주세요.';
}
}

View File

@@ -1,4 +1,5 @@
/// User 도메인 UseCase 모음
library;
export 'get_users_usecase.dart';
export 'create_user_usecase.dart';
export 'update_user_usecase.dart';

View File

@@ -0,0 +1,155 @@
import 'package:injectable/injectable.dart';
import 'package:superport/data/models/vendor_dto.dart';
import 'package:superport/data/models/vendor_stats_dto.dart';
import 'package:superport/data/repositories/vendor_repository.dart';
import 'package:superport/utils/constants.dart';
abstract class VendorUseCase {
Future<VendorListResponse> getVendors({
int page = 1,
int limit = PaginationConstants.defaultPageSize,
String? search,
bool? isActive,
});
Future<VendorDto> getVendorById(int id);
Future<VendorDto> createVendor(VendorDto vendor);
Future<VendorDto> updateVendor(int id, VendorDto vendor);
Future<void> deleteVendor(int id);
Future<void> restoreVendor(int id);
Future<bool> validateVendor(VendorDto vendor);
Future<bool> checkDuplicateName(String name, {int? excludeId});
Future<VendorStatsDto> getVendorStats();
}
@Injectable(as: VendorUseCase)
class VendorUseCaseImpl implements VendorUseCase {
final VendorRepository _repository;
VendorUseCaseImpl(this._repository);
@override
Future<VendorListResponse> getVendors({
int page = 1,
int limit = PaginationConstants.defaultPageSize,
String? search,
bool? isActive,
}) async {
// 비즈니스 로직: 페이지네이션 유효성 검사
if (page < 1) page = 1;
if (limit < 1 || limit > 100) limit = 20;
return await _repository.getAll(
page: page,
limit: limit,
search: search,
isActive: isActive,
);
}
@override
Future<VendorDto> getVendorById(int id) async {
if (id <= 0) {
throw ArgumentError('유효하지 않은 벤더 ID입니다.');
}
return await _repository.getById(id);
}
@override
Future<VendorDto> createVendor(VendorDto vendor) async {
// 비즈니스 규칙 검증
await _validateVendorData(vendor);
// 중복 이름 검사
final isDuplicate = await checkDuplicateName(vendor.name);
if (isDuplicate) {
throw Exception('이미 존재하는 벤더명입니다.');
}
return await _repository.create(vendor);
}
@override
Future<VendorDto> updateVendor(int id, VendorDto vendor) async {
if (id <= 0) {
throw ArgumentError('유효하지 않은 벤더 ID입니다.');
}
// 비즈니스 규칙 검증
await _validateVendorData(vendor);
// 중복 이름 검사 (자기 자신 제외)
final isDuplicate = await checkDuplicateName(vendor.name, excludeId: id);
if (isDuplicate) {
throw Exception('이미 존재하는 벤더명입니다.');
}
return await _repository.update(id, vendor);
}
@override
Future<void> deleteVendor(int id) async {
if (id <= 0) {
throw ArgumentError('유효하지 않은 벤더 ID입니다.');
}
// 비즈니스 로직: 연관된 모델이나 장비가 있는지 확인
// TODO: 모델 및 장비 연관성 체크 구현
await _repository.delete(id);
}
@override
Future<void> restoreVendor(int id) async {
if (id <= 0) {
throw ArgumentError('유효하지 않은 벤더 ID입니다.');
}
await _repository.restore(id);
}
@override
Future<bool> validateVendor(VendorDto vendor) async {
try {
await _validateVendorData(vendor);
return true;
} catch (e) {
return false;
}
}
@override
Future<bool> checkDuplicateName(String name, {int? excludeId}) async {
if (name.trim().isEmpty) {
return false;
}
try {
final response = await _repository.getAll(search: name);
final duplicates = response.items.where((v) =>
v.name.toLowerCase() == name.toLowerCase() &&
(excludeId == null || v.id != excludeId)
);
return duplicates.isNotEmpty;
} catch (e) {
// 에러 발생 시 중복 없음으로 처리
return false;
}
}
Future<void> _validateVendorData(VendorDto vendor) async {
// 필수 필드 검증
if (vendor.name.trim().isEmpty) {
throw ArgumentError('벤더명은 필수 입력 항목입니다.');
}
if (vendor.name.length > 100) {
throw ArgumentError('벤더명은 100자를 초과할 수 없습니다.');
}
// 백엔드 스키마와 일치하는 필드만 검증 (name, is_deleted, registered_at, updated_at)
}
@override
Future<VendorStatsDto> getVendorStats() async {
return await _repository.getStats();
}
}

View File

@@ -1,8 +1,7 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/warehouse/warehouse_dto.dart';
import '../../../data/models/warehouse/warehouse_location_dto.dart';
import '../../../models/warehouse_location_model.dart';
import '../../../models/address_model.dart';
import '../../repositories/warehouse_location_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
@@ -22,33 +21,25 @@ class CreateWarehouseLocationUseCase implements UseCase<WarehouseLocationDto, Cr
return Left(ValidationFailure(message: '창고 위치 이름은 필수입니다'));
}
// 비즈니스 로직: 주소 유효성 검증
if (params.address.isEmpty) {
return Left(ValidationFailure(message: '창고 주소는 필수입니다'));
}
// 비즈니스 로직: 연락처 형식 검증
if (params.contactNumber != null && params.contactNumber!.isNotEmpty) {
final phoneRegex = RegExp(r'^[\d\-\+\(\)\s]+$');
if (!phoneRegex.hasMatch(params.contactNumber!)) {
return Left(ValidationFailure(message: '올바른 연락처 형식이 아닙니다'));
}
// 비즈니스 로직: 우편번호 유효성 검증
if (params.zipcode != null && params.zipcode!.isEmpty) {
return Left(ValidationFailure(message: '올바른 우편번호를 입력해주세요'));
}
final warehouseLocation = WarehouseLocation(
id: 0, // Default id for new warehouse location
name: params.name,
address: params.address,
address: params.zipcode ?? '', // Use zipcode as address
remark: params.description,
);
final result = await repository.createWarehouseLocation(warehouseLocation);
return result.map((createdLocation) => WarehouseLocationDto(
id: createdLocation.id ?? 0,
id: createdLocation.id,
name: createdLocation.name,
address: createdLocation.address,
isActive: true, // Default value since model doesn't have isActive
createdAt: DateTime.now(), // Add required createdAt parameter
zipcodesZipcode: params.zipcode,
remark: params.description,
registeredAt: DateTime.now(),
));
} catch (e) {
return Left(ServerFailure(message: e.toString()));
@@ -59,32 +50,20 @@ class CreateWarehouseLocationUseCase implements UseCase<WarehouseLocationDto, Cr
/// 창고 위치 생성 파라미터
class CreateWarehouseLocationParams {
final String name;
final String address;
final String? zipcode;
final String? description;
final String? contactNumber;
final String? manager;
final double? latitude;
final double? longitude;
CreateWarehouseLocationParams({
required this.name,
required this.address,
this.zipcode,
this.description,
this.contactNumber,
this.manager,
this.latitude,
this.longitude,
});
Map<String, dynamic> toMap() {
return {
'name': name,
'address': address,
'description': description,
'contact_number': contactNumber,
'manager': manager,
'latitude': latitude,
'longitude': longitude,
'Name': name,
'zipcodes_zipcode': zipcode,
'Remark': description,
};
}
}

View File

@@ -1,6 +1,6 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/warehouse/warehouse_dto.dart';
import '../../../data/models/warehouse/warehouse_location_dto.dart';
import '../../repositories/warehouse_location_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
@@ -17,11 +17,11 @@ class GetWarehouseLocationDetailUseCase implements UseCase<WarehouseLocationDto,
try {
final result = await repository.getWarehouseLocationById(id);
return result.map((location) => WarehouseLocationDto(
id: location.id ?? 0,
id: location.id,
name: location.name,
address: location.address.toString(),
isActive: true, // Default value since model doesn't have isActive
createdAt: DateTime.now(), // Add required createdAt parameter
zipcodesZipcode: location.address, // Map address to zipcode
remark: location.remark,
registeredAt: DateTime.now(),
));
} catch (e) {
return Left(ServerFailure(message: e.toString()));

View File

@@ -1,20 +1,20 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/common/pagination_params.dart';
import '../../../data/models/warehouse/warehouse_dto.dart';
import '../../../data/models/warehouse/warehouse_location_dto.dart';
import '../../repositories/warehouse_location_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 창고 위치 목록 조회 UseCase
@injectable
class GetWarehouseLocationsUseCase implements UseCase<WarehouseLocationListDto, GetWarehouseLocationsParams> {
class GetWarehouseLocationsUseCase implements UseCase<WarehouseLocationListResponse, GetWarehouseLocationsParams> {
final WarehouseLocationRepository repository;
GetWarehouseLocationsUseCase(this.repository);
@override
Future<Either<Failure, WarehouseLocationListDto>> call(GetWarehouseLocationsParams params) async {
Future<Either<Failure, WarehouseLocationListResponse>> call(GetWarehouseLocationsParams params) async {
try {
final result = await repository.getWarehouseLocations(
page: params.page,
@@ -26,18 +26,18 @@ class GetWarehouseLocationsUseCase implements UseCase<WarehouseLocationListDto,
sortBy: params.filters?['sortBy'],
sortOrder: params.filters?['sortOrder'],
);
return result.map((paginatedResponse) => WarehouseLocationListDto(
return result.map((paginatedResponse) => WarehouseLocationListResponse(
items: paginatedResponse.items.map((location) => WarehouseLocationDto(
id: location.id ?? 0,
id: location.id,
name: location.name,
address: location.address.toString(),
isActive: true, // Default value since model doesn't have isActive
createdAt: DateTime.now(), // Add required createdAt parameter
zipcodesZipcode: location.address, // Map address to zipcode
remark: location.remark,
registeredAt: DateTime.now(),
)).toList(),
page: paginatedResponse.page,
perPage: params.perPage, // Add missing required perPage parameter
total: paginatedResponse.totalElements,
currentPage: paginatedResponse.page,
totalCount: paginatedResponse.totalElements,
totalPages: paginatedResponse.totalPages,
pageSize: params.perPage,
));
} catch (e) {
return Left(ServerFailure(message: e.toString()));

View File

@@ -1,8 +1,7 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/warehouse/warehouse_dto.dart';
import '../../../data/models/warehouse/warehouse_location_dto.dart';
import '../../../models/warehouse_location_model.dart';
import '../../../models/address_model.dart';
import '../../repositories/warehouse_location_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
@@ -22,41 +21,25 @@ class UpdateWarehouseLocationUseCase implements UseCase<WarehouseLocationDto, Up
return Left(ValidationFailure(message: '창고 위치 이름은 비어있을 수 없습니다'));
}
// 비즈니스 로직: 주소 유효성 검증
if (params.address != null && params.address!.isEmpty) {
return Left(ValidationFailure(message: '창고 주소는 비어있을 수 없습니다'));
}
// 비즈니스 로직: 연락처 형식 검증
if (params.contactNumber != null && params.contactNumber!.isNotEmpty) {
final phoneRegex = RegExp(r'^[\d\-\+\(\)\s]+$');
if (!phoneRegex.hasMatch(params.contactNumber!)) {
return Left(ValidationFailure(message: '올바른 연락처 형식이 아닙니다'));
}
}
// 비즈니스 로직: 좌표 유효성 검증
if (params.latitude != null && (params.latitude! < -90 || params.latitude! > 90)) {
return Left(ValidationFailure(message: '유효하지 않은 위도값입니다'));
}
if (params.longitude != null && (params.longitude! < -180 || params.longitude! > 180)) {
return Left(ValidationFailure(message: '유효하지 않은 경도값입니다'));
// 비즈니스 로직: 우편번호 유효성 검증
if (params.zipcode != null && params.zipcode!.isEmpty) {
return Left(ValidationFailure(message: '올바른 우편번호를 입력해주세요'));
}
final warehouseLocation = WarehouseLocation(
id: params.id,
name: params.name ?? '',
address: params.address,
address: params.zipcode ?? '', // Use zipcode as address
remark: params.description,
);
final result = await repository.updateWarehouseLocation(params.id, warehouseLocation);
return result.map((updatedLocation) => WarehouseLocationDto(
id: updatedLocation.id ?? 0,
id: updatedLocation.id,
name: updatedLocation.name,
address: updatedLocation.address,
isActive: true, // Default value since model doesn't have isActive
createdAt: DateTime.now(), // Add required createdAt parameter
zipcodesZipcode: params.zipcode,
remark: params.description,
updatedAt: DateTime.now(),
));
} catch (e) {
return Left(ServerFailure(message: e.toString()));
@@ -68,36 +51,21 @@ class UpdateWarehouseLocationUseCase implements UseCase<WarehouseLocationDto, Up
class UpdateWarehouseLocationParams {
final int id;
final String? name;
final String? address;
final String? zipcode;
final String? description;
final String? contactNumber;
final String? manager;
final double? latitude;
final double? longitude;
final bool? isActive;
UpdateWarehouseLocationParams({
required this.id,
this.name,
this.address,
this.zipcode,
this.description,
this.contactNumber,
this.manager,
this.latitude,
this.longitude,
this.isActive,
});
Map<String, dynamic> toMap() {
final Map<String, dynamic> data = {};
if (name != null) data['name'] = name;
if (address != null) data['address'] = address;
if (description != null) data['description'] = description;
if (contactNumber != null) data['contact_number'] = contactNumber;
if (manager != null) data['manager'] = manager;
if (latitude != null) data['latitude'] = latitude;
if (longitude != null) data['longitude'] = longitude;
if (isActive != null) data['is_active'] = isActive;
if (name != null) data['Name'] = name;
if (zipcode != null) data['zipcodes_zipcode'] = zipcode;
if (description != null) data['Remark'] = description;
return data;
}
}

View File

@@ -0,0 +1,135 @@
import 'package:injectable/injectable.dart';
import 'package:superport/data/models/zipcode_dto.dart';
import 'package:superport/data/repositories/zipcode_repository.dart';
abstract class ZipcodeUseCase {
/// 우편번호 검색 (페이지네이션 지원)
Future<ZipcodeListResponse> searchZipcodes({
int page = 1,
int limit = 20,
String? search,
String? sido,
String? gu,
});
/// 우편번호로 정확한 주소 조회
Future<ZipcodeDto?> getZipcodeByNumber(int zipcode);
/// 시도별 구 목록 조회
Future<List<String>> getGuListBySido(String sido);
/// 전체 시도 목록 조회
Future<List<String>> getAllSidoList();
/// 주소 문자열로 우편번호 검색 (최적화된 검색)
Future<List<ZipcodeDto>> searchByAddress(String address);
/// 우편번호 유효성 검사
bool validateZipcode(int zipcode);
/// 검색어 유효성 검사 및 정규화
String normalizeSearchQuery(String query);
}
@Injectable(as: ZipcodeUseCase)
class ZipcodeUseCaseImpl implements ZipcodeUseCase {
final ZipcodeRepository _repository;
ZipcodeUseCaseImpl(this._repository);
@override
Future<ZipcodeListResponse> searchZipcodes({
int page = 1,
int limit = 20,
String? search,
String? sido,
String? gu,
}) async {
// 비즈니스 로직: 페이지네이션 유효성 검사
if (page < 1) page = 1;
if (limit < 1 || limit > 100) limit = 20;
// 검색어 정규화
final normalizedSearch = search != null && search.isNotEmpty
? normalizeSearchQuery(search)
: null;
return await _repository.search(
page: page,
limit: limit,
search: normalizedSearch,
sido: sido?.trim(),
gu: gu?.trim(),
);
}
@override
Future<ZipcodeDto?> getZipcodeByNumber(int zipcode) async {
// 우편번호 유효성 검사
if (!validateZipcode(zipcode)) {
throw ArgumentError('유효하지 않은 우편번호입니다. (5자리 숫자)');
}
return await _repository.getByZipcode(zipcode);
}
@override
Future<List<String>> getGuListBySido(String sido) async {
if (sido.trim().isEmpty) {
throw ArgumentError('시도명을 입력해주세요.');
}
final normalizedSido = sido.trim();
return await _repository.getGuBySido(normalizedSido);
}
@override
Future<List<String>> getAllSidoList() async {
return await _repository.getAllSido();
}
@override
Future<List<ZipcodeDto>> searchByAddress(String address) async {
if (address.trim().isEmpty) {
return [];
}
final normalizedAddress = normalizeSearchQuery(address);
try {
// 먼저 전체 검색으로 시도
final response = await _repository.search(
search: normalizedAddress,
limit: 10, // 상위 10개만 가져오기
);
return response.items;
} catch (e) {
// 검색 실패 시 빈 목록 반환
return [];
}
}
@override
bool validateZipcode(int zipcode) {
// 한국 우편번호는 5자리 숫자 (00000 ~ 99999)
return zipcode >= 0 && zipcode <= 99999;
}
@override
String normalizeSearchQuery(String query) {
if (query.trim().isEmpty) return '';
String normalized = query.trim();
// 공백 정규화 (여러 공백을 하나로)
normalized = normalized.replaceAll(RegExp(r'\s+'), ' ');
// 특수문자 제거 (단, 한글, 영문, 숫자, 공백, 하이픈만 유지)
normalized = normalized.replaceAll(RegExp(r'[^\w\s가-힣ㄱ-ㅎㅏ-ㅣ-]'), '');
return normalized;
}
}