주석화 진행상황 정리하고 핵심 모듈에 한글 주석 추가
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/customer.dart';
|
||||
|
||||
/// 고객(Customer) API 응답을 다루는 DTO.
|
||||
class CustomerDto {
|
||||
CustomerDto({
|
||||
this.id,
|
||||
@@ -36,6 +37,7 @@ class CustomerDto {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// 원본 JSON으로부터 DTO를 생성한다.
|
||||
factory CustomerDto.fromJson(Map<String, dynamic> json) {
|
||||
return CustomerDto(
|
||||
id: json['id'] as int?,
|
||||
@@ -57,6 +59,7 @@ class CustomerDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 JSON 맵으로 변환한다.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
@@ -76,6 +79,7 @@ class CustomerDto {
|
||||
};
|
||||
}
|
||||
|
||||
/// DTO를 도메인 [Customer] 엔티티로 변환한다.
|
||||
Customer toEntity() => Customer(
|
||||
id: id,
|
||||
customerCode: customerCode,
|
||||
@@ -93,6 +97,7 @@ class CustomerDto {
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
/// 페이징 응답을 파싱해 [PaginatedResult] 형식으로 반환한다.
|
||||
static PaginatedResult<Customer> parsePaginated(Map<String, dynamic>? json) {
|
||||
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
|
||||
final items = rawItems
|
||||
@@ -108,6 +113,7 @@ class CustomerDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// 고객 주소의 우편번호 정보를 담는 DTO.
|
||||
class CustomerZipcodeDto {
|
||||
CustomerZipcodeDto({
|
||||
required this.zipcode,
|
||||
@@ -121,6 +127,7 @@ class CustomerZipcodeDto {
|
||||
final String? sigungu;
|
||||
final String? roadName;
|
||||
|
||||
/// JSON에서 우편번호 정보를 파싱한다.
|
||||
factory CustomerZipcodeDto.fromJson(Map<String, dynamic> json) {
|
||||
return CustomerZipcodeDto(
|
||||
zipcode: json['zipcode'] as String,
|
||||
@@ -130,6 +137,7 @@ class CustomerZipcodeDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 JSON 맵으로 직렬화한다.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'zipcode': zipcode,
|
||||
@@ -139,6 +147,7 @@ class CustomerZipcodeDto {
|
||||
};
|
||||
}
|
||||
|
||||
/// DTO를 [CustomerZipcode] 엔티티로 변환한다.
|
||||
CustomerZipcode toEntity() => CustomerZipcode(
|
||||
zipcode: zipcode,
|
||||
sido: sido,
|
||||
@@ -147,6 +156,7 @@ class CustomerZipcodeDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// 문자열/DateTime 값을 파싱해 [DateTime]으로 변환한다.
|
||||
DateTime? _parseDate(Object? value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
@@ -154,6 +164,7 @@ DateTime? _parseDate(Object? value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 고객 입력 모델을 API 요청 바디로 변환한다.
|
||||
Map<String, dynamic> customerInputToJson(CustomerInput input) {
|
||||
final map = input.toPayload();
|
||||
map.removeWhere((key, value) => value == null);
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../domain/entities/customer.dart';
|
||||
import '../../domain/repositories/customer_repository.dart';
|
||||
import '../dtos/customer_dto.dart';
|
||||
|
||||
/// 고객(API) CRUD를 호출하는 원격 저장소 구현체.
|
||||
class CustomerRepositoryRemote implements CustomerRepository {
|
||||
CustomerRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
|
||||
|
||||
@@ -13,6 +14,7 @@ class CustomerRepositoryRemote implements CustomerRepository {
|
||||
|
||||
static const _basePath = '/customers';
|
||||
|
||||
/// 고객 목록을 조회한다.
|
||||
@override
|
||||
Future<PaginatedResult<Customer>> list({
|
||||
int page = 1,
|
||||
@@ -37,6 +39,7 @@ class CustomerRepositoryRemote implements CustomerRepository {
|
||||
return CustomerDto.parsePaginated(response.data ?? const {});
|
||||
}
|
||||
|
||||
/// 고객을 생성한다.
|
||||
@override
|
||||
Future<Customer> create(CustomerInput input) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
@@ -48,6 +51,7 @@ class CustomerRepositoryRemote implements CustomerRepository {
|
||||
return CustomerDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 고객 정보를 수정한다.
|
||||
@override
|
||||
Future<Customer> update(int id, CustomerInput input) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
@@ -59,11 +63,13 @@ class CustomerRepositoryRemote implements CustomerRepository {
|
||||
return CustomerDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 고객을 삭제한다.
|
||||
@override
|
||||
Future<void> delete(int id) async {
|
||||
await _api.delete<void>('$_basePath/$id');
|
||||
}
|
||||
|
||||
/// 삭제된 고객을 복구한다.
|
||||
@override
|
||||
Future<Customer> restore(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// 고객(Customer) 도메인 엔티티.
|
||||
class Customer {
|
||||
Customer({
|
||||
this.id,
|
||||
@@ -31,6 +32,7 @@ class Customer {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// 선택한 속성만 변경한 새 인스턴스를 반환한다.
|
||||
Customer copyWith({
|
||||
int? id,
|
||||
String? customerCode,
|
||||
@@ -66,6 +68,7 @@ class Customer {
|
||||
}
|
||||
}
|
||||
|
||||
/// 고객 주소의 우편번호/행정구역 정보를 표현한다.
|
||||
class CustomerZipcode {
|
||||
CustomerZipcode({
|
||||
required this.zipcode,
|
||||
@@ -80,6 +83,7 @@ class CustomerZipcode {
|
||||
final String? roadName;
|
||||
}
|
||||
|
||||
/// 고객 생성/수정 시 사용하는 입력 모델.
|
||||
class CustomerInput {
|
||||
CustomerInput({
|
||||
required this.customerCode,
|
||||
@@ -105,6 +109,7 @@ class CustomerInput {
|
||||
final bool isActive;
|
||||
final String? note;
|
||||
|
||||
/// API 요청 바디에 사용하기 위한 맵으로 직렬화한다.
|
||||
Map<String, dynamic> toPayload() {
|
||||
return {
|
||||
'customer_code': customerCode,
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../entities/customer.dart';
|
||||
|
||||
/// 고객 데이터를 다루는 도메인 저장소 인터페이스.
|
||||
abstract class CustomerRepository {
|
||||
/// 고객 목록을 조회한다.
|
||||
Future<PaginatedResult<Customer>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
@@ -12,11 +14,15 @@ abstract class CustomerRepository {
|
||||
bool? isActive,
|
||||
});
|
||||
|
||||
/// 고객을 생성한다.
|
||||
Future<Customer> create(CustomerInput input);
|
||||
|
||||
/// 고객을 수정한다.
|
||||
Future<Customer> update(int id, CustomerInput input);
|
||||
|
||||
/// 고객을 삭제한다.
|
||||
Future<void> delete(int id);
|
||||
|
||||
/// 삭제된 고객을 복구한다.
|
||||
Future<Customer> restore(int id);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import '../../domain/entities/customer.dart';
|
||||
import '../../domain/repositories/customer_repository.dart';
|
||||
|
||||
/// 고객 유형 필터 옵션.
|
||||
enum CustomerTypeFilter { all, partner, general }
|
||||
|
||||
/// 고객 활성 상태 필터 옵션.
|
||||
enum CustomerStatusFilter { all, activeOnly, inactiveOnly }
|
||||
|
||||
/// 고객 목록 조회/등록/수정을 담당하는 프레젠테이션 컨트롤러.
|
||||
class CustomerController extends ChangeNotifier {
|
||||
static const int defaultPageSize = 20;
|
||||
|
||||
@@ -34,6 +37,7 @@ class CustomerController extends ChangeNotifier {
|
||||
int get pageSize => _pageSize;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
/// 고객 목록을 조회한다. 필터/페이지 상태는 내부에서 유지된다.
|
||||
Future<void> fetch({int page = 1}) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
@@ -82,6 +86,7 @@ class CustomerController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색어를 변경한다.
|
||||
void updateQuery(String value) {
|
||||
if (_query == value) {
|
||||
return;
|
||||
@@ -90,6 +95,7 @@ class CustomerController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 고객 유형 필터를 변경한다.
|
||||
void updateTypeFilter(CustomerTypeFilter filter) {
|
||||
if (_typeFilter == filter) {
|
||||
return;
|
||||
@@ -98,6 +104,7 @@ class CustomerController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 고객 활성 상태 필터를 변경한다.
|
||||
void updateStatusFilter(CustomerStatusFilter filter) {
|
||||
if (_statusFilter == filter) {
|
||||
return;
|
||||
@@ -106,6 +113,7 @@ class CustomerController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 페이지 크기를 변경한다.
|
||||
void updatePageSize(int size) {
|
||||
if (size <= 0 || _pageSize == size) {
|
||||
return;
|
||||
@@ -114,6 +122,7 @@ class CustomerController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 신규 고객을 생성한다.
|
||||
Future<Customer?> create(CustomerInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -129,6 +138,7 @@ class CustomerController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 기존 고객을 수정한다.
|
||||
Future<Customer?> update(int id, CustomerInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -144,6 +154,7 @@ class CustomerController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 고객을 삭제한다.
|
||||
Future<bool> delete(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -159,6 +170,7 @@ class CustomerController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 삭제된 고객을 복구한다.
|
||||
Future<Customer?> restore(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -174,6 +186,7 @@ class CustomerController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 에러 메시지를 초기화한다.
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
@@ -16,6 +16,7 @@ import '../../domain/entities/customer.dart';
|
||||
import '../../domain/repositories/customer_repository.dart';
|
||||
import '../controllers/customer_controller.dart';
|
||||
|
||||
/// 고객 관리 화면. 기능 플래그에 따라 사양 페이지를 보여주거나 실제 목록을 노출한다.
|
||||
class CustomerPage extends StatelessWidget {
|
||||
const CustomerPage({super.key, required this.routeUri});
|
||||
|
||||
@@ -86,6 +87,7 @@ class CustomerPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 고객 관리 기능이 활성화된 경우 사용하는 실제 화면 위젯.
|
||||
class _CustomerEnabledPage extends StatefulWidget {
|
||||
const _CustomerEnabledPage({required this.routeUri});
|
||||
|
||||
@@ -95,6 +97,7 @@ class _CustomerEnabledPage extends StatefulWidget {
|
||||
State<_CustomerEnabledPage> createState() => _CustomerEnabledPageState();
|
||||
}
|
||||
|
||||
/// 고객 목록 UI와 라우트 파라미터 싱크를 담당하는 상태 클래스.
|
||||
class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
late final CustomerController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
@@ -403,6 +406,7 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
GoRouter.of(context).go(newLocation);
|
||||
}
|
||||
|
||||
/// URL 파라미터에서 고객 유형 필터 값을 파싱한다.
|
||||
CustomerTypeFilter _typeFromParam(String? value) {
|
||||
switch (value) {
|
||||
case 'partner':
|
||||
@@ -414,6 +418,7 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 고객 유형 필터를 URL 파라미터 문자열로 변환한다.
|
||||
String? _encodeType(CustomerTypeFilter filter) {
|
||||
switch (filter) {
|
||||
case CustomerTypeFilter.all:
|
||||
@@ -425,6 +430,7 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// URL 파라미터에서 고객 활성 상태를 파싱한다.
|
||||
CustomerStatusFilter _statusFromParam(String? value) {
|
||||
switch (value) {
|
||||
case 'active':
|
||||
@@ -436,6 +442,7 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 고객 상태 필터를 URL 파라미터 문자열로 변환한다.
|
||||
String? _encodeStatus(CustomerStatusFilter filter) {
|
||||
switch (filter) {
|
||||
case CustomerStatusFilter.all:
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/group.dart';
|
||||
|
||||
/// 권한 그룹(Group) API 응답을 표현하는 DTO.
|
||||
class GroupDto {
|
||||
GroupDto({
|
||||
this.id,
|
||||
@@ -26,6 +27,7 @@ class GroupDto {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// JSON에서 그룹 정보를 파싱한다.
|
||||
factory GroupDto.fromJson(Map<String, dynamic> json) {
|
||||
return GroupDto(
|
||||
id: json['id'] as int?,
|
||||
@@ -40,6 +42,7 @@ class GroupDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 도메인 [Group] 엔티티로 변환한다.
|
||||
Group toEntity() => Group(
|
||||
id: id,
|
||||
groupName: groupName,
|
||||
@@ -52,6 +55,7 @@ class GroupDto {
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
/// 페이징 응답을 [PaginatedResult]로 변환한다.
|
||||
static PaginatedResult<Group> parsePaginated(Map<String, dynamic>? json) {
|
||||
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
|
||||
final items = rawItems
|
||||
@@ -67,6 +71,7 @@ class GroupDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// 문자열/DateTime을 파싱해 [DateTime]으로 반환한다.
|
||||
DateTime? _parseDate(Object? value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../domain/entities/group.dart';
|
||||
import '../../domain/repositories/group_repository.dart';
|
||||
import '../dtos/group_dto.dart';
|
||||
|
||||
/// 권한 그룹 API를 호출하는 원격 저장소 구현체.
|
||||
class GroupRepositoryRemote implements GroupRepository {
|
||||
GroupRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
|
||||
|
||||
@@ -13,6 +14,7 @@ class GroupRepositoryRemote implements GroupRepository {
|
||||
|
||||
static const _basePath = '/groups';
|
||||
|
||||
/// 그룹 목록을 조회한다.
|
||||
@override
|
||||
Future<PaginatedResult<Group>> list({
|
||||
int page = 1,
|
||||
@@ -35,6 +37,7 @@ class GroupRepositoryRemote implements GroupRepository {
|
||||
return GroupDto.parsePaginated(response.data ?? const {});
|
||||
}
|
||||
|
||||
/// 새 그룹을 생성한다.
|
||||
@override
|
||||
Future<Group> create(GroupInput input) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
@@ -46,6 +49,7 @@ class GroupRepositoryRemote implements GroupRepository {
|
||||
return GroupDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 그룹 정보를 수정한다.
|
||||
@override
|
||||
Future<Group> update(int id, GroupInput input) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
@@ -57,11 +61,13 @@ class GroupRepositoryRemote implements GroupRepository {
|
||||
return GroupDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 그룹을 삭제한다.
|
||||
@override
|
||||
Future<void> delete(int id) async {
|
||||
await _api.delete<void>('$_basePath/$id');
|
||||
}
|
||||
|
||||
/// 삭제된 그룹을 복구한다.
|
||||
@override
|
||||
Future<Group> restore(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
|
||||
@@ -4,8 +4,10 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import '../../domain/entities/group.dart';
|
||||
import '../../domain/repositories/group_repository.dart';
|
||||
|
||||
/// 기본 그룹 여부 필터.
|
||||
enum GroupDefaultFilter { all, defaultOnly, nonDefault }
|
||||
|
||||
/// 그룹 사용 상태 필터.
|
||||
enum GroupStatusFilter { all, activeOnly, inactiveOnly }
|
||||
|
||||
/// 그룹 마스터 화면 상태 컨트롤러
|
||||
@@ -34,6 +36,7 @@ class GroupController extends ChangeNotifier {
|
||||
GroupStatusFilter get statusFilter => _statusFilter;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
/// 그룹 목록을 조회한다.
|
||||
Future<void> fetch({int page = 1}) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
@@ -65,21 +68,25 @@ class GroupController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색어를 변경한다.
|
||||
void updateQuery(String value) {
|
||||
_query = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 기본 그룹 여부 필터를 변경한다.
|
||||
void updateDefaultFilter(GroupDefaultFilter filter) {
|
||||
_defaultFilter = filter;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 사용 여부 필터를 변경한다.
|
||||
void updateStatusFilter(GroupStatusFilter filter) {
|
||||
_statusFilter = filter;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 새 그룹을 생성한다.
|
||||
Future<Group?> create(GroupInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -95,6 +102,7 @@ class GroupController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 그룹 정보를 수정한다.
|
||||
Future<Group?> update(int id, GroupInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -110,6 +118,7 @@ class GroupController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 그룹을 삭제한다.
|
||||
Future<bool> delete(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -125,6 +134,7 @@ class GroupController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 삭제된 그룹을 복구한다.
|
||||
Future<Group?> restore(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -140,11 +150,13 @@ class GroupController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 에러 메시지를 초기화한다.
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 제출 상태 플래그를 갱신하고 리스너에 알린다.
|
||||
void _setSubmitting(bool value) {
|
||||
_isSubmitting = value;
|
||||
notifyListeners();
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../../domain/entities/group.dart';
|
||||
import '../../domain/repositories/group_repository.dart';
|
||||
import '../controllers/group_controller.dart';
|
||||
|
||||
/// 권한 그룹 관리 페이지. 기능 플래그에 따라 사양 화면 또는 실제 목록을 보여준다.
|
||||
class GroupPage extends StatelessWidget {
|
||||
const GroupPage({super.key});
|
||||
|
||||
@@ -69,6 +70,7 @@ class GroupPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 그룹 기능이 활성화된 경우 사용하는 실제 화면 위젯.
|
||||
class _GroupEnabledPage extends StatefulWidget {
|
||||
const _GroupEnabledPage();
|
||||
|
||||
@@ -76,6 +78,7 @@ class _GroupEnabledPage extends StatefulWidget {
|
||||
State<_GroupEnabledPage> createState() => _GroupEnabledPageState();
|
||||
}
|
||||
|
||||
/// 그룹 목록과 필터/폼 상태를 관리하는 상태 클래스.
|
||||
class _GroupEnabledPageState extends State<_GroupEnabledPage> {
|
||||
late final GroupController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../../domain/entities/group_permission.dart';
|
||||
|
||||
/// 그룹별 메뉴 권한을 표현하는 DTO.
|
||||
class GroupPermissionDto {
|
||||
GroupPermissionDto({
|
||||
this.id,
|
||||
@@ -31,6 +32,7 @@ class GroupPermissionDto {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// JSON에서 권한 정보를 파싱한다.
|
||||
factory GroupPermissionDto.fromJson(Map<String, dynamic> json) {
|
||||
return GroupPermissionDto(
|
||||
id: json['id'] as int?,
|
||||
@@ -52,6 +54,7 @@ class GroupPermissionDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 도메인 [GroupPermission] 엔티티로 변환한다.
|
||||
GroupPermission toEntity() => GroupPermission(
|
||||
id: id,
|
||||
group: group.toEntity(),
|
||||
@@ -67,6 +70,7 @@ class GroupPermissionDto {
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
/// 페이징 응답을 [PaginatedResult]로 변환한다.
|
||||
static PaginatedResult<GroupPermission> parsePaginated(
|
||||
Map<String, dynamic>? json,
|
||||
) {
|
||||
@@ -84,12 +88,14 @@ class GroupPermissionDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// 권한 설정에 포함된 그룹 정보를 담는 DTO.
|
||||
class GroupPermissionGroupDto {
|
||||
GroupPermissionGroupDto({required this.id, required this.groupName});
|
||||
|
||||
final int id;
|
||||
final String groupName;
|
||||
|
||||
/// JSON에서 그룹 정보를 파싱한다.
|
||||
factory GroupPermissionGroupDto.fromJson(Map<String, dynamic> json) {
|
||||
return GroupPermissionGroupDto(
|
||||
id: json['id'] as int? ?? json['group_id'] as int,
|
||||
@@ -98,16 +104,19 @@ class GroupPermissionGroupDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 [GroupPermissionGroup] 엔티티로 변환한다.
|
||||
GroupPermissionGroup toEntity() =>
|
||||
GroupPermissionGroup(id: id, groupName: groupName);
|
||||
}
|
||||
|
||||
/// 권한 대상 메뉴 정보를 담는 DTO.
|
||||
class GroupPermissionMenuDto {
|
||||
GroupPermissionMenuDto({required this.id, required this.menuName});
|
||||
|
||||
final int id;
|
||||
final String menuName;
|
||||
|
||||
/// JSON에서 메뉴 정보를 파싱한다.
|
||||
factory GroupPermissionMenuDto.fromJson(Map<String, dynamic> json) {
|
||||
return GroupPermissionMenuDto(
|
||||
id: json['id'] as int? ?? json['menu_id'] as int,
|
||||
@@ -115,10 +124,12 @@ class GroupPermissionMenuDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 [GroupPermissionMenu] 엔티티로 변환한다.
|
||||
GroupPermissionMenu toEntity() =>
|
||||
GroupPermissionMenu(id: id, menuName: menuName);
|
||||
}
|
||||
|
||||
/// 문자열/DateTime 값을 파싱해 [DateTime]으로 변환한다.
|
||||
DateTime? _parseDate(Object? value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../domain/entities/group_permission.dart';
|
||||
import '../../domain/repositories/group_permission_repository.dart';
|
||||
import '../dtos/group_permission_dto.dart';
|
||||
|
||||
/// 그룹-메뉴 권한 API를 호출하는 원격 저장소.
|
||||
class GroupPermissionRepositoryRemote implements GroupPermissionRepository {
|
||||
GroupPermissionRepositoryRemote({required ApiClient apiClient})
|
||||
: _api = apiClient;
|
||||
@@ -14,6 +15,7 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository {
|
||||
|
||||
static const _basePath = '/group-menu-permissions';
|
||||
|
||||
/// 그룹 권한 목록을 조회한다.
|
||||
@override
|
||||
Future<PaginatedResult<GroupPermission>> list({
|
||||
int page = 1,
|
||||
@@ -39,6 +41,7 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository {
|
||||
return GroupPermissionDto.parsePaginated(response.data ?? const {});
|
||||
}
|
||||
|
||||
/// 그룹 권한을 생성한다.
|
||||
@override
|
||||
Future<GroupPermission> create(GroupPermissionInput input) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
@@ -50,6 +53,7 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository {
|
||||
return GroupPermissionDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 그룹 권한을 수정한다.
|
||||
@override
|
||||
Future<GroupPermission> update(int id, GroupPermissionInput input) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
@@ -61,11 +65,13 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository {
|
||||
return GroupPermissionDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 그룹 권한을 삭제한다.
|
||||
@override
|
||||
Future<void> delete(int id) async {
|
||||
await _api.delete<void>('$_basePath/$id');
|
||||
}
|
||||
|
||||
/// 삭제된 그룹 권한을 복구한다.
|
||||
@override
|
||||
Future<GroupPermission> restore(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../entities/group_permission.dart';
|
||||
|
||||
/// 그룹-메뉴 권한을 다루는 도메인 저장소 인터페이스.
|
||||
abstract class GroupPermissionRepository {
|
||||
/// 권한 목록을 조회한다.
|
||||
Future<PaginatedResult<GroupPermission>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
@@ -12,11 +14,15 @@ abstract class GroupPermissionRepository {
|
||||
bool includeDeleted = false,
|
||||
});
|
||||
|
||||
/// 그룹 권한을 생성한다.
|
||||
Future<GroupPermission> create(GroupPermissionInput input);
|
||||
|
||||
/// 그룹 권한을 수정한다.
|
||||
Future<GroupPermission> update(int id, GroupPermissionInput input);
|
||||
|
||||
/// 그룹 권한을 삭제한다.
|
||||
Future<void> delete(int id);
|
||||
|
||||
/// 삭제된 그룹 권한을 복구한다.
|
||||
Future<GroupPermission> restore(int id);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../../../menu/domain/repositories/menu_repository.dart';
|
||||
import '../../domain/entities/group_permission.dart';
|
||||
import '../../domain/repositories/group_permission_repository.dart';
|
||||
|
||||
/// 그룹 권한 활성 여부 필터.
|
||||
enum GroupPermissionStatusFilter { all, activeOnly, inactiveOnly }
|
||||
|
||||
/// 그룹-메뉴 권한 화면용 컨트롤러
|
||||
@@ -53,6 +54,7 @@ class GroupPermissionController extends ChangeNotifier {
|
||||
List<Group> get groups => List.unmodifiable(_groups);
|
||||
List<MenuItem> get menus => List.unmodifiable(_menus);
|
||||
|
||||
/// 그룹 목록을 로드해 권한 연결 시 선택할 수 있도록 준비한다.
|
||||
Future<void> loadGroups() async {
|
||||
_isLoadingGroups = true;
|
||||
notifyListeners();
|
||||
@@ -69,6 +71,7 @@ class GroupPermissionController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 메뉴 목록을 로드해 권한 연결 시 선택할 수 있도록 준비한다.
|
||||
Future<void> loadMenus() async {
|
||||
_isLoadingMenus = true;
|
||||
notifyListeners();
|
||||
@@ -89,6 +92,7 @@ class GroupPermissionController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 그룹 권한 목록을 조회한다.
|
||||
Future<void> fetch({int page = 1}) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
@@ -116,26 +120,31 @@ class GroupPermissionController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 그룹 필터를 변경한다.
|
||||
void updateGroupFilter(int? groupId) {
|
||||
_groupFilter = groupId;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 메뉴 필터를 변경한다.
|
||||
void updateMenuFilter(int? menuId) {
|
||||
_menuFilter = menuId;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 권한 활성 상태 필터를 변경한다.
|
||||
void updateStatusFilter(GroupPermissionStatusFilter filter) {
|
||||
_statusFilter = filter;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 삭제 포함 여부를 변경한다.
|
||||
void updateIncludeDeleted(bool value) {
|
||||
_includeDeleted = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 그룹 권한을 생성한다.
|
||||
Future<GroupPermission?> create(GroupPermissionInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -151,6 +160,7 @@ class GroupPermissionController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 그룹 권한을 수정한다.
|
||||
Future<GroupPermission?> update(int id, GroupPermissionInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -166,6 +176,7 @@ class GroupPermissionController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 그룹 권한을 삭제한다.
|
||||
Future<bool> delete(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -181,6 +192,7 @@ class GroupPermissionController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 삭제된 그룹 권한을 복구한다.
|
||||
Future<GroupPermission?> restore(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -196,11 +208,13 @@ class GroupPermissionController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 에러 메시지를 초기화한다.
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 제출 상태 플래그를 갱신하고 리스너에게 알린다.
|
||||
void _setSubmitting(bool value) {
|
||||
_isSubmitting = value;
|
||||
notifyListeners();
|
||||
|
||||
@@ -18,6 +18,7 @@ import '../../domain/entities/group_permission.dart';
|
||||
import '../../domain/repositories/group_permission_repository.dart';
|
||||
import '../controllers/group_permission_controller.dart';
|
||||
|
||||
/// 그룹-메뉴 권한 설정 페이지. 기능 플래그에 따라 사양/실제 화면을 전환한다.
|
||||
class GroupPermissionPage extends StatelessWidget {
|
||||
const GroupPermissionPage({super.key});
|
||||
|
||||
@@ -99,6 +100,7 @@ class GroupPermissionPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 그룹 권한 기능이 활성화된 경우 사용하는 실제 화면 위젯.
|
||||
class _GroupPermissionEnabledPage extends StatefulWidget {
|
||||
const _GroupPermissionEnabledPage();
|
||||
|
||||
@@ -107,6 +109,7 @@ class _GroupPermissionEnabledPage extends StatefulWidget {
|
||||
_GroupPermissionEnabledPageState();
|
||||
}
|
||||
|
||||
/// 그룹 권한 목록/필터/폼 상태를 관리하는 상태 클래스.
|
||||
class _GroupPermissionEnabledPageState
|
||||
extends State<_GroupPermissionEnabledPage> {
|
||||
late final GroupPermissionController _controller;
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/menu.dart';
|
||||
|
||||
/// 메뉴(Menu) API 응답을 표현하는 DTO.
|
||||
class MenuDto {
|
||||
MenuDto({
|
||||
this.id,
|
||||
@@ -30,6 +31,7 @@ class MenuDto {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// JSON에서 메뉴 정보를 파싱한다.
|
||||
factory MenuDto.fromJson(Map<String, dynamic> json) {
|
||||
return MenuDto(
|
||||
id: json['id'] as int?,
|
||||
@@ -50,6 +52,7 @@ class MenuDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 도메인 [MenuItem]으로 변환한다.
|
||||
MenuItem toEntity() => MenuItem(
|
||||
id: id,
|
||||
menuCode: menuCode,
|
||||
@@ -64,6 +67,7 @@ class MenuDto {
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
/// 페이징 응답을 [PaginatedResult]로 변환한다.
|
||||
static PaginatedResult<MenuItem> parsePaginated(Map<String, dynamic>? json) {
|
||||
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
|
||||
final items = rawItems
|
||||
@@ -79,12 +83,14 @@ class MenuDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// 하위 메뉴 요약 정보를 담는 DTO.
|
||||
class MenuSummaryDto {
|
||||
MenuSummaryDto({required this.id, required this.menuName});
|
||||
|
||||
final int id;
|
||||
final String menuName;
|
||||
|
||||
/// JSON에서 요약 정보를 파싱한다.
|
||||
factory MenuSummaryDto.fromJson(Map<String, dynamic> json) {
|
||||
return MenuSummaryDto(
|
||||
id: json['id'] as int,
|
||||
@@ -92,9 +98,11 @@ class MenuSummaryDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 [MenuSummary] 엔티티로 변환한다.
|
||||
MenuSummary toEntity() => MenuSummary(id: id, menuName: menuName);
|
||||
}
|
||||
|
||||
/// 문자열/DateTime을 파싱해 [DateTime]으로 변환한다.
|
||||
DateTime? _parseDate(Object? value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../domain/entities/menu.dart';
|
||||
import '../../domain/repositories/menu_repository.dart';
|
||||
import '../dtos/menu_dto.dart';
|
||||
|
||||
/// 메뉴 마스터 API를 호출하는 원격 저장소.
|
||||
class MenuRepositoryRemote implements MenuRepository {
|
||||
MenuRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
|
||||
|
||||
@@ -13,6 +14,7 @@ class MenuRepositoryRemote implements MenuRepository {
|
||||
|
||||
static const _basePath = '/menus';
|
||||
|
||||
/// 메뉴 목록을 조회한다.
|
||||
@override
|
||||
Future<PaginatedResult<MenuItem>> list({
|
||||
int page = 1,
|
||||
@@ -38,6 +40,7 @@ class MenuRepositoryRemote implements MenuRepository {
|
||||
return MenuDto.parsePaginated(response.data ?? const {});
|
||||
}
|
||||
|
||||
/// 새 메뉴를 생성한다.
|
||||
@override
|
||||
Future<MenuItem> create(MenuInput input) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
@@ -49,6 +52,7 @@ class MenuRepositoryRemote implements MenuRepository {
|
||||
return MenuDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 메뉴 정보를 수정한다.
|
||||
@override
|
||||
Future<MenuItem> update(int id, MenuInput input) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
@@ -60,11 +64,13 @@ class MenuRepositoryRemote implements MenuRepository {
|
||||
return MenuDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 메뉴를 삭제한다.
|
||||
@override
|
||||
Future<void> delete(int id) async {
|
||||
await _api.delete<void>('$_basePath/$id');
|
||||
}
|
||||
|
||||
/// 삭제된 메뉴를 복구한다.
|
||||
@override
|
||||
Future<MenuItem> restore(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import '../../domain/entities/menu.dart';
|
||||
import '../../domain/repositories/menu_repository.dart';
|
||||
|
||||
/// 메뉴 사용 여부 필터.
|
||||
enum MenuStatusFilter { all, activeOnly, inactiveOnly }
|
||||
|
||||
/// 메뉴 마스터 상태 컨트롤러
|
||||
@@ -38,6 +39,7 @@ class MenuController extends ChangeNotifier {
|
||||
String? get errorMessage => _errorMessage;
|
||||
List<MenuItem> get parents => _parents;
|
||||
|
||||
/// 상위 메뉴 목록을 로드해 드롭다운에 표시한다.
|
||||
Future<void> loadParents() async {
|
||||
_isLoadingParents = true;
|
||||
notifyListeners();
|
||||
@@ -56,6 +58,7 @@ class MenuController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 메뉴 목록을 조회한다.
|
||||
Future<void> fetch({int page = 1}) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
@@ -83,26 +86,31 @@ class MenuController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색어를 변경한다.
|
||||
void updateQuery(String value) {
|
||||
_query = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 상위 메뉴 필터를 변경한다.
|
||||
void updateParentFilter(int? parentId) {
|
||||
_parentFilter = parentId;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 메뉴 사용 여부 필터를 변경한다.
|
||||
void updateStatusFilter(MenuStatusFilter filter) {
|
||||
_statusFilter = filter;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 삭제 포함 여부를 변경한다.
|
||||
void updateIncludeDeleted(bool value) {
|
||||
_includeDeleted = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 메뉴를 생성한다.
|
||||
Future<MenuItem?> create(MenuInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -119,6 +127,7 @@ class MenuController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 메뉴 정보를 수정한다.
|
||||
Future<MenuItem?> update(int id, MenuInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -135,6 +144,7 @@ class MenuController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 메뉴를 삭제한다.
|
||||
Future<bool> delete(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -151,6 +161,7 @@ class MenuController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 삭제된 메뉴를 복구한다.
|
||||
Future<MenuItem?> restore(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -167,11 +178,13 @@ class MenuController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 에러 메시지를 초기화한다.
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 제출 상태 플래그를 갱신하고 리스너에 알린다.
|
||||
void _setSubmitting(bool value) {
|
||||
_isSubmitting = value;
|
||||
notifyListeners();
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../../domain/entities/menu.dart';
|
||||
import '../../domain/repositories/menu_repository.dart';
|
||||
import '../controllers/menu_controller.dart' as menu;
|
||||
|
||||
/// 메뉴 관리 페이지. 기능 플래그에 따라 사양/실제 화면을 전환한다.
|
||||
class MenuPage extends StatelessWidget {
|
||||
const MenuPage({super.key});
|
||||
|
||||
@@ -89,6 +90,7 @@ class MenuPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 메뉴 기능이 활성화된 경우 사용하는 실제 화면 위젯.
|
||||
class _MenuEnabledPage extends StatefulWidget {
|
||||
const _MenuEnabledPage();
|
||||
|
||||
@@ -96,6 +98,7 @@ class _MenuEnabledPage extends StatefulWidget {
|
||||
State<_MenuEnabledPage> createState() => _MenuEnabledPageState();
|
||||
}
|
||||
|
||||
/// 메뉴 목록과 필터를 관리하는 상태 클래스.
|
||||
class _MenuEnabledPageState extends State<_MenuEnabledPage> {
|
||||
late final menu.MenuController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/product.dart';
|
||||
|
||||
/// 제품(Product) API 응답을 표현하는 DTO.
|
||||
class ProductDto {
|
||||
ProductDto({
|
||||
this.id,
|
||||
@@ -28,6 +29,7 @@ class ProductDto {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// JSON에서 제품 정보를 파싱한다.
|
||||
factory ProductDto.fromJson(Map<String, dynamic> json) {
|
||||
return ProductDto(
|
||||
id: json['id'] as int?,
|
||||
@@ -47,6 +49,7 @@ class ProductDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 JSON 맵으로 직렬화한다.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
@@ -62,6 +65,7 @@ class ProductDto {
|
||||
};
|
||||
}
|
||||
|
||||
/// DTO를 도메인 [Product] 엔티티로 변환한다.
|
||||
Product toEntity() => Product(
|
||||
id: id,
|
||||
productCode: productCode,
|
||||
@@ -75,6 +79,7 @@ class ProductDto {
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
/// 엔티티 값을 DTO로 역변환한다.
|
||||
static ProductDto fromEntity(Product entity) => ProductDto(
|
||||
id: entity.id,
|
||||
productCode: entity.productCode,
|
||||
@@ -96,6 +101,7 @@ class ProductDto {
|
||||
updatedAt: entity.updatedAt,
|
||||
);
|
||||
|
||||
/// 페이징 응답을 [PaginatedResult]로 변환한다.
|
||||
static PaginatedResult<Product> parsePaginated(Map<String, dynamic>? json) {
|
||||
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
|
||||
final items = rawItems
|
||||
@@ -111,6 +117,7 @@ class ProductDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// 제품에 연결된 공급업체 정보를 담는 DTO.
|
||||
class ProductVendorDto {
|
||||
ProductVendorDto({
|
||||
required this.id,
|
||||
@@ -122,6 +129,7 @@ class ProductVendorDto {
|
||||
final String vendorCode;
|
||||
final String vendorName;
|
||||
|
||||
/// JSON에서 공급업체 정보를 파싱한다.
|
||||
factory ProductVendorDto.fromJson(Map<String, dynamic> json) {
|
||||
return ProductVendorDto(
|
||||
id: json['id'] as int,
|
||||
@@ -130,20 +138,24 @@ class ProductVendorDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 JSON 맵으로 직렬화한다.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'id': id, 'vendor_code': vendorCode, 'vendor_name': vendorName};
|
||||
}
|
||||
|
||||
/// DTO를 [ProductVendor] 엔티티로 변환한다.
|
||||
ProductVendor toEntity() =>
|
||||
ProductVendor(id: id, vendorCode: vendorCode, vendorName: vendorName);
|
||||
}
|
||||
|
||||
/// 제품의 단위(UOM) 정보를 담는 DTO.
|
||||
class ProductUomDto {
|
||||
ProductUomDto({required this.id, required this.uomName});
|
||||
|
||||
final int id;
|
||||
final String uomName;
|
||||
|
||||
/// JSON에서 단위 정보를 파싱한다.
|
||||
factory ProductUomDto.fromJson(Map<String, dynamic> json) {
|
||||
return ProductUomDto(
|
||||
id: json['id'] as int,
|
||||
@@ -151,13 +163,16 @@ class ProductUomDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 JSON 맵으로 직렬화한다.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'id': id, 'uom_name': uomName};
|
||||
}
|
||||
|
||||
/// DTO를 [ProductUom] 엔티티로 변환한다.
|
||||
ProductUom toEntity() => ProductUom(id: id, uomName: uomName);
|
||||
}
|
||||
|
||||
/// 문자열/DateTime을 파싱해 [DateTime]으로 변환한다.
|
||||
DateTime? _parseDate(Object? value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
@@ -165,6 +180,7 @@ DateTime? _parseDate(Object? value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 제품 입력 모델을 API 요청 바디로 변환한다.
|
||||
Map<String, dynamic> productInputToJson(ProductInput input) {
|
||||
final map = input.toPayload();
|
||||
map.removeWhere((key, value) => value == null);
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../domain/entities/product.dart';
|
||||
import '../../domain/repositories/product_repository.dart';
|
||||
import '../dtos/product_dto.dart';
|
||||
|
||||
/// 제품 마스터 API를 호출하는 원격 저장소.
|
||||
class ProductRepositoryRemote implements ProductRepository {
|
||||
ProductRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
|
||||
|
||||
@@ -13,6 +14,7 @@ class ProductRepositoryRemote implements ProductRepository {
|
||||
|
||||
static const _basePath = '/products';
|
||||
|
||||
/// 제품 목록을 조회한다.
|
||||
@override
|
||||
Future<PaginatedResult<Product>> list({
|
||||
int page = 1,
|
||||
@@ -38,6 +40,7 @@ class ProductRepositoryRemote implements ProductRepository {
|
||||
return ProductDto.parsePaginated(response.data ?? const {});
|
||||
}
|
||||
|
||||
/// 제품을 생성한다.
|
||||
@override
|
||||
Future<Product> create(ProductInput input) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
@@ -49,6 +52,7 @@ class ProductRepositoryRemote implements ProductRepository {
|
||||
return ProductDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 제품 정보를 수정한다.
|
||||
@override
|
||||
Future<Product> update(int id, ProductInput input) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
@@ -60,11 +64,13 @@ class ProductRepositoryRemote implements ProductRepository {
|
||||
return ProductDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 제품을 삭제한다.
|
||||
@override
|
||||
Future<void> delete(int id) async {
|
||||
await _api.delete<void>('$_basePath/$id');
|
||||
}
|
||||
|
||||
/// 삭제된 제품을 복구한다.
|
||||
@override
|
||||
Future<Product> restore(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// 제품(Product) 도메인 엔티티.
|
||||
class Product {
|
||||
Product({
|
||||
this.id,
|
||||
@@ -23,6 +24,7 @@ class Product {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// 일부 속성을 변경한 새 인스턴스를 반환한다.
|
||||
Product copyWith({
|
||||
int? id,
|
||||
String? productCode,
|
||||
@@ -50,6 +52,7 @@ class Product {
|
||||
}
|
||||
}
|
||||
|
||||
/// 제품에 연결된 공급업체 정보.
|
||||
class ProductVendor {
|
||||
ProductVendor({
|
||||
required this.id,
|
||||
@@ -62,6 +65,7 @@ class ProductVendor {
|
||||
final String vendorName;
|
||||
}
|
||||
|
||||
/// 제품의 단위(UOM) 정보.
|
||||
class ProductUom {
|
||||
ProductUom({required this.id, required this.uomName});
|
||||
|
||||
@@ -69,6 +73,7 @@ class ProductUom {
|
||||
final String uomName;
|
||||
}
|
||||
|
||||
/// 제품 생성/수정에 사용하는 입력 모델.
|
||||
class ProductInput {
|
||||
ProductInput({
|
||||
required this.productCode,
|
||||
@@ -86,6 +91,7 @@ class ProductInput {
|
||||
final bool isActive;
|
||||
final String? note;
|
||||
|
||||
/// API 요청 바디로 직렬화한다.
|
||||
Map<String, dynamic> toPayload() {
|
||||
return {
|
||||
'product_code': productCode,
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../entities/product.dart';
|
||||
|
||||
/// 제품 데이터를 다루는 도메인 저장소 인터페이스.
|
||||
abstract class ProductRepository {
|
||||
/// 제품 목록을 조회한다.
|
||||
Future<PaginatedResult<Product>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
@@ -12,11 +14,15 @@ abstract class ProductRepository {
|
||||
bool? isActive,
|
||||
});
|
||||
|
||||
/// 제품을 생성한다.
|
||||
Future<Product> create(ProductInput input);
|
||||
|
||||
/// 제품을 수정한다.
|
||||
Future<Product> update(int id, ProductInput input);
|
||||
|
||||
/// 제품을 삭제한다.
|
||||
Future<void> delete(int id);
|
||||
|
||||
/// 삭제된 제품을 복구한다.
|
||||
Future<Product> restore(int id);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import '../../../uom/domain/repositories/uom_repository.dart';
|
||||
import '../../domain/entities/product.dart';
|
||||
import '../../domain/repositories/product_repository.dart';
|
||||
|
||||
/// 제품 사용 여부 필터.
|
||||
enum ProductStatusFilter { all, activeOnly, inactiveOnly }
|
||||
|
||||
/// 제품 마스터 화면 상태를 관리하는 컨트롤러.
|
||||
class ProductController extends ChangeNotifier {
|
||||
static const int defaultPageSize = 20;
|
||||
|
||||
@@ -52,6 +54,7 @@ class ProductController extends ChangeNotifier {
|
||||
List<Vendor> get vendorOptions => _vendorOptions;
|
||||
List<Uom> get uomOptions => _uomOptions;
|
||||
|
||||
/// 제품 목록을 조회한다.
|
||||
Future<void> fetch({int page = 1}) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
@@ -82,6 +85,7 @@ class ProductController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 필터/폼에서 사용할 공급업체와 단위 목록을 로드한다.
|
||||
Future<void> loadLookups() async {
|
||||
_isLoadingLookups = true;
|
||||
notifyListeners();
|
||||
@@ -98,6 +102,7 @@ class ProductController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색어를 변경한다.
|
||||
void updateQuery(String value) {
|
||||
if (_query == value) {
|
||||
return;
|
||||
@@ -106,6 +111,7 @@ class ProductController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 공급업체 필터를 변경한다.
|
||||
void updateVendorFilter(int? vendorId) {
|
||||
if (_vendorFilter == vendorId) {
|
||||
return;
|
||||
@@ -114,6 +120,7 @@ class ProductController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 단위(UOM) 필터를 변경한다.
|
||||
void updateUomFilter(int? uomId) {
|
||||
if (_uomFilter == uomId) {
|
||||
return;
|
||||
@@ -122,6 +129,7 @@ class ProductController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 사용 여부 필터를 변경한다.
|
||||
void updateStatusFilter(ProductStatusFilter filter) {
|
||||
if (_statusFilter == filter) {
|
||||
return;
|
||||
@@ -130,6 +138,7 @@ class ProductController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 페이지 크기를 변경한다.
|
||||
void updatePageSize(int size) {
|
||||
if (size <= 0 || _pageSize == size) {
|
||||
return;
|
||||
@@ -138,6 +147,7 @@ class ProductController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 제품을 생성한다.
|
||||
Future<Product?> create(ProductInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -153,6 +163,7 @@ class ProductController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 제품 정보를 수정한다.
|
||||
Future<Product?> update(int id, ProductInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -168,6 +179,7 @@ class ProductController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 제품을 삭제한다.
|
||||
Future<bool> delete(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -183,6 +195,7 @@ class ProductController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 삭제된 제품을 복구한다.
|
||||
Future<Product?> restore(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -198,11 +211,13 @@ class ProductController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 에러 메시지를 초기화한다.
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 제출 상태 플래그를 갱신하고 리스너에 알린다.
|
||||
void _setSubmitting(bool value) {
|
||||
_isSubmitting = value;
|
||||
notifyListeners();
|
||||
|
||||
@@ -19,6 +19,7 @@ import '../../domain/entities/product.dart';
|
||||
import '../../domain/repositories/product_repository.dart';
|
||||
import '../controllers/product_controller.dart';
|
||||
|
||||
/// 제품 관리 페이지. 기능 플래그에 따라 사양/실제 화면을 전환한다.
|
||||
class ProductPage extends StatelessWidget {
|
||||
const ProductPage({super.key, required this.routeUri});
|
||||
|
||||
@@ -74,6 +75,7 @@ class ProductPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 제품 기능이 활성화된 경우 사용하는 실제 화면 위젯.
|
||||
class _ProductEnabledPage extends StatefulWidget {
|
||||
const _ProductEnabledPage({required this.routeUri});
|
||||
|
||||
@@ -83,6 +85,7 @@ class _ProductEnabledPage extends StatefulWidget {
|
||||
State<_ProductEnabledPage> createState() => _ProductEnabledPageState();
|
||||
}
|
||||
|
||||
/// 제품 목록과 필터/폼 상태를 관리하는 상태 클래스.
|
||||
class _ProductEnabledPageState extends State<_ProductEnabledPage> {
|
||||
late final ProductController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/uom.dart';
|
||||
|
||||
/// 단위(UOM) API 응답을 표현하는 DTO.
|
||||
class UomDto {
|
||||
UomDto({
|
||||
this.id,
|
||||
@@ -24,6 +25,7 @@ class UomDto {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// JSON에서 단위 정보를 파싱한다.
|
||||
factory UomDto.fromJson(Map<String, dynamic> json) {
|
||||
return UomDto(
|
||||
id: json['id'] as int?,
|
||||
@@ -37,6 +39,7 @@ class UomDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 도메인 [Uom] 엔티티로 변환한다.
|
||||
Uom toEntity() => Uom(
|
||||
id: id,
|
||||
uomName: uomName,
|
||||
@@ -48,6 +51,7 @@ class UomDto {
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
/// 페이징 응답을 [PaginatedResult]로 변환한다.
|
||||
static PaginatedResult<Uom> parsePaginated(Map<String, dynamic>? json) {
|
||||
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
|
||||
final items = rawItems
|
||||
@@ -63,6 +67,7 @@ class UomDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// 문자열/DateTime을 파싱해 [DateTime]으로 변환한다.
|
||||
DateTime? _parseDate(Object? value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../domain/entities/uom.dart';
|
||||
import '../../domain/repositories/uom_repository.dart';
|
||||
import '../dtos/uom_dto.dart';
|
||||
|
||||
/// 단위(UOM) 마스터 API를 호출하는 원격 저장소.
|
||||
class UomRepositoryRemote implements UomRepository {
|
||||
UomRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
|
||||
|
||||
@@ -13,6 +14,7 @@ class UomRepositoryRemote implements UomRepository {
|
||||
|
||||
static const _basePath = '/uoms';
|
||||
|
||||
/// UOM 목록을 조회한다.
|
||||
@override
|
||||
Future<PaginatedResult<Uom>> list({
|
||||
int page = 1,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// 단위(UOM) 도메인 엔티티.
|
||||
class Uom {
|
||||
Uom({
|
||||
this.id,
|
||||
@@ -19,6 +20,7 @@ class Uom {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// 선택한 속성만 변경한 새 인스턴스를 반환한다.
|
||||
Uom copyWith({
|
||||
int? id,
|
||||
String? uomName,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/user.dart';
|
||||
|
||||
/// 사용자(User) API 응답을 표현하는 DTO.
|
||||
class UserDto {
|
||||
UserDto({
|
||||
this.id,
|
||||
@@ -30,6 +31,7 @@ class UserDto {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// JSON에서 사용자 정보를 파싱한다.
|
||||
factory UserDto.fromJson(Map<String, dynamic> json) {
|
||||
return UserDto(
|
||||
id: json['id'] as int?,
|
||||
@@ -48,6 +50,7 @@ class UserDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 도메인 [UserAccount] 엔티티로 변환한다.
|
||||
UserAccount toEntity() => UserAccount(
|
||||
id: id,
|
||||
employeeNo: employeeNo,
|
||||
@@ -62,6 +65,7 @@ class UserDto {
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
/// 페이징 응답을 [PaginatedResult]로 변환한다.
|
||||
static PaginatedResult<UserAccount> parsePaginated(
|
||||
Map<String, dynamic>? json,
|
||||
) {
|
||||
@@ -79,12 +83,14 @@ class UserDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자에 연결된 그룹 정보를 담는 DTO.
|
||||
class UserGroupDto {
|
||||
UserGroupDto({required this.id, required this.groupName});
|
||||
|
||||
final int id;
|
||||
final String groupName;
|
||||
|
||||
/// JSON에서 그룹 정보를 파싱한다.
|
||||
factory UserGroupDto.fromJson(Map<String, dynamic> json) {
|
||||
return UserGroupDto(
|
||||
id: json['id'] as int,
|
||||
@@ -92,9 +98,11 @@ class UserGroupDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 [UserGroup] 엔티티로 변환한다.
|
||||
UserGroup toEntity() => UserGroup(id: id, groupName: groupName);
|
||||
}
|
||||
|
||||
/// 문자열/DateTime을 파싱해 [DateTime]으로 변환한다.
|
||||
DateTime? _parseDate(Object? value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../domain/entities/user.dart';
|
||||
import '../../domain/repositories/user_repository.dart';
|
||||
import '../dtos/user_dto.dart';
|
||||
|
||||
/// 사용자 마스터 API를 호출하는 원격 저장소.
|
||||
class UserRepositoryRemote implements UserRepository {
|
||||
UserRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
|
||||
|
||||
@@ -13,6 +14,7 @@ class UserRepositoryRemote implements UserRepository {
|
||||
|
||||
static const _basePath = '/employees';
|
||||
|
||||
/// 사용자 목록을 조회한다.
|
||||
@override
|
||||
Future<PaginatedResult<UserAccount>> list({
|
||||
int page = 1,
|
||||
@@ -36,6 +38,7 @@ class UserRepositoryRemote implements UserRepository {
|
||||
return UserDto.parsePaginated(response.data ?? const {});
|
||||
}
|
||||
|
||||
/// 사용자를 생성한다.
|
||||
@override
|
||||
Future<UserAccount> create(UserInput input) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
@@ -47,6 +50,7 @@ class UserRepositoryRemote implements UserRepository {
|
||||
return UserDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 사용자 정보를 수정한다.
|
||||
@override
|
||||
Future<UserAccount> update(int id, UserInput input) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
@@ -58,11 +62,13 @@ class UserRepositoryRemote implements UserRepository {
|
||||
return UserDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 사용자를 삭제한다.
|
||||
@override
|
||||
Future<void> delete(int id) async {
|
||||
await _api.delete<void>('$_basePath/$id');
|
||||
}
|
||||
|
||||
/// 삭제된 사용자를 복구한다.
|
||||
@override
|
||||
Future<UserAccount> restore(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// 사용자(User) 도메인 엔티티.
|
||||
class UserAccount {
|
||||
UserAccount({
|
||||
this.id,
|
||||
@@ -25,6 +26,7 @@ class UserAccount {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// 선택된 속성만 변경한 새 인스턴스를 반환한다.
|
||||
UserAccount copyWith({
|
||||
int? id,
|
||||
String? employeeNo,
|
||||
@@ -54,6 +56,7 @@ class UserAccount {
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자에 연결된 그룹 정보.
|
||||
class UserGroup {
|
||||
UserGroup({required this.id, required this.groupName});
|
||||
|
||||
@@ -61,6 +64,7 @@ class UserGroup {
|
||||
final String groupName;
|
||||
}
|
||||
|
||||
/// 사용자 생성/수정 입력 모델.
|
||||
class UserInput {
|
||||
UserInput({
|
||||
required this.employeeNo,
|
||||
@@ -80,6 +84,7 @@ class UserInput {
|
||||
final bool isActive;
|
||||
final String? note;
|
||||
|
||||
/// API 요청 바디로 직렬화한다.
|
||||
Map<String, dynamic> toPayload() {
|
||||
return {
|
||||
'employee_no': employeeNo,
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../entities/user.dart';
|
||||
|
||||
/// 사용자 데이터를 다루는 도메인 저장소 인터페이스.
|
||||
abstract class UserRepository {
|
||||
/// 사용자 목록을 조회한다.
|
||||
Future<PaginatedResult<UserAccount>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
@@ -11,11 +13,15 @@ abstract class UserRepository {
|
||||
bool? isActive,
|
||||
});
|
||||
|
||||
/// 사용자를 생성한다.
|
||||
Future<UserAccount> create(UserInput input);
|
||||
|
||||
/// 사용자 정보를 수정한다.
|
||||
Future<UserAccount> update(int id, UserInput input);
|
||||
|
||||
/// 사용자를 삭제한다.
|
||||
Future<void> delete(int id);
|
||||
|
||||
/// 삭제된 사용자를 복구한다.
|
||||
Future<UserAccount> restore(int id);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import '../../../group/domain/repositories/group_repository.dart';
|
||||
import '../../domain/entities/user.dart';
|
||||
import '../../domain/repositories/user_repository.dart';
|
||||
|
||||
/// 사용자 활성 여부 필터.
|
||||
enum UserStatusFilter { all, activeOnly, inactiveOnly }
|
||||
|
||||
/// 사용자 마스터 화면 상태를 관리하는 컨트롤러.
|
||||
class UserController extends ChangeNotifier {
|
||||
UserController({
|
||||
required UserRepository userRepository,
|
||||
@@ -38,6 +40,7 @@ class UserController extends ChangeNotifier {
|
||||
String? get errorMessage => _errorMessage;
|
||||
List<Group> get groups => _groups;
|
||||
|
||||
/// 권한 그룹 목록을 로드한다.
|
||||
Future<void> loadGroups() async {
|
||||
_isLoadingGroups = true;
|
||||
notifyListeners();
|
||||
@@ -52,6 +55,7 @@ class UserController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자 목록을 조회한다.
|
||||
Future<void> fetch({int page = 1}) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
@@ -78,21 +82,25 @@ class UserController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색어를 변경한다.
|
||||
void updateQuery(String value) {
|
||||
_query = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 그룹 필터를 변경한다.
|
||||
void updateGroupFilter(int? groupId) {
|
||||
_groupFilter = groupId;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 사용자 상태 필터를 변경한다.
|
||||
void updateStatusFilter(UserStatusFilter filter) {
|
||||
_statusFilter = filter;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 사용자를 생성한다.
|
||||
Future<UserAccount?> create(UserInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -108,6 +116,7 @@ class UserController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자 정보를 수정한다.
|
||||
Future<UserAccount?> update(int id, UserInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -123,6 +132,7 @@ class UserController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자를 삭제한다.
|
||||
Future<bool> delete(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -138,6 +148,7 @@ class UserController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 삭제된 사용자를 복구한다.
|
||||
Future<UserAccount?> restore(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -153,11 +164,13 @@ class UserController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 에러 메시지를 초기화한다.
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 제출 상태 플래그를 갱신하고 리스너에게 알린다.
|
||||
void _setSubmitting(bool value) {
|
||||
_isSubmitting = value;
|
||||
notifyListeners();
|
||||
|
||||
@@ -15,6 +15,7 @@ import '../../domain/entities/user.dart';
|
||||
import '../../domain/repositories/user_repository.dart';
|
||||
import '../controllers/user_controller.dart';
|
||||
|
||||
/// 사용자 관리 페이지. 기능 플래그에 따라 사양/실제 화면을 보여준다.
|
||||
class UserPage extends StatelessWidget {
|
||||
const UserPage({super.key});
|
||||
|
||||
@@ -80,6 +81,7 @@ class UserPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자 기능이 활성화된 경우 사용하는 실제 화면 위젯.
|
||||
class _UserEnabledPage extends StatefulWidget {
|
||||
const _UserEnabledPage();
|
||||
|
||||
@@ -87,6 +89,7 @@ class _UserEnabledPage extends StatefulWidget {
|
||||
State<_UserEnabledPage> createState() => _UserEnabledPageState();
|
||||
}
|
||||
|
||||
/// 사용자 목록과 필터 상태를 관리하는 상태 클래스.
|
||||
class _UserEnabledPageState extends State<_UserEnabledPage> {
|
||||
late final UserController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@@ -15,6 +15,7 @@ import '../../../vendor/domain/entities/vendor.dart';
|
||||
import '../../../vendor/domain/repositories/vendor_repository.dart';
|
||||
import '../controllers/vendor_controller.dart';
|
||||
|
||||
/// 벤더 관리 페이지. 기능 플래그에 따라 사양/실제 화면을 전환한다.
|
||||
class VendorPage extends StatelessWidget {
|
||||
const VendorPage({super.key, required this.routeUri});
|
||||
|
||||
@@ -67,6 +68,7 @@ class VendorPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 벤더 기능이 활성화된 경우 사용하는 실제 화면 위젯.
|
||||
class _VendorEnabledPage extends StatefulWidget {
|
||||
const _VendorEnabledPage({required this.routeUri});
|
||||
|
||||
@@ -76,6 +78,7 @@ class _VendorEnabledPage extends StatefulWidget {
|
||||
State<_VendorEnabledPage> createState() => _VendorEnabledPageState();
|
||||
}
|
||||
|
||||
/// 벤더 목록과 필터/폼 상태를 관리하는 상태 클래스.
|
||||
class _VendorEnabledPageState extends State<_VendorEnabledPage> {
|
||||
late final VendorController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/warehouse.dart';
|
||||
|
||||
/// 창고(Warehouse) API 응답을 표현하는 DTO.
|
||||
class WarehouseDto {
|
||||
WarehouseDto({
|
||||
this.id,
|
||||
@@ -28,6 +29,7 @@ class WarehouseDto {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// JSON에서 창고 정보를 파싱한다.
|
||||
factory WarehouseDto.fromJson(Map<String, dynamic> json) {
|
||||
return WarehouseDto(
|
||||
id: json['id'] as int?,
|
||||
@@ -47,6 +49,7 @@ class WarehouseDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 JSON 맵으로 직렬화한다.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
@@ -62,6 +65,7 @@ class WarehouseDto {
|
||||
};
|
||||
}
|
||||
|
||||
/// DTO를 도메인 [Warehouse] 엔티티로 변환한다.
|
||||
Warehouse toEntity() => Warehouse(
|
||||
id: id,
|
||||
warehouseCode: warehouseCode,
|
||||
@@ -75,6 +79,7 @@ class WarehouseDto {
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
/// 페이징 응답을 [PaginatedResult]로 변환한다.
|
||||
static PaginatedResult<Warehouse> parsePaginated(Map<String, dynamic>? json) {
|
||||
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
|
||||
final items = rawItems
|
||||
@@ -90,6 +95,7 @@ class WarehouseDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 주소에 대한 우편번호 정보를 담는 DTO.
|
||||
class WarehouseZipcodeDto {
|
||||
WarehouseZipcodeDto({
|
||||
required this.zipcode,
|
||||
@@ -103,6 +109,7 @@ class WarehouseZipcodeDto {
|
||||
final String? sigungu;
|
||||
final String? roadName;
|
||||
|
||||
/// JSON에서 우편번호 정보를 파싱한다.
|
||||
factory WarehouseZipcodeDto.fromJson(Map<String, dynamic> json) {
|
||||
return WarehouseZipcodeDto(
|
||||
zipcode: json['zipcode'] as String,
|
||||
@@ -112,6 +119,7 @@ class WarehouseZipcodeDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// DTO를 JSON 맵으로 직렬화한다.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'zipcode': zipcode,
|
||||
@@ -121,6 +129,7 @@ class WarehouseZipcodeDto {
|
||||
};
|
||||
}
|
||||
|
||||
/// DTO를 [WarehouseZipcode] 엔티티로 변환한다.
|
||||
WarehouseZipcode toEntity() => WarehouseZipcode(
|
||||
zipcode: zipcode,
|
||||
sido: sido,
|
||||
@@ -129,6 +138,7 @@ class WarehouseZipcodeDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// 문자열/DateTime 값을 파싱해 [DateTime]으로 변환한다.
|
||||
DateTime? _parseDate(Object? value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
@@ -136,6 +146,7 @@ DateTime? _parseDate(Object? value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 창고 입력 모델을 API 요청 바디로 변환한다.
|
||||
Map<String, dynamic> warehouseInputToJson(WarehouseInput input) {
|
||||
final map = input.toPayload();
|
||||
map.removeWhere((key, value) => value == null);
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../domain/entities/warehouse.dart';
|
||||
import '../../domain/repositories/warehouse_repository.dart';
|
||||
import '../dtos/warehouse_dto.dart';
|
||||
|
||||
/// 창고(Warehouse) 마스터 API를 호출하는 원격 저장소.
|
||||
class WarehouseRepositoryRemote implements WarehouseRepository {
|
||||
WarehouseRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
|
||||
|
||||
@@ -13,6 +14,7 @@ class WarehouseRepositoryRemote implements WarehouseRepository {
|
||||
|
||||
static const _basePath = '/warehouses';
|
||||
|
||||
/// 창고 목록을 조회한다.
|
||||
@override
|
||||
Future<PaginatedResult<Warehouse>> list({
|
||||
int page = 1,
|
||||
@@ -33,6 +35,7 @@ class WarehouseRepositoryRemote implements WarehouseRepository {
|
||||
return WarehouseDto.parsePaginated(response.data ?? const {});
|
||||
}
|
||||
|
||||
/// 창고를 생성한다.
|
||||
@override
|
||||
Future<Warehouse> create(WarehouseInput input) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
@@ -44,6 +47,7 @@ class WarehouseRepositoryRemote implements WarehouseRepository {
|
||||
return WarehouseDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 창고 정보를 수정한다.
|
||||
@override
|
||||
Future<Warehouse> update(int id, WarehouseInput input) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
@@ -55,11 +59,13 @@ class WarehouseRepositoryRemote implements WarehouseRepository {
|
||||
return WarehouseDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// 창고를 삭제한다.
|
||||
@override
|
||||
Future<void> delete(int id) async {
|
||||
await _api.delete<void>('$_basePath/$id');
|
||||
}
|
||||
|
||||
/// 삭제된 창고를 복구한다.
|
||||
@override
|
||||
Future<Warehouse> restore(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// 창고(Warehouse) 도메인 엔티티.
|
||||
class Warehouse {
|
||||
Warehouse({
|
||||
this.id,
|
||||
@@ -23,6 +24,7 @@ class Warehouse {
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// 일부 속성만 변경한 새 인스턴스를 반환한다.
|
||||
Warehouse copyWith({
|
||||
int? id,
|
||||
String? warehouseCode,
|
||||
@@ -50,6 +52,7 @@ class Warehouse {
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 주소에 대한 우편번호/행정 정보.
|
||||
class WarehouseZipcode {
|
||||
WarehouseZipcode({
|
||||
required this.zipcode,
|
||||
@@ -64,6 +67,7 @@ class WarehouseZipcode {
|
||||
final String? roadName;
|
||||
}
|
||||
|
||||
/// 창고 생성/수정에 사용하는 입력 모델.
|
||||
class WarehouseInput {
|
||||
WarehouseInput({
|
||||
required this.warehouseCode,
|
||||
@@ -81,6 +85,7 @@ class WarehouseInput {
|
||||
final bool isActive;
|
||||
final String? note;
|
||||
|
||||
/// API 요청 바디로 직렬화한다.
|
||||
Map<String, dynamic> toPayload() {
|
||||
return {
|
||||
'warehouse_code': warehouseCode,
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../entities/warehouse.dart';
|
||||
|
||||
/// 창고 데이터를 다루는 도메인 저장소 인터페이스.
|
||||
abstract class WarehouseRepository {
|
||||
/// 창고 목록을 조회한다.
|
||||
Future<PaginatedResult<Warehouse>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
@@ -10,11 +12,15 @@ abstract class WarehouseRepository {
|
||||
bool? isActive,
|
||||
});
|
||||
|
||||
/// 창고를 생성한다.
|
||||
Future<Warehouse> create(WarehouseInput input);
|
||||
|
||||
/// 창고 정보를 수정한다.
|
||||
Future<Warehouse> update(int id, WarehouseInput input);
|
||||
|
||||
/// 창고를 삭제한다.
|
||||
Future<void> delete(int id);
|
||||
|
||||
/// 삭제된 창고를 복구한다.
|
||||
Future<Warehouse> restore(int id);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import '../../domain/entities/warehouse.dart';
|
||||
import '../../domain/repositories/warehouse_repository.dart';
|
||||
|
||||
/// 창고 사용 여부 필터.
|
||||
enum WarehouseStatusFilter { all, activeOnly, inactiveOnly }
|
||||
|
||||
/// 창고 마스터 화면 상태를 관리하는 컨트롤러.
|
||||
class WarehouseController extends ChangeNotifier {
|
||||
static const int defaultPageSize = 20;
|
||||
|
||||
@@ -30,6 +32,7 @@ class WarehouseController extends ChangeNotifier {
|
||||
int get pageSize => _pageSize;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
/// 창고 목록을 조회한다.
|
||||
Future<void> fetch({int page = 1}) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
@@ -58,6 +61,7 @@ class WarehouseController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색어를 변경한다.
|
||||
void updateQuery(String value) {
|
||||
if (_query == value) {
|
||||
return;
|
||||
@@ -66,6 +70,7 @@ class WarehouseController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 사용 여부 필터를 변경한다.
|
||||
void updateStatusFilter(WarehouseStatusFilter filter) {
|
||||
if (_statusFilter == filter) {
|
||||
return;
|
||||
@@ -74,6 +79,7 @@ class WarehouseController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 페이지 크기를 변경한다.
|
||||
void updatePageSize(int size) {
|
||||
if (size <= 0 || _pageSize == size) {
|
||||
return;
|
||||
@@ -82,6 +88,7 @@ class WarehouseController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 창고를 생성한다.
|
||||
Future<Warehouse?> create(WarehouseInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -97,6 +104,7 @@ class WarehouseController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 정보를 수정한다.
|
||||
Future<Warehouse?> update(int id, WarehouseInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -112,6 +120,7 @@ class WarehouseController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고를 삭제한다.
|
||||
Future<bool> delete(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -127,6 +136,7 @@ class WarehouseController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 삭제된 창고를 복구한다.
|
||||
Future<Warehouse?> restore(int id) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
@@ -142,11 +152,13 @@ class WarehouseController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 에러 메시지를 초기화한다.
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 제출 상태 플래그를 갱신하고 리스너에 알린다.
|
||||
void _setSubmitting(bool value) {
|
||||
_isSubmitting = value;
|
||||
notifyListeners();
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../../domain/entities/warehouse.dart';
|
||||
import '../../domain/repositories/warehouse_repository.dart';
|
||||
import '../controllers/warehouse_controller.dart';
|
||||
|
||||
/// 창고 관리 페이지. 기능 플래그에 따라 사양/실제 화면을 전환한다.
|
||||
class WarehousePage extends StatelessWidget {
|
||||
const WarehousePage({super.key, required this.routeUri});
|
||||
|
||||
@@ -81,6 +82,7 @@ class WarehousePage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 기능이 활성화된 경우 사용하는 실제 화면 위젯.
|
||||
class _WarehouseEnabledPage extends StatefulWidget {
|
||||
const _WarehouseEnabledPage({required this.routeUri});
|
||||
|
||||
@@ -90,6 +92,7 @@ class _WarehouseEnabledPage extends StatefulWidget {
|
||||
State<_WarehouseEnabledPage> createState() => _WarehouseEnabledPageState();
|
||||
}
|
||||
|
||||
/// 창고 목록과 필터 상태를 관리하는 상태 클래스.
|
||||
class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
|
||||
late final WarehouseController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
Reference in New Issue
Block a user