From 148172d706c2c36b1383ed9f6d742ff72bfd02ce Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Tue, 8 Oct 2024 10:47:10 +0900 Subject: [PATCH 1/2] feat : enable search by integrating api --- lib/screens/products/item_list_page.dart | 43 +++--- lib/screens/search/search_result_screen.dart | 138 +++++++++++++++++++ lib/screens/search/search_screen.dart | 114 ++++++++------- lib/widgets/textfield.dart | 35 +++++ 4 files changed, 262 insertions(+), 68 deletions(-) create mode 100644 lib/screens/search/search_result_screen.dart create mode 100644 lib/widgets/textfield.dart diff --git a/lib/screens/products/item_list_page.dart b/lib/screens/products/item_list_page.dart index 687f491..8b728b5 100644 --- a/lib/screens/products/item_list_page.dart +++ b/lib/screens/products/item_list_page.dart @@ -151,27 +151,28 @@ class _ItemListPageState extends State { runSpacing: 15, children: [ FutureBuilder( - future: _products, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Text('Error: ${snapshot.error.toString()}'); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const Center(child: Text('상품이 없습니다')); - } else { - final products = snapshot.data!; - return Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.spaceBetween, - spacing: 15, - runSpacing: 15, - children: products - .map((product) => ProductCard(product: product)) - .toList(), - ); - } - }) + future: _products, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error.toString()}'); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('상품이 없습니다')); + } else { + final products = snapshot.data!; + return Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceBetween, + spacing: 15, + runSpacing: 15, + children: products + .map((product) => ProductCard(product: product)) + .toList(), + ); + } + }, + ) ], ), ), diff --git a/lib/screens/search/search_result_screen.dart b/lib/screens/search/search_result_screen.dart new file mode 100644 index 0000000..3eab05d --- /dev/null +++ b/lib/screens/search/search_result_screen.dart @@ -0,0 +1,138 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:saphy/utils/colors.dart'; +import 'package:saphy/utils/textstyles.dart'; +import 'package:saphy/widgets/product_card.dart'; +import 'package:saphy/models/product.dart'; +import 'package:saphy/widgets/textfield.dart'; + +class SearchResultScreen extends StatefulWidget { + final String query; + + const SearchResultScreen({super.key, required this.query}); + + @override + State createState() => _SearchResultScreenState(); +} + +class _SearchResultScreenState extends State { + late TextEditingController _controller; + Future>? _products; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + _products = searchText(widget.query); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var screenWidth = MediaQuery.of(context).size.width; + return Scaffold( + backgroundColor: const Color(0xfff4f4f4), + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: altWhite, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon( + Icons.arrow_back_ios, + size: 25, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + textField( + _controller, + context, + (String value) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SearchResultScreen(query: value), + ), + ); + }, + ), + IconButton( + icon: const Icon( + Icons.search, + size: 25, + ), + onPressed: () { + setState(() { + _products = searchText(_controller.text); + }); + }, + ), + ], + ), + ), + body: CustomScrollView(slivers: [ + SliverPadding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 30), + sliver: SliverToBoxAdapter( + child: FutureBuilder>( + future: _products, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center( + child: Text('Error: ${snapshot.error.toString()}')); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('상품이 없습니다')); + } else { + final products = snapshot.data!; + return Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceBetween, + spacing: 15, + runSpacing: 15, + children: products + .map((product) => ProductCard(product: product)) + .toList(), + ); + } + }, + ), + ), + ), + ]), + ); + } + + Future> searchText(String value) async { + final dio = Dio(); + try { + final response = await dio.get( + 'https://saphy.site/api/items?deviceType=PHONE&size=20&query=$value'); + + if (response.statusCode == 200) { + final data = response.data as Map; + if (data['results'] != null) { + List products = (data['results'] as List) + .map((item) => Product.fromJson(item)) + .toList(); + return products; + } else { + throw Exception('검색 결과가 없습니다.'); + } + } else { + throw Exception('상품 로드에 실패했습니다.'); + } + } catch (e) { + throw Exception('에러가 발생했습니다.'); + } + } +} diff --git a/lib/screens/search/search_screen.dart b/lib/screens/search/search_screen.dart index 3620cd8..9be0310 100644 --- a/lib/screens/search/search_screen.dart +++ b/lib/screens/search/search_screen.dart @@ -1,74 +1,94 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:saphy/utils/colors.dart'; import 'package:saphy/utils/textstyles.dart'; +import 'package:saphy/screens/search/search_result_screen.dart'; +import 'package:saphy/widgets/textfield.dart'; -class SearchScreen extends StatelessWidget { +class SearchScreen extends StatefulWidget { const SearchScreen({super.key}); + @override + State createState() => _SearchScreenState(); +} + +class _SearchScreenState extends State { + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - var screenWidth = MediaQuery.of(context).size.width; return Scaffold( backgroundColor: const Color(0xfff4f4f4), appBar: AppBar( + toolbarHeight: 70, backgroundColor: altWhite, - title: Padding( - padding: const EdgeInsets.only(top: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - icon: const Icon( - Icons.arrow_back_ios, - size: 25, - ), - onPressed: () {}, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon( + Icons.arrow_back_ios, + size: 25, ), - Container( - height: 50, - width: screenWidth - 150, - decoration: BoxDecoration( - color: white, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: gray200, width: 1), - ), - ), - IconButton( - icon: const Icon( - Icons.search, - size: 25, - ), - onPressed: () {}, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + textField( + _controller, + context, + (String value) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SearchResultScreen(query: value), + ), + ); + }, + ), + IconButton( + icon: const Icon( + Icons.search, + size: 25, ), - ], - ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + SearchResultScreen(query: _controller.text), + ), + ); + }, + ), + ], ), ), body: Column( children: [ Padding( - padding: const EdgeInsets.all(20.0), + padding: const EdgeInsets.all(30.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // 아래 모두 데이터 연동 & stateful 위젯으로의 전환 추후 진행예정 - Text( - "최근 검색어", - style: titleText(), - ), - const SizedBox( - height: 20, - ), + Text("최근 검색어", style: titleText()), + const SizedBox(height: 20), Text("인기 검색어", style: titleText()), - - const SizedBox( - height: 20, - ), - Text( - "인기 브랜드", - style: titleText(), - ), + const SizedBox(height: 20), + Text("인기 브랜드", style: titleText()), ], ), ), diff --git a/lib/widgets/textfield.dart b/lib/widgets/textfield.dart new file mode 100644 index 0000000..7f0c6df --- /dev/null +++ b/lib/widgets/textfield.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:saphy/utils/colors.dart'; +import 'package:saphy/utils/textstyles.dart'; + +SizedBox textField( + TextEditingController controller, BuildContext context, onSubmitted) { + var screenWidth = MediaQuery.of(context).size.width; + + return SizedBox( + width: screenWidth - 150, + child: TextField( + style: bodyText(), + controller: controller, + onSubmitted: onSubmitted, + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: gray800, width: 1), + borderRadius: BorderRadius.circular(15), + ), + border: OutlineInputBorder( + borderSide: const BorderSide(color: gray800, width: 1), + borderRadius: BorderRadius.circular(15), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: mainPrimary, width: 1), + borderRadius: BorderRadius.circular(15), + ), + hintText: '검색어를 입력하세요', + hintStyle: const TextStyle( + color: gray700, + ), + ), + ), + ); +} From 48f43d67a477880079d97969debb59be4975107c Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Tue, 8 Oct 2024 10:59:51 +0900 Subject: [PATCH 2/2] refactor : make app bar module --- lib/screens/products/item_list_page.dart | 49 +---------------------- lib/screens/products/liked_list_page.dart | 21 +--------- lib/widgets/app_bar.dart | 47 ++++++++++++++-------- 3 files changed, 34 insertions(+), 83 deletions(-) diff --git a/lib/screens/products/item_list_page.dart b/lib/screens/products/item_list_page.dart index 8b728b5..a9495d0 100644 --- a/lib/screens/products/item_list_page.dart +++ b/lib/screens/products/item_list_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:saphy/utils/colors.dart'; import 'package:saphy/utils/textstyles.dart'; +import 'package:saphy/widgets/app_bar.dart'; import 'package:saphy/widgets/product_card.dart'; import 'package:dio/dio.dart'; import 'package:saphy/models/product.dart'; @@ -59,10 +60,9 @@ class _ItemListPageState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xfff4f4f4), - appBar: _appBar(), + appBar: TopAppBar(label: widget.name), body: CustomScrollView( slivers: [ - _buildHeader(), const SliverToBoxAdapter( child: SizedBox(height: 10), ), @@ -73,51 +73,6 @@ class _ItemListPageState extends State { ); } - AppBar _appBar() { - return AppBar( - automaticallyImplyLeading: false, - backgroundColor: altWhite, - title: Padding( - padding: const EdgeInsets.only(top: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: const Icon( - Icons.arrow_back_ios, - size: 25, - ), - onPressed: () { - Navigator.pop(context); - }, - ), - IconButton( - icon: const Icon( - Icons.search, - size: 25, - ), - onPressed: () {}, - ), - ], - ), - ), - ); - } - - SliverToBoxAdapter _buildHeader() { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 30.0, - ), - child: Text( - widget.name, - style: titleText(), - ), - ), - ); - } - SliverPadding _buildSorter() { return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 30), diff --git a/lib/screens/products/liked_list_page.dart b/lib/screens/products/liked_list_page.dart index 0a77dff..ba9292a 100644 --- a/lib/screens/products/liked_list_page.dart +++ b/lib/screens/products/liked_list_page.dart @@ -6,6 +6,7 @@ import 'package:saphy/models/product.dart'; import 'package:saphy/utils/textstyles.dart'; import 'package:saphy/widgets/product_card.dart'; import 'package:saphy/utils/colors.dart'; +import 'package:saphy/widgets/app_bar.dart'; class LikedListPage extends StatefulWidget { const LikedListPage({super.key}); @@ -19,27 +20,9 @@ class _LikedListPageState extends State { @override Widget build(BuildContext context) { - var screenWidth = MediaQuery.of(context).size.width; return Scaffold( backgroundColor: const Color(0xfff4f4f4), - appBar: AppBar( - backgroundColor: altWhite, - title: Padding( - padding: const EdgeInsets.only(top: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios, size: 25), - onPressed: () {}, - ), - SizedBox(height: 50, width: screenWidth - 150), - IconButton( - icon: const Icon(Icons.search, size: 25), onPressed: () {}), - ], - ), - ), - ), + appBar: const TopAppBar(label: "찜 목록"), body: CustomScrollView( slivers: [ SliverPadding( diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index 92bdba3..c211f61 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -1,30 +1,43 @@ import 'package:flutter/material.dart'; +import 'package:saphy/utils/colors.dart'; +import 'package:saphy/utils/textstyles.dart'; class TopAppBar extends StatelessWidget implements PreferredSizeWidget { final AppBar? appBar; - const TopAppBar({super.key, this.appBar}); + final String? label; + const TopAppBar({super.key, this.appBar, this.label}); @override Widget build(BuildContext context) { return AppBar( - backgroundColor: Colors.white, - title: Padding( - padding: const EdgeInsets.only(left: 20.0), - child: Image.asset( - "assets/images/SaphyLogoSmall.png", - height: 24, - ), - ), - centerTitle: false, - actions: [ - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: IconButton( - icon: const Icon(Icons.notifications_outlined), + toolbarHeight: 70, + automaticallyImplyLeading: false, + backgroundColor: altWhite, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon( + Icons.arrow_back_ios, + size: 25, + ), + onPressed: () { + Navigator.pop(context); + }, + ), + Text( + label ?? "", + style: titleText(), + ), + IconButton( + icon: const Icon( + Icons.search, + size: 25, + ), onPressed: () {}, ), - ), - ], + ], + ), ); }