diff --git a/asset/image/location.svg b/asset/image/location.svg new file mode 100644 index 0000000..3358301 --- /dev/null +++ b/asset/image/location.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lib/Model/model/equipment/equipment_list_model.dart b/lib/Model/model/equipment/equipment_list_model.dart new file mode 100644 index 0000000..40c9115 --- /dev/null +++ b/lib/Model/model/equipment/equipment_list_model.dart @@ -0,0 +1,72 @@ +import 'dart:ffi'; + +import 'package:frontend/Model/model/general_list_model.dart'; +import 'package:frontend/Model/model/general_model.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'equipment_list_model.g.dart'; + +@JsonSerializable() +class EquipmentResponseModel extends GeneralModel { + final EquipmentResponse data; + + EquipmentResponseModel({ + required super.status, + required super.code, + required super.message, + required this.data, + }); + + factory EquipmentResponseModel.fromJson(Map json) => + _$EquipmentResponseModelFromJson(json); +} + +@JsonSerializable() +class EquipmentResponse extends GeneralListModel { + List content; + + EquipmentResponse({ + required super.pageable, + required super.last, + required super.totalPages, + required super.totalElements, + required super.size, + required super.number, + required super.sort, + required super.first, + required super.numberOfElements, + required super.empty, + required this.content, + }); + + factory EquipmentResponse.fromJson(Map json) => + _$EquipmentResponseFromJson(json); +} + +@JsonSerializable() +class EquipmentModel { + String category; + String contact; + String? description; + int equipmentId; + String? imgUrl; + String keeper; + String? location; + String name; + int quantity; + + EquipmentModel({ + required this.category, + required this.contact, + required this.description, + required this.equipmentId, + required this.imgUrl, + required this.keeper, + required this.location, + required this.name, + required this.quantity, + }); + + factory EquipmentModel.fromJson(Map json) => + _$EquipmentModelFromJson(json); +} diff --git a/lib/Model/model/equipment/equipment_list_model.g.dart b/lib/Model/model/equipment/equipment_list_model.g.dart new file mode 100644 index 0000000..2f8dbd4 --- /dev/null +++ b/lib/Model/model/equipment/equipment_list_model.g.dart @@ -0,0 +1,83 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'equipment_list_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +EquipmentResponseModel _$EquipmentResponseModelFromJson( + Map json) => + EquipmentResponseModel( + status: json['status'] as int, + code: json['code'] as String, + message: json['message'] as String, + data: EquipmentResponse.fromJson(json['data'] as Map), + ); + +Map _$EquipmentResponseModelToJson( + EquipmentResponseModel instance) => + { + 'status': instance.status, + 'code': instance.code, + 'message': instance.message, + 'data': instance.data, + }; + +EquipmentResponse _$EquipmentResponseFromJson(Map json) => + EquipmentResponse( + pageable: Pageable.fromJson(json['pageable'] as Map), + last: json['last'] as bool, + totalPages: json['totalPages'] as int, + totalElements: json['totalElements'] as int, + size: json['size'] as int, + number: json['number'] as int, + sort: Sort.fromJson(json['sort'] as Map), + first: json['first'] as bool, + numberOfElements: json['numberOfElements'] as int, + empty: json['empty'] as bool, + content: (json['content'] as List) + .map((e) => EquipmentModel.fromJson(e as Map)) + .toList(), + ); + +Map _$EquipmentResponseToJson(EquipmentResponse instance) => + { + 'pageable': instance.pageable, + 'last': instance.last, + 'totalPages': instance.totalPages, + 'totalElements': instance.totalElements, + 'size': instance.size, + 'number': instance.number, + 'sort': instance.sort, + 'first': instance.first, + 'numberOfElements': instance.numberOfElements, + 'empty': instance.empty, + 'content': instance.content, + }; + +EquipmentModel _$EquipmentModelFromJson(Map json) => + EquipmentModel( + category: json['category'] as String, + contact: json['contact'] as String, + description: json['description'] as String?, + equipmentId: json['equipmentId'] as int, + imgUrl: json['imgUrl'] as String?, + keeper: json['keeper'] as String, + location: json['location'] as String?, + name: json['name'] as String, + quantity: json['quantity'] as int, + ); + +Map _$EquipmentModelToJson(EquipmentModel instance) => + { + 'category': instance.category, + 'contact': instance.contact, + 'description': instance.description, + 'equipmentId': instance.equipmentId, + 'imgUrl': instance.imgUrl, + 'keeper': instance.keeper, + 'location': instance.location, + 'name': instance.name, + 'quantity': instance.quantity, + }; diff --git a/lib/Presenter/equipment/equipment_service.dart b/lib/Presenter/equipment/equipment_service.dart new file mode 100644 index 0000000..4d87cd0 --- /dev/null +++ b/lib/Presenter/equipment/equipment_service.dart @@ -0,0 +1,17 @@ +import 'package:frontend/Model/network/api_manager.dart'; + +class EquipmentService { + final equipmentURL = '/equipments?size=100'; + + static final EquipmentService _equipmentService = EquipmentService._(); + EquipmentService._(); + factory EquipmentService() { + return _equipmentService; + } + + Future getEquipment() async { + final response = + APIManager().request(RequestType.get, equipmentURL, null, null, null); + return response; + } +} diff --git a/lib/View/common/screen/root_tab.dart b/lib/View/common/screen/root_tab.dart index c74721d..5beb5db 100644 --- a/lib/View/common/screen/root_tab.dart +++ b/lib/View/common/screen/root_tab.dart @@ -1,19 +1,16 @@ import 'package:flutter/material.dart'; import 'package:frontend/Model/network/api_manager.dart'; import 'package:frontend/View/colors.dart'; +import 'package:frontend/View/equipment/screen/equipment_screen.dart'; import '../../booking/screen/booking_screen.dart'; import '../component/tabbar_item.dart'; import '../../booking/screen/booking_history_screen.dart'; class RootTab extends StatefulWidget { - final int? initialIndex; - const RootTab({ - this.initialIndex, - Key? key - }) : super(key: key); + const RootTab({this.initialIndex, Key? key}) : super(key: key); @override State createState() => _RootTabState(); @@ -38,9 +35,10 @@ class _RootTabState extends State with SingleTickerProviderStateMixin { super.initState(); controller = TabController( - length: APIManager().isAdmin ? adminTabBarIcons.length : basicTabBarIcons.length, - vsync: this - ); + length: APIManager().isAdmin + ? adminTabBarIcons.length + : basicTabBarIcons.length, + vsync: this); controller.addListener(tabListener); index = widget.initialIndex ?? 0; selectedTab = widget.initialIndex ?? 0; @@ -71,15 +69,21 @@ class _RootTabState extends State with SingleTickerProviderStateMixin { if (APIManager().isAdmin) { return [ const BookingScreen(), - const BookingScreen(), - const BookingHistoryScreen(isAdmin: false,), - const BookingHistoryScreen(isAdmin: true,), + const EquipmentScreen(), + const BookingHistoryScreen( + isAdmin: false, + ), + const BookingHistoryScreen( + isAdmin: true, + ), ]; } else { return [ const BookingScreen(), - const BookingScreen(), - const BookingHistoryScreen(isAdmin: false,), + const EquipmentScreen(), + const BookingHistoryScreen( + isAdmin: false, + ), ]; } } @@ -87,10 +91,9 @@ class _RootTabState extends State with SingleTickerProviderStateMixin { /// 화면 중앙 UI 구현 메소드 /// Widget renderTabBarView() { return TabBarView( - physics: const NeverScrollableScrollPhysics(), - controller: controller, - children: getTabBarItemList() - ); + physics: const NeverScrollableScrollPhysics(), + controller: controller, + children: getTabBarItemList()); } /// 하단 탭바 구현 메소드 /// @@ -101,12 +104,10 @@ class _RootTabState extends State with SingleTickerProviderStateMixin { backgroundColor: Colors.white, selectedItemColor: purple, unselectedItemColor: Colors.black, - selectedLabelStyle: renderLabelStyle(), unselectedLabelStyle: renderLabelStyle(), type: BottomNavigationBarType.fixed, - - onTap: (index){ + onTap: (index) { controller.animateTo(index); setState(() { selectedTab = 0; @@ -119,9 +120,6 @@ class _RootTabState extends State with SingleTickerProviderStateMixin { } TextStyle renderLabelStyle() { - return const TextStyle( - fontSize: 10, - fontWeight: FontWeight.w500 - ); + return const TextStyle(fontSize: 10, fontWeight: FontWeight.w500); } } diff --git a/lib/View/equipment/component/equipment_cell.dart b/lib/View/equipment/component/equipment_cell.dart new file mode 100644 index 0000000..862f3e6 --- /dev/null +++ b/lib/View/equipment/component/equipment_cell.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:frontend/Model/model/equipment/equipment_list_model.dart'; +import 'package:frontend/Model/model/notification/notification_response.dart'; + +class EquipmentCell extends StatefulWidget { + final EquipmentModel equipment; + + const EquipmentCell({super.key, required this.equipment}); + + @override + State createState() => _EquipmentCellState(); +} + +class _EquipmentCellState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return equipmentCell(); + } + + Widget equipmentCell() { + return Container( + width: double.infinity, + height: 120, + decoration: ShapeDecoration( + color: const Color(0xFFF7F3FB), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + margin: const EdgeInsets.only(top: 10), + child: Row( + children: [ + Container( + margin: const EdgeInsets.only(top: 14, left: 14, bottom: 14), + child: (widget.equipment.imgUrl == null) + ? Image.asset('asset/image/pladi_icon.png') + : Image.network(widget.equipment.imgUrl!, + fit: BoxFit.fitWidth)), + Container( + margin: const EdgeInsets.only( + top: 17, left: 10, bottom: 14, right: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + alignment: Alignment.center, + width: 65, + height: 30, + padding: const EdgeInsets.symmetric( + horizontal: 4.72, vertical: 1.18), + decoration: ShapeDecoration( + color: const Color(0xFF640FAF), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + widget.equipment.category, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'NanumSquare_ac', + fontWeight: FontWeight.w400, + height: 0, + ), + ), + ), + const SizedBox( + width: 10, + ), + Text( + widget.equipment.name, + style: const TextStyle( + color: Color(0xFF640FAF), + fontSize: 15, + fontFamily: 'NanumSquare_ac', + fontWeight: FontWeight.w700, + height: 0.10, + letterSpacing: 0.10, + ), + ), + const SizedBox( + width: 10, + ), + Text( + widget.equipment.quantity.toString(), + style: const TextStyle( + color: Color(0xFF717171), + fontSize: 13, + fontFamily: 'NanumSquare_ac', + fontWeight: FontWeight.w700, + height: 0.14, + letterSpacing: 0.10, + ), + ) + ], + ), + const SizedBox( + height: 10, + ), + Row( + children: [ + SvgPicture.asset( + 'asset/image/mypage_icon.svg', + width: 22, + height: 22, + ), + Container( + width: 10, + ), + Text( + "${widget.equipment.keeper}(${widget.equipment.contact})", + style: const TextStyle( + color: Color(0xFF717171), + fontSize: 13, + fontFamily: 'NanumSquare_ac', + fontWeight: FontWeight.w700, + height: 0.14, + letterSpacing: 0.10, + ), + ) + ], + ), + const SizedBox( + height: 10, + ), + Row( + children: [ + SvgPicture.asset( + 'asset/image/location.svg', + width: 22, + height: 22, + ), + Container( + width: 10, + ), + Text( + widget.equipment.location == null + ? "보관 장소" + : widget.equipment.location!, + style: const TextStyle( + color: Color(0xFF717171), + fontSize: 13, + fontFamily: 'NanumSquare_ac', + fontWeight: FontWeight.w700, + height: 0.14, + letterSpacing: 0.10, + ), + ) + ], + ), + ], + ), + ) + ], + )); + } +} diff --git a/lib/View/equipment/screen/equipment_screen.dart b/lib/View/equipment/screen/equipment_screen.dart new file mode 100644 index 0000000..6208703 --- /dev/null +++ b/lib/View/equipment/screen/equipment_screen.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/Model/model/equipment/equipment_list_model.dart'; +import 'package:frontend/Presenter/equipment/equipment_service.dart'; +import 'package:frontend/View/common/component/main_app_bar.dart'; +import 'package:frontend/View/equipment/component/equipment_cell.dart'; + +class EquipmentScreen extends StatefulWidget { + const EquipmentScreen({super.key}); + + @override + State createState() => _EquipmentScreen(); +} + +class _EquipmentScreen extends State { + @override + Widget build(BuildContext context) { + return Scaffold(appBar: const MainAppBar(), body: futureBody()); + } + + Widget futureBody() { + List notificationList; + return FutureBuilder( + future: EquipmentService().getEquipment(), + builder: (context, snapshot) { + if (snapshot.hasError) { + return noDataBody(); + } + + if (snapshot.hasData) { + notificationList = + EquipmentResponseModel.fromJson(snapshot.data).data.content; + if (notificationList.isEmpty) { + return noDataBody(); + } else { + return renderBody(notificationList); + } + } else { + return const CircularProgressIndicator(); + } + }); + } + + Widget noDataBody() { + return Center( + child: Container( + width: double.infinity, + height: double.infinity, + alignment: Alignment.center, + child: const Text("비품 목록이 없습니다.", + style: TextStyle(fontSize: 16, color: Colors.purple)), + )); + } + + Widget renderBody(List equipmentList) { + return ListView.builder( + padding: const EdgeInsets.only(left: 20, right: 20, top: 7), + scrollDirection: Axis.vertical, + itemCount: equipmentList.length, + itemBuilder: (BuildContext context, int index) { + return EquipmentCell(equipment: equipmentList[index]); + }, + ); + } +}