diff --git a/lib/core/config/router/router.dart b/lib/core/config/router/router.dart index d50ac9d..b8218ee 100644 --- a/lib/core/config/router/router.dart +++ b/lib/core/config/router/router.dart @@ -1,5 +1,6 @@ +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:jusicool_ios/main.dart'; +import 'package:jusicool_ios/core/config/widget/menu_bottom.dart'; import 'package:jusicool_ios/presentation/community/screens/community_post_list_screen.dart'; import 'package:jusicool_ios/presentation/my_capital/screens/maincapital_screen.dart'; import 'package:jusicool_ios/presentation/my_capital/screens/my_assets_screen.dart'; @@ -11,7 +12,7 @@ import 'package:jusicool_ios/presentation/sign_up/screens/find_school_screen.dar import 'package:jusicool_ios/presentation/sign_up/screens/name_input_screen.dart'; import 'package:jusicool_ios/presentation/sign_up/screens/password_create_screen.dart'; import 'package:jusicool_ios/presentation/splash/screens/splash_screen.dart'; - +import 'package:jusicool_ios/presentation/news/screens/news_list_screen.dart'; class RoutePaths { static const String splash = '/splash'; @@ -36,6 +37,9 @@ class AppRouter { factory AppRouter() => _instance; + static final GlobalKey _shellNavigatorKey = + GlobalKey(); + static final GoRouter router = GoRouter( initialLocation: RoutePaths.splash, routes: [ @@ -47,10 +51,6 @@ class AppRouter { path: RoutePaths.login, builder: (context, state) => LoginScreen(), ), - GoRoute( - path: RoutePaths.main, - builder: (context, state) => const MainPage(), - ), GoRoute( path: RoutePaths.nameInput, builder: (context, state) => const NameInputScreen(), @@ -67,10 +67,6 @@ class AppRouter { path: RoutePaths.findSchool, builder: (context, state) => const FindSchoolScreen(), ), - GoRoute( - path: RoutePaths.mainCapital, - builder: (context, state) => const MainCapitalScreen(), - ), GoRoute( path: RoutePaths.monthlyRevenue, builder: (context, state) => const MonthlyRevenueScreen(), @@ -87,6 +83,31 @@ class AppRouter { path: RoutePaths.communityPostList, builder: (context, state) => const CommunityPostListScreen(), ), + + ShellRoute( + navigatorKey: _shellNavigatorKey, + builder: (context, state, child) { + return MenuBottom(child: child); + }, + routes: [ + GoRoute( + path: RoutePaths.mainCapital, + builder: (context, state) => const MainCapitalScreen(), + ), + GoRoute( + path: '/chart', + builder: (context, state) => const MainCapitalScreen(), + ), + GoRoute( + path: '/news-list', + builder: (context, state) => const NewsListScreen(), + ), + GoRoute( + path: '/mypage', + builder: (context, state) => const MyAssetsScreen(), + ), + ], + ), ], ); } diff --git a/lib/core/config/widget/menu_bottom.dart b/lib/core/config/widget/menu_bottom.dart index f501153..d30c5c6 100644 --- a/lib/core/config/widget/menu_bottom.dart +++ b/lib/core/config/widget/menu_bottom.dart @@ -1,40 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:go_router/go_router.dart'; import 'package:jusicool_design_system/jusicool_design_system.dart'; -import 'package:jusicool_ios/presentation/my_capital/screens/maincapital_screen.dart'; - -class ScreenConfig { - ScreenConfig({required this.title, required this.widget}); - - final String title; - final Widget widget; -} - -class ScreenList { - static final List configs = [ - ScreenConfig(title: '자산', widget: const MainCapitalScreen()), - ScreenConfig(title: '차트', widget: const MainCapitalScreen()), - ScreenConfig(title: '뉴스', widget: const MainCapitalScreen()), - ScreenConfig( - title: '마이 페이지', - widget: const MainCapitalScreen(), - ), //임시 경로 설정, 추후 파일이 생성되면 변경 필요 - ]; - - static Widget getScreen(int index) { - if (index < 0 || index >= configs.length) { - return configs[0].widget; - } - return configs[index].widget; - } - - static String getTitle(int index) { - if (index < 0 || index >= configs.length) { - return configs[0].title; - } - return configs[index].title; - } -} class NavBarItem extends StatelessWidget { const NavBarItem({ @@ -91,24 +58,26 @@ class NavBarItem extends StatelessWidget { } } -class MenuBottom extends StatefulWidget { - const MenuBottom({super.key}); +class MenuBottom extends StatelessWidget { + const MenuBottom({super.key, required this.child}); + final Widget child; - @override - _MenuBottomState createState() => _MenuBottomState(); -} - -class _MenuBottomState extends State { - int selectedIndex = 0; + static const List<_NavItemData> _navItems = [ + _NavItemData(path: '/main-capital', iconName: 'capital', label: '자산'), + _NavItemData(path: '/main-capital', iconName: 'chart', label: '차트'), + _NavItemData(path: '/news-list', iconName: 'news', label: '뉴스'), + _NavItemData(path: '/main-capital', iconName: 'account', label: '마이 페이지'), + ]; - void onTap(int index) { - setState(() { - selectedIndex = index; - }); + int _locationToIndex(String location) { + return _navItems.indexWhere((item) => location.startsWith(item.path)); } @override Widget build(BuildContext context) { + final location = GoRouterState.of(context).uri.toString(); + final selectedIndex = _locationToIndex(location); + return Scaffold( backgroundColor: JusicoolColor.white, body: SafeArea( @@ -117,15 +86,7 @@ class _MenuBottomState extends State { maintainBottomViewPadding: true, child: Column( children: [ - Expanded( - child: IndexedStack( - index: selectedIndex, - children: List.generate( - ScreenList.configs.length, - (index) => ScreenList.getScreen(index), - ), - ), - ), + Expanded(child: child), Container( height: 52.h, color: JusicoolColor.white, @@ -148,10 +109,14 @@ class _MenuBottomState extends State { children: List.generate( _navItems.length, (index) => NavBarItem( - iconName: _navItems[index]['image']!, - label: ScreenList.getTitle(index), + iconName: _navItems[index].iconName, + label: _navItems[index].label, isSelected: selectedIndex == index, - onTap: () => onTap(index), + onTap: () { + if (selectedIndex != index) { + context.go(_navItems[index].path); + } + }, ), ), ), @@ -165,11 +130,16 @@ class _MenuBottomState extends State { ), ); } +} - static const List> _navItems = [ - {'label': '자산', 'image': 'capital'}, - {'label': '차트', 'image': 'chart'}, - {'label': '뉴스', 'image': 'news'}, - {'label': '마이 페이지', 'image': 'account'}, - ]; +class _NavItemData { + final String path; + final String iconName; + final String label; + + const _NavItemData({ + required this.path, + required this.iconName, + required this.label, + }); } diff --git a/lib/main.dart b/lib/main.dart index 7222dbc..70e84d5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,7 +7,6 @@ import 'package:jusicool_design_system/jusicool_design_system.dart'; import 'package:jusicool_ios/core/config/di/dependencies.dart'; import 'core/config/router/router.dart'; import 'core/config/theme/app_theme.dart'; -import 'core/config/widget/menu_bottom.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -50,12 +49,3 @@ class MyApp extends StatelessWidget { ); } } - -class MainPage extends StatelessWidget { - const MainPage({super.key}); - - @override - Widget build(BuildContext context) { - return const MenuBottom(); - } -} diff --git a/lib/presentation/my_capital/screens/maincapital_screen.dart b/lib/presentation/my_capital/screens/maincapital_screen.dart index 0105f81..4008eb6 100644 --- a/lib/presentation/my_capital/screens/maincapital_screen.dart +++ b/lib/presentation/my_capital/screens/maincapital_screen.dart @@ -3,274 +3,40 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:intl/intl.dart'; import 'package:jusicool_design_system/jusicool_design_system.dart'; import 'package:go_router/go_router.dart'; +import 'package:jusicool_ios/presentation/my_capital/widgets/stock_cards.dart'; -class StockCard extends StatelessWidget { - final String imagePath; - final String companyName; - final String stockCount; - final String amount; - final int changeValue; - final double changePercentage; - - const StockCard({ - super.key, - required this.imagePath, - required this.companyName, - required this.stockCount, - required this.amount, - required this.changeValue, - required this.changePercentage, - }); +class MainCapitalScreen extends StatelessWidget { + const MainCapitalScreen({super.key}); @override Widget build(BuildContext context) { - final numberFormat = NumberFormat("#,###", "en_US"); - String changeSign = changeValue >= 0 ? "+" : "-"; - String formattedChangeValue = numberFormat.format(changeValue.abs()); - String changeText = - "$changeSign$formattedChangeValue (${changePercentage.toStringAsFixed(1)}%)"; - Color changeColor; - if (changeValue > 0) { - changeColor = JusicoolColor.error; - } else if (changeValue < 0) { - changeColor = JusicoolColor.main; - } else { - changeColor = JusicoolColor.gray400; - } + //=================== + final NumberFormat currencyFormat = NumberFormat('#,###'); + final int investmentValue = 123456789; + final int changeValue = -6555778; + final double changePercent = 4.0; + final int monthlyOrderCount = 6; + final int monthlyProfitValue = 111111111; - return Padding( - padding: EdgeInsets.only(bottom: 4.h), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.only(right: 14.w), - child: Image.network( - imagePath, - width: 40.w, - height: 40.h, - fit: BoxFit.cover, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - companyName, - style: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: JusicoolColor.black, - ), - ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: Text( - stockCount, - style: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: JusicoolColor.gray400, - ), - ), - ), - ], - ), - Expanded(child: Container()), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - amount, - style: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: JusicoolColor.black, - ), - ), - Padding( - padding: EdgeInsets.only(top: 4.h), - child: Text( - changeText, - style: JusicoolTypography.label.copyWith( - fontSize: 12.sp, - fontWeight: FontWeight.w400, - height: 16 / 12, - letterSpacing: 0, - color: changeColor, - ), - ), - ), - ], - ), - ], - ), + final String formattedInvestmentValue = currencyFormat.format( + investmentValue, ); - } -} - -class CoinCard extends StatelessWidget { - final String imagePath; - final String companyName; - final String stockCount; - final String amount; - final int changeValue; - final double changePercentage; - - const CoinCard({ - super.key, - required this.imagePath, - required this.companyName, - required this.stockCount, - required this.amount, - required this.changeValue, - required this.changePercentage, - }); - - @override - Widget build(BuildContext context) { - final numberFormat = NumberFormat("#,###", "en_US"); - String changeSign = changeValue >= 0 ? "+" : "-"; - String formattedChangeValue = numberFormat.format(changeValue.abs()); - String changeText = - "$changeSign$formattedChangeValue (${changePercentage.toStringAsFixed(1)}%)"; - Color changeColor; - if (changeValue > 0) { - changeColor = JusicoolColor.error; - } else if (changeValue < 0) { - changeColor = JusicoolColor.main; - } else { - changeColor = JusicoolColor.gray400; - } - - return Padding( - padding: EdgeInsets.only(bottom: 4.h), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.only(right: 14.w), - child: Image.network( - imagePath, - width: 40.w, - height: 40.h, - fit: BoxFit.cover, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - companyName, - style: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: JusicoolColor.black, - ), - ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: Text( - stockCount, - style: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: JusicoolColor.gray400, - ), - ), - ), - ], - ), - Expanded(child: Container()), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - amount, - style: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: JusicoolColor.black, - ), - ), - Padding( - padding: EdgeInsets.only(top: 4.h), - child: Text( - changeText, - style: JusicoolTypography.label.copyWith( - fontSize: 12.sp, - fontWeight: FontWeight.w400, - height: 16 / 12, - letterSpacing: 0, - color: changeColor, - ), - ), - ), - ], - ), - ], - ), + final String formattedChangeValue = currencyFormat.format( + changeValue.abs(), ); - } -} - -class MainCapitalScreen extends StatelessWidget { - const MainCapitalScreen({super.key}); - - @override - Widget build(BuildContext context) { - const int investmentValue = 123456789; - const int changeValue = -6555778; - const double changePercent = 4.0; + final String formattedMonthlyProfit = currencyFormat.format( + monthlyProfitValue, + ); + final String formattedOrderCount = currencyFormat.format(monthlyOrderCount); - const int monthlyOrderCount = 6; - final String formattedOrderCount = NumberFormat( - '#,###', - ).format(monthlyOrderCount); + final String changeSign = changeValue >= 0 ? "+" : "-"; + final String changeText = + "$changeSign$formattedChangeValue원 (${changePercent.toStringAsFixed(1)}%)"; + final Color changeColor = + changeValue >= 0 ? JusicoolColor.error : JusicoolColor.main; final String monthlyOrderText = "이번달 $formattedOrderCount건"; - - const int monthlyProfitValue = 111111111; - final String formattedMonthlyProfit = NumberFormat( - '#,###', - ).format(monthlyProfitValue); final String monthlyProfit = "+$formattedMonthlyProfit원"; - - final String formattedInvestmentValue = NumberFormat( - '#,###', - ).format(investmentValue); - final String formattedChangeValue = NumberFormat( - '#,###', - ).format(changeValue.abs()); - - String changeSign = changeValue >= 0 ? "+" : "-"; - String changeText = - "$changeSign$formattedChangeValue원 (${changePercent.toStringAsFixed(1)}%)"; - Color changeColor; - if (changeValue > 0) { - changeColor = JusicoolColor.error; - } else if (changeValue < 0) { - changeColor = JusicoolColor.main; - } else { - changeColor = JusicoolColor.gray400; - } - - final List> stockData = [ + final stockData = [ { 'imagePath': 'https://1000logos.net/wp-content/uploads/2016/10/Apple-Logo-500x281.png', @@ -291,7 +57,7 @@ class MainCapitalScreen extends StatelessWidget { }, ]; - final List> coinData = [ + final coinData = [ { 'imagePath': 'https://1000logos.net/wp-content/uploads/2016/10/Apple-Logo-500x281.png', @@ -311,393 +77,134 @@ class MainCapitalScreen extends StatelessWidget { 'changePercentage': 0.9, }, ]; + //=================== return Scaffold( backgroundColor: JusicoolColor.white, - appBar: PreferredSize( - preferredSize: Size.fromHeight(72.h), - child: AppBar( - scrolledUnderElevation: 0, - backgroundColor: JusicoolColor.white, - elevation: 0, - leading: Container(), - flexibleSpace: Padding( - padding: EdgeInsets.only(top: 40.h, left: 24.w), - child: Align( - alignment: Alignment.centerLeft, - child: JusicoolImage.logo(width: 116.w, height: 16.81.h), - ), + appBar: AppBar( + automaticallyImplyLeading: false, + scrolledUnderElevation: 0, + backgroundColor: JusicoolColor.white, + elevation: 0, + flexibleSpace: Padding( + padding: EdgeInsets.only(top: 40.h, left: 24.w), + child: Align( + alignment: Alignment.centerLeft, + child: JusicoolImage.logo(width: 116.w, height: 16.81.h), ), ), ), body: SingleChildScrollView( child: Column( - crossAxisAlignment: CrossAxisAlignment.center, + spacing: 8.h, children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 24.h, children: [ GestureDetector( - onTap: () { - context.push('/login'); - }, + onTap: () => context.push('/my-assets'), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 2.h, children: [ - Padding( - padding: EdgeInsets.only(bottom: 4.h), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "내 자산", - style: JusicoolTypography.bodyMedium.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - height: 22 / 16, - letterSpacing: 0, - color: JusicoolColor.black, - ), - ), - Padding( - padding: EdgeInsets.only(left: 4.w), - child: Icon( - Icons.arrow_forward_ios, - size: 15.w, - color: JusicoolColor.black, - ), + Row( + spacing: 4.w, + children: [ + Text( + "내 자산", + style: JusicoolTypography.bodyMedium.copyWith( + color: JusicoolColor.black, ), - ], - ), + ), + const Icon( + Icons.arrow_forward_ios, + size: 16, + color: JusicoolColor.black, + ), + ], ), Text( "$formattedInvestmentValue원", style: JusicoolTypography.titleSmall.copyWith( - fontSize: 24.sp, - fontWeight: FontWeight.w600, - height: 31 / 24, - letterSpacing: 0, color: JusicoolColor.black, ), ), ], ), ), - Padding( - padding: EdgeInsets.only(top: 24.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "투자 자산", - style: JusicoolTypography.bodyMedium.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - height: 22 / 16, - letterSpacing: 0, - color: JusicoolColor.black, - ), + + /// 투자 자산 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 2.h, + children: [ + Text( + "투자 자산", + style: JusicoolTypography.bodyMedium.copyWith( + color: JusicoolColor.black, ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: Text( - "$formattedInvestmentValue원", - style: JusicoolTypography.titleMedium.copyWith( - fontSize: 36.sp, - fontWeight: FontWeight.w600, - height: 43 / 36, - letterSpacing: 0, - color: JusicoolColor.black, - ), - ), + ), + Text( + "$formattedInvestmentValue원", + style: JusicoolTypography.titleMedium.copyWith( + color: JusicoolColor.black, ), - Padding( - padding: EdgeInsets.only(top: 2.h), - child: Text( - changeText, - style: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: changeColor, - ), - ), + ), + Text( + changeText, + style: JusicoolTypography.bodySmall.copyWith( + color: changeColor, ), - ], - ), + ), + ], ), - Padding( - padding: EdgeInsets.only(top: 24.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - color: JusicoolColor.white, - child: Padding( - padding: EdgeInsets.only(top: 16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "보유 주식&코인", - style: JusicoolTypography.subTitle.copyWith( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - height: 27 / 18, - letterSpacing: 0, - color: JusicoolColor.black, - ), - ), - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Text( - "주식", - style: JusicoolTypography.bodySmall - .copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: JusicoolColor.black, - ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Column( - children: List.generate(stockData.length, ( - index, - ) { - final stock = stockData[index]; - return StockCard( - imagePath: stock['imagePath'] as String, - companyName: - stock['companyName'] as String, - stockCount: - stock['stockCount'] as String, - amount: stock['amount'] as String, - changeValue: - stock['changeValue'] as int, - changePercentage: - stock['changePercentage'] as double, - ); - }), - ), - ), - Padding( - padding: EdgeInsets.only(top: 16.h), - child: Text( - "코인", - style: JusicoolTypography.bodySmall - .copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: JusicoolColor.black, - ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Column( - children: List.generate(coinData.length, ( - index, - ) { - final coin = coinData[index]; - return CoinCard( - imagePath: coin['imagePath'] as String, - companyName: - coin['companyName'] as String, - stockCount: - coin['stockCount'] as String, - amount: coin['amount'] as String, - changeValue: coin['changeValue'] as int, - changePercentage: - coin['changePercentage'] as double, - ); - }), - ), - ), - Padding( - padding: EdgeInsets.only( - top: 20.h, - bottom: 20.h, - ), - child: Container( - height: 1.h, - width: 312.w, - color: JusicoolColor.gray400, - ), - ), - Container( - width: 312.w, - height: 60.h, - color: JusicoolColor.white, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - width: 312.w, - height: 26.h, - color: JusicoolColor.white, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Text( - "주문내역", - style: JusicoolTypography - .bodySmall - .copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: JusicoolColor.black, - ), - ), - GestureDetector( - onTap: () { - context.push('/order-detail'); - }, - child: Row( - children: [ - Text( - monthlyOrderText, - style: JusicoolTypography - .bodySmall - .copyWith( - fontSize: 14.sp, - fontWeight: - FontWeight.w400, - height: 16 / 14, - letterSpacing: 0, - color: - JusicoolColor - .gray600, - ), - ), - Padding( - padding: EdgeInsets.only( - left: 4.w, - ), - child: - JusicoolIcon.forwardArrow( - width: 24.w, - height: 24.h, - ), - ), - ], - ), - ), - ], - ), - ), - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Container( - width: 312.w, - height: 26.h, - color: JusicoolColor.white, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Text( - "이번달 수익", - style: JusicoolTypography - .bodySmall - .copyWith( - fontSize: 16.sp, - fontWeight: - FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: - JusicoolColor.black, - ), - ), - GestureDetector( - onTap: () { - context.push( - '/monthly-revenue', - ); - }, - child: Row( - children: [ - Text( - monthlyProfit, - style: JusicoolTypography - .bodySmall - .copyWith( - fontSize: 14.sp, - fontWeight: - FontWeight.w400, - height: 16 / 14, - letterSpacing: 0, - color: - JusicoolColor - .gray600, - ), - ), - Padding( - padding: EdgeInsets.only( - left: 4.w, - ), - child: - JusicoolIcon.forwardArrow( - width: 24.w, - height: 24.h, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ], - ), - ), - ], - ), - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + Text( + "보유 주식&코인", + style: JusicoolTypography.subTitle.copyWith( + color: JusicoolColor.black, ), - ], - ), + ), + _buildAssetSection("주식", stockData, currencyFormat), + _buildAssetSection("코인", coinData, currencyFormat), + ], + ), + Divider(height: 1.h, color: JusicoolColor.gray400), + Column( + spacing: 8.h, + children: [ + _buildSummaryRow( + "주문내역", + monthlyOrderText, + () => context.push('/order-detail'), + ), + _buildSummaryRow( + "이번달 수익", + monthlyProfit, + () => context.push('/monthly-revenue'), + ), + ], ), ], ), ), - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Container( - width: double.infinity, - height: 24.h, - color: JusicoolColor.gray100, - ), + Container( + width: double.infinity, + height: 24.h, + color: JusicoolColor.gray100, ), Container( - width: 360.w, - height: 361.h, - padding: EdgeInsets.only( - top: 16.h, - right: 24.w, - bottom: 16.h, - left: 24.w, - ), + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h), color: JusicoolColor.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16.h, children: [ Text( "보유 종목 뉴스", @@ -707,61 +214,41 @@ class MainCapitalScreen extends StatelessWidget { color: JusicoolColor.black, ), ), - Padding( - padding: EdgeInsets.only(top: 16.h), - child: GestureDetector( - onTap: () { - context.push('/login'); - }, - child: Container( - width: 312.w, - height: 244.h, - padding: EdgeInsets.zero, - color: JusicoolColor.white, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 312.w, - height: 156.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.r), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12.r), - child: Image.network( - 'https://gongu.copyright.or.kr/gongu/wrt/cmmn/wrtFileImageView.do?wrtSn=11288734&filePath=L2Rpc2sxL25ld2RhdGEvMjAxNS8wMi9DTFM2OS9OVVJJXzAwMV8wMjIwX251cmltZWRpYV8yMDE1MTIwMw==&thumbAt=Y&thumbSe=b_tbumb&wrtTy=10006', - fit: BoxFit.cover, - ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Text( - "애플, 사상 최고가... 올해 세계경제 2.6% 성장 전망", - style: JusicoolTypography.bodyMedium.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: JusicoolColor.black, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Text( - "이데일리", - style: JusicoolTypography.label.copyWith( - fontSize: 14.sp, - fontWeight: FontWeight.w400, - color: JusicoolColor.gray400, - ), - ), + GestureDetector( + onTap: () => context.push('/login'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12.r), + child: AspectRatio( + aspectRatio: 2, + child: Image.network( + 'https://gongu.copyright.or.kr/gongu/wrt/cmmn/wrtFileImageView.do?wrtSn=11288734&filePath=L2Rpc2sxL25ld2RhdGEvMjAxNS8wMi9DTFM2OS9OVVJJXzAwMV8wMjIwX251cmltZWRpYV8yMDE1MTIwMw==&thumbAt=Y&thumbSe=b_tbumb&wrtTy=10006', + fit: BoxFit.cover, ), - ], + ), ), - ), + Text( + "애플, 사상 최고가... 올해 세계경제 2.6% 성장 전망", + style: JusicoolTypography.bodyMedium.copyWith( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: JusicoolColor.black, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Text( + "2024.07.01", + style: JusicoolTypography.bodySmall.copyWith( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: JusicoolColor.gray400, + ), + ), + ], ), ), ], @@ -772,4 +259,67 @@ class MainCapitalScreen extends StatelessWidget { ), ); } + + Widget _buildSummaryRow(String title, String value, VoidCallback onTap) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: JusicoolTypography.bodySmall.copyWith( + fontSize: 16.sp, + fontWeight: FontWeight.w400, + color: JusicoolColor.black, + ), + ), + GestureDetector( + onTap: onTap, + child: Row( + spacing: 1.w, + children: [ + Text( + value, + style: JusicoolTypography.bodySmall.copyWith( + fontSize: 14.sp, + color: JusicoolColor.gray600, + ), + ), + JusicoolIcon.forwardArrow(width: 24.w, height: 24.h), + ], + ), + ), + ], + ); + } + + Widget _buildAssetSection( + String title, + List> dataList, + NumberFormat currencyFormat, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + Text(title, style: JusicoolTypography.bodySmall), + ...dataList.map((item) { + final isPositive = (item['changeValue'] as int) >= 0; + final sign = isPositive ? "+" : "-"; + final changeValueStr = currencyFormat.format( + (item['changeValue'] as int).abs(), + ); + final priceChange = + "$sign$changeValueStr원 (${(item['changePercentage'] as double).toStringAsFixed(1)}%)"; + return StockCards( + companyName: item['companyName'] as String, + logoUrl: item['imagePath'] as String, + price: item['amount'] as String, + priceChange: priceChange, + share: item['stockCount'] as String, + isPositive: isPositive, + ); + }), + ], + ); + } } diff --git a/lib/presentation/my_capital/screens/my_assets_screen.dart b/lib/presentation/my_capital/screens/my_assets_screen.dart index 05bd9e0..d787508 100644 --- a/lib/presentation/my_capital/screens/my_assets_screen.dart +++ b/lib/presentation/my_capital/screens/my_assets_screen.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:intl/intl.dart'; import 'package:jusicool_design_system/jusicool_design_system.dart'; import 'package:jusicool_ios/presentation/my_capital/widgets/my_asset_tile.dart'; @@ -23,7 +24,6 @@ class _MyAssetsScreenState extends State { _futureData = _loadAssetsData(); } - /* --------------------- 데이터 로드 --------------------- */ Future _loadAssetsData() async { final jsonString = await rootBundle.loadString( 'assets/data/my_assets.json', @@ -32,10 +32,9 @@ class _MyAssetsScreenState extends State { return MyAssetsData.fromJson(jsonMap); } - /* ----------------- HEX → Color 유틸 ------------------ */ Color hexToColor(String hex) { final buffer = StringBuffer(); - if (hex.length == 7) buffer.write('ff'); // 투명도(100%) + if (hex.length == 7) buffer.write('ff'); buffer.write(hex.replaceFirst('#', '')); return Color(int.parse(buffer.toString(), radix: 16)); } @@ -51,7 +50,11 @@ class _MyAssetsScreenState extends State { backgroundColor: JusicoolColor.white, centerTitle: true, elevation: 0, - leading: const BackButton(color: JusicoolColor.black), + leading: IconButton( + padding: EdgeInsets.only(left: 24.sp), + icon: const Icon(Icons.arrow_back, color: JusicoolColor.black), + onPressed: () => Navigator.of(context).pop(), + ), title: Text('내 자산', style: JusicoolTypography.subTitle), ), body: SafeArea( @@ -64,69 +67,83 @@ class _MyAssetsScreenState extends State { if (snap.hasError) { return Center(child: Text('에러: ${snap.error}')); } + final data = snap.data!; + return SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24), + padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 56.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 24.h, children: [ - const SizedBox(height: 24), - Text( - "${formatter.format(data.totalAsset)}원", - style: JusicoolTypography.titleMedium, - ), - const SizedBox(height: 8), - RichText( - text: TextSpan( - children: [ - TextSpan( - text: '지난 달 보다 ', - style: JusicoolTypography.bodySmall, - ), - TextSpan( - text: '${formatter.format(data.change)}원 ', - style: JusicoolTypography.bodySmall.copyWith( - color: Colors.red, - ), - ), - TextSpan( - text: '늘었어요', - style: JusicoolTypography.bodySmall, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4.h, + children: [ + Text( + "${formatter.format(data.totalAsset)}원", + style: JusicoolTypography.titleMedium, + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: '지난 달 보다 ', + style: JusicoolTypography.bodySmall, + ), + TextSpan( + text: '${formatter.format(data.change)}원 ', + style: JusicoolTypography.bodySmall.copyWith( + color: Colors.red, + ), + ), + TextSpan( + text: '늘었어요', + style: JusicoolTypography.bodySmall, + ), + ], ), - ], - ), - ), - const SizedBox(height: 32), - Text( - "주문 가능 금액", - style: JusicoolTypography.bodyMedium.copyWith( - color: JusicoolColor.gray600, - ), - ), - const SizedBox(height: 4), - Text( - "${formatter.format(data.availableAmount)}원", - style: JusicoolTypography.titleSmall, + ), + ], ), - const SizedBox(height: 24), - Text( - "투자 금액", - style: JusicoolTypography.bodyMedium.copyWith( - color: JusicoolColor.gray600, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 2.h, + children: [ + Text( + "주문 가능 금액", + style: JusicoolTypography.bodyMedium.copyWith( + color: JusicoolColor.gray600, + ), + ), + Text( + "${formatter.format(data.availableAmount)}원", + style: JusicoolTypography.titleSmall, + ), + ], ), - const SizedBox(height: 4), - Text( - "${formatter.format(data.investmentAmount)}원", - style: JusicoolTypography.titleSmall, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 2.h, + children: [ + Text( + "투자 금액", + style: JusicoolTypography.bodyMedium.copyWith( + color: JusicoolColor.gray600, + ), + ), + Text( + "${formatter.format(data.investmentAmount)}원", + style: JusicoolTypography.titleSmall, + ), + ], ), - const SizedBox(height: 32), - SizedBox( - height: 200, + AspectRatio( + aspectRatio: 1.5, child: PieChart( PieChartData( sectionsSpace: 4, - centerSpaceRadius: 80, + centerSpaceRadius: 70.h, sections: data.sections .map( @@ -134,33 +151,25 @@ class _MyAssetsScreenState extends State { color: hexToColor(s.colorHex), value: s.percentage, title: '', - radius: 45, + radius: 40.h, ), ) .toList(), ), ), ), - const SizedBox(height: 24), - - /* ---------- 자산 리스트: ListView.separated ---------- */ - ListView.separated( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: data.sections.length, - separatorBuilder: (_, _) => const SizedBox(height: 24), - itemBuilder: (context, index) { - final s = data.sections[index]; - return MyAssetTile( - stockName: s.name, - stockPrice: "${formatter.format(s.price)}원", - percentage: "${s.percentage.toStringAsFixed(1)}%", - iconColor: hexToColor(s.colorHex), - ); - }, + Column( + spacing: 16.h, + children: + data.sections.map((s) { + return MyAssetTile( + stockName: s.name, + stockPrice: "${formatter.format(s.price)}원", + percentage: "${s.percentage.toStringAsFixed(1)}%", + iconColor: hexToColor(s.colorHex), + ); + }).toList(), ), - - const SizedBox(height: 32), ], ), ); diff --git a/lib/presentation/my_capital/screens/order_screens/order_detail_screen.dart b/lib/presentation/my_capital/screens/order_screens/order_detail_screen.dart index 900c5e8..7c8b0e0 100644 --- a/lib/presentation/my_capital/screens/order_screens/order_detail_screen.dart +++ b/lib/presentation/my_capital/screens/order_screens/order_detail_screen.dart @@ -7,12 +7,19 @@ class OrderDetailScreen extends StatefulWidget { const OrderDetailScreen({super.key}); @override - _OrderDetailScreenState createState() => _OrderDetailScreenState(); + State createState() => _OrderDetailScreenState(); } class _OrderDetailScreenState extends State with SingleTickerProviderStateMixin { - late TabController _tabController; + late final TabController _tabController; + + final List> _completedOrders = _generateDummyOrders( + "판매 완료", + ); + final List> _reservedOrders = _generateDummyOrders( + "구매 예약", + ); @override void initState() { @@ -26,10 +33,9 @@ class _OrderDetailScreenState extends State super.dispose(); } - //==================================== - - List> _generateDummyCompletedOrders() { - final companies = [ + //===================== + static List> _generateDummyOrders(String type) { + const companies = [ "애플", "삼성", "테슬라", @@ -41,56 +47,16 @@ class _OrderDetailScreenState extends State "엔비디아", "인텔", ]; - final List> orders = []; - for (int i = 0; i < 20; i++) { - final companyIndex = i % companies.length; + return List.generate(20, (i) { + final company = companies[i % companies.length]; final amount = (i % 2 == 0 ? 1 : -1) * (37250 + i * 1000); - orders.add({'companyName': companies[companyIndex], 'amount': amount}); - } - - return orders.length > 100 ? orders.sublist(0, 100) : orders; + return {'companyName': company, 'amount': amount, 'statusText': type}; + }); } + //===================== - List> _generateDummyReservedOrders() { - final companies = [ - "애플", - "삼성", - "테슬라", - "구글", - "아마존", - "마이크로소프트", - "페이스북", - "넷플릭스", - "엔비디아", - "인텔", - ]; - final List> orders = []; - for (int i = 0; i < 20; i++) { - final companyIndex = i % companies.length; - final amount = (i % 2 == 0 ? 1 : -1) * (37250 + i * 1000); - orders.add({'companyName': companies[companyIndex], 'amount': amount}); - } - - return orders; - } - - //==================================== @override Widget build(BuildContext context) { - final dummyCompletedOrders = _generateDummyCompletedOrders(); - final dummyReservedOrders = _generateDummyReservedOrders(); - - final statusBarHeight = MediaQuery.of(context).padding.top; - - const double jusicoolBarHeight = kToolbarHeight; - - const tabBarHeight = 48.0; - - final adjustedTopPadding = - (176.h - statusBarHeight - jusicoolBarHeight - tabBarHeight) > 0 - ? (176.h - statusBarHeight - jusicoolBarHeight - tabBarHeight) - : 0.0; - return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( @@ -98,96 +64,68 @@ class _OrderDetailScreenState extends State elevation: 0, centerTitle: true, title: Text("주문내역", style: JusicoolTypography.subTitle), - bottom: TabBar( - controller: _tabController, - labelColor: JusicoolColor.black, - unselectedLabelColor: JusicoolColor.gray400, - padding: EdgeInsets.symmetric(horizontal: 24.sp), - indicatorColor: JusicoolColor.black, - automaticIndicatorColorAdjustment: true, - indicatorWeight: 1.0, - indicatorPadding: EdgeInsets.zero, - overlayColor: WidgetStateProperty.resolveWith(( - Set states, - ) { - return states.contains(WidgetState.focused) - ? null - : JusicoolColor.white; - }), - splashFactory: NoSplash.splashFactory, - indicator: const BoxDecoration( - color: JusicoolColor.white, - border: Border( - bottom: BorderSide(color: JusicoolColor.black, width: 1.0), - ), - ), - labelStyle: JusicoolTypography.bodyMedium, - unselectedLabelStyle: JusicoolTypography.bodySmall, - indicatorSize: TabBarIndicatorSize.tab, - tabs: const [Tab(text: "완료된 주문"), Tab(text: "주문 예약")], + bottom: _buildTabBar(), + leading: IconButton( + padding: EdgeInsets.only(left: 24.sp), + icon: const Icon(Icons.arrow_back, color: JusicoolColor.black), + onPressed: () { + Navigator.of(context).pop(); + }, ), ), body: TabBarView( controller: _tabController, children: [ - Container( - color: JusicoolColor.white, - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.only( - top: adjustedTopPadding, - left: 24.sp, - bottom: 24.sp, - ), - child: Column( - children: List.generate(dummyCompletedOrders.length, (index) { - final order = dummyCompletedOrders[index]; - return Column( - children: [ - OrderItem( - companyName: order['companyName'] as String, - amount: order['amount'] as int, - statusText: "판매 완료", - ), - if (index < dummyCompletedOrders.length - 1) - SizedBox(height: 24.h), - ], - ); - }), - ), - ), - ), - ), - Container( - color: JusicoolColor.white, - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.only( - top: adjustedTopPadding, - left: 24.sp, - bottom: 24.sp, - ), - child: Column( - children: List.generate(dummyReservedOrders.length, (index) { - final order = dummyReservedOrders[index]; - return Column( - children: [ - OrderItem( - companyName: order['companyName'] as String, - amount: order['amount'] as int, - statusText: "구매 예약", - ), - if (index < dummyReservedOrders.length - 1) - SizedBox(height: 24.h), - ], - ); - }), - ), - ), - ), - ), + _buildOrderListView(orders: _completedOrders), + _buildOrderListView(orders: _reservedOrders), ], ), ); } + + PreferredSizeWidget _buildTabBar() { + return TabBar( + controller: _tabController, + labelColor: JusicoolColor.black, + unselectedLabelColor: JusicoolColor.gray400, + padding: EdgeInsets.symmetric(horizontal: 24.sp), + indicatorColor: JusicoolColor.black, + indicatorPadding: EdgeInsets.zero, + overlayColor: WidgetStateProperty.resolveWith( + (states) => + states.contains(WidgetState.focused) ? null : JusicoolColor.white, + ), + splashFactory: NoSplash.splashFactory, + indicator: const BoxDecoration( + color: JusicoolColor.white, + border: Border(bottom: BorderSide(color: JusicoolColor.black)), + ), + labelStyle: JusicoolTypography.bodyMedium, + unselectedLabelStyle: JusicoolTypography.bodySmall, + indicatorSize: TabBarIndicatorSize.tab, + tabs: const [Tab(text: "완료된 주문"), Tab(text: "주문 예약")], + ); + } + + Widget _buildOrderListView({required List> orders}) { + return Container( + color: JusicoolColor.white, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only(top: 24.h, left: 24.sp, bottom: 24.sp), + child: Column( + spacing: 24.h, + children: + orders.map((order) { + return OrderItem( + companyName: order['companyName'] as String, + amount: order['amount'] as int, + statusText: order['statusText'] as String, + ); + }).toList(), + ), + ), + ), + ); + } } diff --git a/lib/presentation/my_capital/screens/order_screens/order_item.dart b/lib/presentation/my_capital/screens/order_screens/order_item.dart index 2751897..e793066 100644 --- a/lib/presentation/my_capital/screens/order_screens/order_item.dart +++ b/lib/presentation/my_capital/screens/order_screens/order_item.dart @@ -31,18 +31,12 @@ class OrderItem extends StatelessWidget { Text( companyName, style: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, color: JusicoolColor.black, ), ), Text( "$formattedAmount원 $statusText", - style: JusicoolTypography.label.copyWith( - fontSize: 12.sp, - fontWeight: FontWeight.w400, - color: changeColor, - ), + style: JusicoolTypography.label.copyWith(color: changeColor), ), ], ), diff --git a/lib/presentation/my_capital/screens/revenue_screens/monthlyrevenue_screen.dart b/lib/presentation/my_capital/screens/revenue_screens/monthlyrevenue_screen.dart index 93cf137..66dc3fb 100644 --- a/lib/presentation/my_capital/screens/revenue_screens/monthlyrevenue_screen.dart +++ b/lib/presentation/my_capital/screens/revenue_screens/monthlyrevenue_screen.dart @@ -5,32 +5,17 @@ import 'package:jusicool_design_system/jusicool_design_system.dart'; import 'package:jusicool_ios/presentation/my_capital/screens/revenue_screens/revenuecard.dart'; import 'package:go_router/go_router.dart'; -const adjustedTopPadding = 16.0; - class MonthlyRevenueScreen extends StatefulWidget { const MonthlyRevenueScreen({super.key}); @override - _MonthlyRevenueScreenState createState() => _MonthlyRevenueScreenState(); + State createState() => _MonthlyRevenueScreenState(); } class _MonthlyRevenueScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; - @override - void initState() { - super.initState(); - _tabController = TabController(length: 3, vsync: this); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - //========================= final List> revenueData = [ { 'date': '1월 31일', @@ -49,7 +34,6 @@ class _MonthlyRevenueScreenState extends State 'companyName': '삼성', 'amount': 987654321, 'changeValue': 2000000, - 'changePercentage': 2.5, 'isStock': true, }, @@ -94,23 +78,39 @@ class _MonthlyRevenueScreenState extends State 'isStock': false, }, ]; - //========================= + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } Map _calculateTotalRevenue() { final numberFormat = NumberFormat("#,###", "en_US"); - int totalChange = revenueData.fold( + + final totalChange = revenueData.fold( 0, (sum, item) => sum + (item['changeValue'] as int), ); - int totalAmount = revenueData.fold( + + final totalAmount = revenueData.fold( 0, (sum, item) => sum + (item['amount'] as int), ); - double weightedPercentage = - revenueData.fold(0.0, (sum, item) { - return sum + - ((item['amount'] as int) * (item['changePercentage'] as double)); - }) / + + final weightedPercentage = + revenueData.fold( + 0.0, + (sum, item) => + sum + + ((item['amount'] as int) * (item['changePercentage'] as double)), + ) / (totalAmount != 0 ? totalAmount : 1); return { @@ -131,6 +131,7 @@ class _MonthlyRevenueScreenState extends State totalChange >= 0 ? "+$formattedChange원 (${weightedPercentage.toStringAsFixed(1)}%)" : "-$formattedChange원 (${weightedPercentage.toStringAsFixed(1)}%)"; + final revenueColor = totalChange > 0 ? JusicoolColor.error @@ -138,14 +139,11 @@ class _MonthlyRevenueScreenState extends State ? JusicoolColor.main : JusicoolColor.gray400; - List> filteredData = revenueData; - if (_tabController.index == 1) { - filteredData = - revenueData.where((item) => item['isStock'] == true).toList(); - } else if (_tabController.index == 2) { - filteredData = - revenueData.where((item) => item['isStock'] == false).toList(); - } + List> filteredData = switch (_tabController.index) { + 1 => revenueData.where((item) => item['isStock'] == true).toList(), + 2 => revenueData.where((item) => item['isStock'] == false).toList(), + _ => revenueData, + }; return Scaffold( appBar: AppBar( @@ -153,124 +151,95 @@ class _MonthlyRevenueScreenState extends State backgroundColor: JusicoolColor.white, elevation: 0, centerTitle: true, + leading: IconButton( + padding: EdgeInsets.only(left: 24.sp), + icon: const Icon(Icons.arrow_back, color: JusicoolColor.black), + onPressed: () => context.pop(), + ), title: Text( "이번 달 수익", style: JusicoolTypography.subTitle.copyWith( - fontSize: 18.sp, - fontWeight: FontWeight.w600, color: JusicoolColor.black, ), ), - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: JusicoolColor.black), - onPressed: () { - context.pop(); - }, - ), ), body: NestedScrollView( - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { - return [ - SliverToBoxAdapter( - child: Container( - color: JusicoolColor.white, - padding: EdgeInsets.only(top: 9.h, left: 24.sp, bottom: 16.h), - child: Text( - revenueText, - style: JusicoolTypography.titleSmall.copyWith( - fontSize: 24.sp, - fontWeight: FontWeight.w600, - color: revenueColor, + headerSliverBuilder: + (context, _) => [ + SliverToBoxAdapter( + child: Container( + color: JusicoolColor.white, + padding: EdgeInsets.only(top: 9.h, left: 24.sp, bottom: 16.h), + child: Text( + revenueText, + style: JusicoolTypography.titleSmall.copyWith( + color: revenueColor, + ), ), ), ), - ), - SliverPersistentHeader( - pinned: true, - delegate: _SliverJusicoolBarDelegate( - TabBar( - controller: _tabController, - labelColor: JusicoolColor.black, - unselectedLabelColor: JusicoolColor.gray400, - padding: EdgeInsets.symmetric(horizontal: 24.sp), - indicatorColor: JusicoolColor.black, - automaticIndicatorColorAdjustment: true, - indicatorWeight: 1.0, - indicatorPadding: EdgeInsets.zero, - overlayColor: WidgetStateProperty.resolveWith(( - Set states, - ) { - return states.contains(WidgetState.focused) - ? null - : JusicoolColor.white; - }), - splashFactory: NoSplash.splashFactory, - indicator: const BoxDecoration( - color: JusicoolColor.white, - border: Border( - bottom: BorderSide( - color: JusicoolColor.black, - width: 1.0, + SliverPersistentHeader( + pinned: true, + delegate: _SliverJusicoolBarDelegate( + TabBar( + controller: _tabController, + labelColor: JusicoolColor.black, + unselectedLabelColor: JusicoolColor.gray400, + padding: EdgeInsets.symmetric(horizontal: 24.sp), + indicatorColor: JusicoolColor.black, + indicator: const BoxDecoration( + color: JusicoolColor.white, + border: Border( + bottom: BorderSide(color: JusicoolColor.black), ), ), + indicatorPadding: EdgeInsets.zero, + splashFactory: NoSplash.splashFactory, + overlayColor: WidgetStateProperty.all(Colors.transparent), + labelStyle: JusicoolTypography.bodyMedium, + unselectedLabelStyle: JusicoolTypography.bodySmall, + indicatorSize: TabBarIndicatorSize.tab, + tabs: const [ + Tab(text: "전체"), + Tab(text: "주식"), + Tab(text: "코인"), + ], + onTap: (_) => setState(() {}), ), - labelStyle: JusicoolTypography.bodyMedium, - unselectedLabelStyle: JusicoolTypography.bodySmall, - indicatorSize: TabBarIndicatorSize.tab, - tabs: const [ - Tab(text: "전체"), - Tab(text: "주식"), - Tab(text: "코인"), - ], - onTap: (index) { - setState(() {}); - }, ), ), - ), - ]; - }, + ], body: Container( color: JusicoolColor.white, child: Padding( - padding: EdgeInsets.only(left: 24.sp, top: adjustedTopPadding.h), - child: ListView.builder( + padding: EdgeInsets.only(left: 24.sp, top: 16.h), + child: ListView.separated( itemCount: filteredData.length, + separatorBuilder: (context, index) => SizedBox(height: 16.h), itemBuilder: (context, index) { final item = filteredData[index]; final date = item['date'] as String; final isNewDate = index == 0 || filteredData[index - 1]['date'] != date; - return Padding( - padding: EdgeInsets.only( - top: isNewDate ? 16.h : 16.h, // 수직 간격을 Padding으로 처리 - bottom: index == filteredData.length - 1 ? 0 : 0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isNewDate) - Padding( - padding: EdgeInsets.only(bottom: 4.h), // 날짜와 카드 사이 간격 - child: Text( - date, - style: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - color: JusicoolColor.black, - ), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4.h, + children: [ + if (isNewDate) + Text( + date, + style: JusicoolTypography.bodySmall.copyWith( + color: JusicoolColor.black, ), - // 수평 간격은 RevenueCard 내부에서 Row + Padding/margin으로 처리 - RevenueCard( - imagePath: item['imagePath'] as String, - companyName: item['companyName'] as String, - amount: item['amount'] as int, - changeValue: item['changeValue'] as int, - changePercentage: item['changePercentage'] as double, ), - ], - ), + RevenueCard( + imagePath: item['imagePath'] as String, + companyName: item['companyName'] as String, + amount: item['amount'] as int, + changeValue: item['changeValue'] as int, + changePercentage: item['changePercentage'] as double, + ), + ], ); }, ), @@ -288,7 +257,6 @@ class _SliverJusicoolBarDelegate extends SliverPersistentHeaderDelegate { @override double get minExtent => _tabBar.preferredSize.height; - @override double get maxExtent => _tabBar.preferredSize.height; @@ -302,7 +270,5 @@ class _SliverJusicoolBarDelegate extends SliverPersistentHeaderDelegate { } @override - bool shouldRebuild(_SliverJusicoolBarDelegate oldDelegate) { - return false; - } + bool shouldRebuild(covariant _SliverJusicoolBarDelegate oldDelegate) => false; } diff --git a/lib/presentation/my_capital/screens/revenue_screens/revenuecard.dart b/lib/presentation/my_capital/screens/revenue_screens/revenuecard.dart index 3893398..00b26cd 100644 --- a/lib/presentation/my_capital/screens/revenue_screens/revenuecard.dart +++ b/lib/presentation/my_capital/screens/revenue_screens/revenuecard.dart @@ -21,13 +21,8 @@ class RevenueCard extends StatelessWidget { String getFormattedAmount() { final numberFormat = NumberFormat("#,###", "en_US"); - if (changeValue > 0) { - return "+${numberFormat.format(amount)}"; - } else if (changeValue < 0) { - return "-${numberFormat.format(amount)}"; - } else { - return "+${numberFormat.format(amount)}"; - } + final sign = changeValue < 0 ? "-" : "+"; + return "$sign${numberFormat.format(amount)}"; } Color getChangeColor() { @@ -42,29 +37,25 @@ class RevenueCard extends StatelessWidget { @override Widget build(BuildContext context) { - final String formattedAmount = getFormattedAmount(); - final Color changeColor = getChangeColor(); + final formattedAmount = getFormattedAmount(); + final changeColor = getChangeColor(); return SizedBox( width: 312.w, height: 48.h, child: Row( crossAxisAlignment: CrossAxisAlignment.center, + spacing: 12.w, // 이미지와 다음 요소 사이 간격 children: [ - Padding( - padding: EdgeInsets.only(right: 12.w), - child: Image.network( - imagePath, - width: 40.w, - height: 40.h, - fit: BoxFit.cover, - ), + Image.network( + imagePath, + width: 40.w, + height: 40.h, + fit: BoxFit.cover, ), - Padding( - padding: EdgeInsets.only(right: 8.w), // 수평 간격 조정 - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( companyName, @@ -72,48 +63,36 @@ class RevenueCard extends StatelessWidget { fontSize: 16.sp, fontWeight: FontWeight.w400, height: 22 / 16, - letterSpacing: 0, color: JusicoolColor.black, ), ), - ], - ), - ), - Expanded(child: Container()), // 남은 공간 채우기 - SizedBox( - width: 160.w, - height: 44.h, - child: Padding( - padding: const EdgeInsets.only(top: 0, bottom: 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - formattedAmount, - style: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - height: 22 / 16, - letterSpacing: 0, - color: changeColor, + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 2.h, // 수익 금액과 퍼센트 사이 간격 + children: [ + Text( + formattedAmount, + style: JusicoolTypography.bodySmall.copyWith( + fontSize: 16.sp, + fontWeight: FontWeight.w400, + height: 22 / 16, + color: changeColor, + ), ), - ), - Padding( - padding: EdgeInsets.only(top: 4.h), // 수직 간격을 Padding으로 처리 - child: Text( + Text( "(${changePercentage.toStringAsFixed(1)}%)", style: TextStyle( fontSize: 12.sp, fontWeight: FontWeight.w400, height: 16 / 12, - letterSpacing: 0, color: changeColor, ), ), - ), - ], - ), + ], + ), + ], ), ), ], diff --git a/lib/presentation/my_capital/widgets/stock_cards.dart b/lib/presentation/my_capital/widgets/stock_cards.dart new file mode 100644 index 0000000..6bcbd89 --- /dev/null +++ b/lib/presentation/my_capital/widgets/stock_cards.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:jusicool_design_system/jusicool_design_system.dart'; + +class StockCards extends StatelessWidget { + final String companyName; + final String logoUrl; + final String price; + final String priceChange; + final String share; + final bool isPositive; + + const StockCards({ + super.key, + required this.companyName, + required this.logoUrl, + required this.price, + required this.priceChange, + required this.share, + this.isPositive = true, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: JusicoolColor.white, + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 14.w, + children: [ + SizedBox( + width: 40.w, + height: 40.h, + child: ClipRRect( + borderRadius: BorderRadius.circular(20.r), + child: Image.network(logoUrl, fit: BoxFit.cover), + ), + ), + Column( + spacing: 2.h, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(companyName, style: JusicoolTypography.bodyMedium), + Text( + share, + style: JusicoolTypography.label.copyWith( + color: JusicoolColor.gray500, + ), + ), + ], + ), + ], + ), + Column( + spacing: 2.h, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(price, style: JusicoolTypography.bodyMedium), + Text( + priceChange, + style: JusicoolTypography.label.copyWith( + color: isPositive ? JusicoolColor.main : JusicoolColor.error, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/news/screens/news_item.dart b/lib/presentation/news/screens/news_item.dart new file mode 100644 index 0000000..0d27b0e --- /dev/null +++ b/lib/presentation/news/screens/news_item.dart @@ -0,0 +1,22 @@ +class NewsItem { + final String title; + final String subtitle; + final String imageUrl; + final String linkUrl; + + const NewsItem({ + required this.title, + required this.subtitle, + required this.imageUrl, + required this.linkUrl, + }); + + factory NewsItem.fromJson(Map json) { + return NewsItem( + title: json['title'] as String, + subtitle: json['subtitle'] as String, + imageUrl: json['imageUrl'] as String, + linkUrl: json['linkUrl'] as String, + ); + } +} diff --git a/lib/presentation/news/screens/news_list_screen.dart b/lib/presentation/news/screens/news_list_screen.dart index 2367590..9dd2ce1 100644 --- a/lib/presentation/news/screens/news_list_screen.dart +++ b/lib/presentation/news/screens/news_list_screen.dart @@ -2,33 +2,9 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:jusicool_design_system/jusicool_design_system.dart'; +import 'package:jusicool_ios/presentation/news/screens/news_item.dart'; import 'package:url_launcher/url_launcher.dart'; -/// 뉴스 아이템 데이터 모델 -class NewsItem { - final String title; - final String subtitle; - final String imageUrl; - final String linkUrl; - - const NewsItem({ - required this.title, - required this.subtitle, - required this.imageUrl, - required this.linkUrl, - }); - - factory NewsItem.fromJson(Map json) { - return NewsItem( - title: json['title'] as String, - subtitle: json['subtitle'] as String, - imageUrl: json['imageUrl'] as String, - linkUrl: json['linkUrl'] as String, - ); - } -} - -/// 뉴스 리스트 화면 class NewsListScreen extends StatefulWidget { const NewsListScreen({super.key}); @@ -37,130 +13,78 @@ class NewsListScreen extends StatefulWidget { } class _NewsListScreenState extends State { - List _newsItems = []; final ScrollController _scrollController = ScrollController(); - double _scrollOffset = 0.0; + List newsItems = []; @override void initState() { super.initState(); - _scrollController.addListener(_handleScroll); - - // 안전하게 JSON 로드 - WidgetsBinding.instance.addPostFrameCallback((_) { - _loadNewsItems(); - }); + _loadNewsItems(); } Future _loadNewsItems() async { try { - final String jsonString = await DefaultAssetBundle.of( + final jsonString = await DefaultAssetBundle.of( context, ).loadString('assets/data/news.json'); final List jsonData = json.decode(jsonString); + final items = jsonData.map((e) => NewsItem.fromJson(e)).toList(); - setState(() { - _newsItems = jsonData.map((item) => NewsItem.fromJson(item)).toList(); - }); + setState(() => newsItems = items); } catch (e) { debugPrint('뉴스 로드 실패: $e'); - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('뉴스 데이터를 불러올 수 없습니다.'))); - } + _showErrorSnackBar('뉴스 데이터를 불러올 수 없습니다.'); } } - void _handleScroll() { - setState(() { - _scrollOffset = _scrollController.offset; - }); - } - - Color _getJusicoolBarColor() { - return _scrollOffset < 20 ? Colors.transparent : JusicoolColor.white; + void _showErrorSnackBar(String message) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); } Future _launchUrl(String url) async { - final Uri uri = Uri.parse(url); + final uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { - if(mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('링크를 열 수 없습니다.'))); - } + _showErrorSnackBar('링크를 열 수 없습니다.'); } } @override void dispose() { - _scrollController - ..removeListener(_handleScroll) - ..dispose(); + _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final double statusBarHeight = MediaQuery.of(context).padding.top; - final double jusicoolBarHeight = kToolbarHeight + statusBarHeight; - return Scaffold( backgroundColor: JusicoolColor.white, - body: Stack( - children: [ - // 뉴스 리스트 - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w), - child: - _newsItems.isEmpty - ? const Center(child: CircularProgressIndicator()) - : ListView.separated( - controller: _scrollController, - padding: EdgeInsets.only(top: jusicoolBarHeight + 24.h), - itemCount: _newsItems.length, - separatorBuilder: (_, _) => SizedBox(height: 20.h), - itemBuilder: (context, index) { - final item = _newsItems[index]; - return GestureDetector( - onTap: () => _launchUrl(item.linkUrl), - child: NewsCard( - key: ValueKey('${item.title}_$index'), - title: item.title, - subtitle: item.subtitle, - imageUrl: item.imageUrl, - ), - ); - }, - ), - ), - // 커스텀 JusicoolBar - Container( - height: jusicoolBarHeight, - padding: EdgeInsets.only(top: statusBarHeight), - color: _getJusicoolBarColor(), - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () => Navigator.of(context).pop(), - ), - const Spacer(), - Text( - "뉴스", - style: JusicoolTypography.subTitle.copyWith( - color: Colors.black, - ), + body: Padding( + padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 56.h), + child: + newsItems.isEmpty + ? const Center(child: CircularProgressIndicator()) + : ListView.separated( + controller: _scrollController, + itemCount: newsItems.length, + separatorBuilder: (_, _) => SizedBox(height: 20.h), + itemBuilder: (context, index) { + final item = newsItems[index]; + return GestureDetector( + onTap: () => _launchUrl(item.linkUrl), + child: NewsCard( + key: ValueKey('${item.title}_$index'), + title: item.title, + subtitle: item.subtitle, + imageUrl: item.imageUrl, + ), + ); + }, ), - const Spacer(), - SizedBox(width: 48.w), // 아이콘 영역 여백 - ], - ), - ), - ], ), ); } diff --git a/lib/presentation/sign_in/screens/login_screen.dart b/lib/presentation/sign_in/screens/login_screen.dart index 17752b8..e0a2cd0 100644 --- a/lib/presentation/sign_in/screens/login_screen.dart +++ b/lib/presentation/sign_in/screens/login_screen.dart @@ -65,7 +65,7 @@ class LoginScreen extends ConsumerWidget { onPressed: () { final result = provider.signIn(); if (result) { - context.pushReplacement(RoutePaths.main); + context.pushReplacement('/main-capital'); } }, backgroundColor: diff --git a/lib/presentation/sign_up/screens/email_auth_screen.dart b/lib/presentation/sign_up/screens/email_auth_screen.dart index 4508229..ca94392 100644 --- a/lib/presentation/sign_up/screens/email_auth_screen.dart +++ b/lib/presentation/sign_up/screens/email_auth_screen.dart @@ -37,7 +37,6 @@ class EmailAuthScreen extends ConsumerWidget { decoration: InputDecoration( hintText: hintText, hintStyle: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, color: JusicoolColor.gray300, ), contentPadding: EdgeInsets.all(16.w), @@ -114,9 +113,13 @@ class EmailAuthScreen extends ConsumerWidget { return Scaffold( appBar: AppBar( - leading: Padding( - padding: EdgeInsets.only(left: 15.w, top: 20.h), - child: const BackButton(), + leading: IconButton( + iconSize: 24.sp, + padding: EdgeInsets.only(left: 24.sp, top: 20.h), + icon: const Icon(Icons.arrow_back, color: JusicoolColor.black), + onPressed: () { + Navigator.of(context).pop(); + }, ), backgroundColor: JusicoolColor.white, elevation: 0, @@ -237,8 +240,7 @@ class EmailAuthScreen extends ConsumerWidget { state.enableButton ? state.codeSent ? () { - bool result = controller.sendVerificationCode( - ); + bool result = controller.sendVerificationCode(); if (result) { context.push(RoutePaths.passwordCreate); } diff --git a/lib/presentation/sign_up/screens/find_school_screen.dart b/lib/presentation/sign_up/screens/find_school_screen.dart index c817cb4..09c989e 100644 --- a/lib/presentation/sign_up/screens/find_school_screen.dart +++ b/lib/presentation/sign_up/screens/find_school_screen.dart @@ -187,9 +187,13 @@ class FindSchoolScreen extends ConsumerWidget { return Scaffold( backgroundColor: JusicoolColor.white, appBar: AppBar( - leading: Padding( - padding: EdgeInsets.only(left: 15.w, top: 20.h), - child: const BackButton(), + leading: IconButton( + iconSize: 24.sp, + padding: EdgeInsets.only(left: 24.sp, top: 20.h), + icon: const Icon(Icons.arrow_back, color: JusicoolColor.black), + onPressed: () { + Navigator.of(context).pop(); + }, ), backgroundColor: JusicoolColor.white, elevation: 0, diff --git a/lib/presentation/sign_up/screens/name_input_screen.dart b/lib/presentation/sign_up/screens/name_input_screen.dart index 4cea720..7ca9579 100644 --- a/lib/presentation/sign_up/screens/name_input_screen.dart +++ b/lib/presentation/sign_up/screens/name_input_screen.dart @@ -64,9 +64,13 @@ class NameInputScreen extends ConsumerWidget { return Scaffold( backgroundColor: JusicoolColor.white, appBar: AppBar( - leading: Padding( - padding: EdgeInsets.only(left: 15.w, top: 20.h), - child: const BackButton(), + leading: IconButton( + iconSize: 24.sp, + padding: EdgeInsets.only(left: 24.sp, top: 20.h), + icon: const Icon(Icons.arrow_back, color: JusicoolColor.black), + onPressed: () { + Navigator.of(context).pop(); + }, ), elevation: 0, backgroundColor: JusicoolColor.white, diff --git a/lib/presentation/sign_up/screens/password_create_screen.dart b/lib/presentation/sign_up/screens/password_create_screen.dart index a2fbec9..060d762 100644 --- a/lib/presentation/sign_up/screens/password_create_screen.dart +++ b/lib/presentation/sign_up/screens/password_create_screen.dart @@ -25,9 +25,13 @@ class PasswordCreateScreen extends ConsumerWidget { return Scaffold( appBar: AppBar( - leading: Padding( - padding: EdgeInsets.only(left: 15.w, top: 20.h), - child: const BackButton(), + leading: IconButton( + iconSize: 24.sp, + padding: EdgeInsets.only(left: 24.sp, top: 20.h), + icon: const Icon(Icons.arrow_back, color: JusicoolColor.black), + onPressed: () { + Navigator.of(context).pop(); + }, ), backgroundColor: JusicoolColor.white, elevation: 0,