From ae99bd661d4072a07f5f4f327b06f4c70457ab88 Mon Sep 17 00:00:00 2001 From: girinb Date: Thu, 10 Jul 2025 18:31:19 +0900 Subject: [PATCH] =?UTF-8?q?=ED=95=9C=EB=B2=88=20=EC=A2=86=EB=90=98?= =?UTF-8?q?=EC=84=9C=20=EB=8B=A4=EC=8B=9C=20=EB=8F=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/custom_bottom_nav_bar.dart | 56 ++++ lib/home_page.dart | 26 +- lib/jobs_page.dart | 32 +++ lib/main.dart | 56 ++-- lib/plan_page.dart | 33 ++- lib/plan_page_detail.dart | 107 +++++--- lib/youtube_player_page.dart | 255 +++++++----------- 7 files changed, 291 insertions(+), 274 deletions(-) create mode 100644 lib/common/widgets/custom_bottom_nav_bar.dart create mode 100644 lib/jobs_page.dart diff --git a/lib/common/widgets/custom_bottom_nav_bar.dart b/lib/common/widgets/custom_bottom_nav_bar.dart new file mode 100644 index 0000000..af7909e --- /dev/null +++ b/lib/common/widgets/custom_bottom_nav_bar.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +class CustomBottomNavBar extends StatelessWidget { + final int currentIndex; + final Function(int) onTap; + + const CustomBottomNavBar({ + super.key, + required this.currentIndex, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + const double customBottomNavHeight = 94.0; + const double customBottomNavIconSize = 22.0; + const double customBottomNavSelectedFontSize = 10.0; + const double customBottomNavUnselectedFontSize = 8.0; + + return SizedBox( + height: customBottomNavHeight, + child: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.home_filled), + label: 'Home', + ), + BottomNavigationBarItem( + icon: Icon(Icons.calendar_today_outlined), + label: 'Plan', + ), + BottomNavigationBarItem( + icon: Icon(Icons.bar_chart_outlined), + label: 'Statistics', + ), + BottomNavigationBarItem( + icon: Icon(Icons.work_outline), + label: 'Jobs', + ), + BottomNavigationBarItem( + icon: Icon(Icons.more_horiz_outlined), + label: 'More', + ), + ], + currentIndex: currentIndex, + + unselectedItemColor: Colors.blue, + onTap: onTap, + type: BottomNavigationBarType.fixed, + iconSize: customBottomNavIconSize, + selectedFontSize: customBottomNavSelectedFontSize, + unselectedFontSize: customBottomNavUnselectedFontSize, + ), + ); + } +} diff --git a/lib/home_page.dart b/lib/home_page.dart index e86736f..910d67a 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -311,16 +311,16 @@ class _HomePageState extends State { const SizedBox(height: 12.0), - Text( - currentPlan.planName, - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - // Test: Plan ID 표시 (디버깅용) - // Text('ID: ${currentPlan.planId}', style: TextStyle(fontSize: 10, color: Colors.grey)), + Text( + currentPlan.planName, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + // Test: Plan ID 표시 (디버깅용) + // Text('ID: ${currentPlan.planId}', style: TextStyle(fontSize: 10, color: Colors.grey)), - AspectRatio( + AspectRatio( aspectRatio: 16 / 9, child: ClipRRect( borderRadius: BorderRadius.circular(8.0), @@ -534,10 +534,10 @@ class _HomePageState extends State { // appBar: AppBar( // AppBar 주석 처리됨 (기존 코드에서) // title: Text(_selectedIndex == 0 ? 'Home' : 'Plan Page'), // ), - body: IndexedStack( // AppBar가 없으므로 SafeArea로 감싸는 것을 고려해볼 수 있습니다. - index: _selectedIndex, - children: pageContents, - ) + body: IndexedStack( // AppBar가 없으므로 SafeArea로 감싸는 것을 고려해볼 수 있습니다. + index: _selectedIndex, + children: pageContents, + ) ); } } \ No newline at end of file diff --git a/lib/jobs_page.dart b/lib/jobs_page.dart new file mode 100644 index 0000000..65837e1 --- /dev/null +++ b/lib/jobs_page.dart @@ -0,0 +1,32 @@ + +import 'package:flutter/material.dart'; +import 'package:csp2/common/widgets/custom_bottom_nav_bar.dart'; + +class JobsPage extends StatefulWidget { + const JobsPage({super.key}); + + @override + State createState() => _JobsPageState(); +} + +class _JobsPageState extends State { + int _selectedIndex = 3; // Assuming Jobs is the 4th item (index 3) + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + // TODO: Add navigation logic here based on index + // For now, we'll just print the index + print('Tapped index: $index'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: const Center( + child: Text('Jobs Page'), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index c110b7c..3be5741 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,9 @@ import 'package:flutter/material.dart'; import 'home_page.dart'; // HomePage에 콜백을 전달해야 하므로 import 경로 확인 import 'plan_page.dart'; import 'statistics_page.dart'; +import 'jobs_page.dart'; import 'more_page.dart'; +import 'common/widgets/custom_bottom_nav_bar.dart'; void main() { runApp(const MyApp()); @@ -51,6 +53,7 @@ class _MyHomePageState extends State { HomePage(onNavigateToPlanTab: _onItemTapped), // 콜백 함수 전달 const PlanPage(), const StatisticsPage(), + const JobsPage(), const MorePage(), ]; } @@ -59,6 +62,7 @@ class _MyHomePageState extends State { setState(() { _selectedIndex = index; }); + print('Selected index: $_selectedIndex'); // 디버깅 출력 추가 // HomePage의 _recommendTimer 제어 로직은 HomePage 내부에서 독립적으로 관리됩니다. // 또는 필요에 따라 GlobalKey 등을 사용하여 HomePage의 상태에 접근할 수 있습니다. } @@ -78,23 +82,22 @@ class _MyHomePageState extends State { String appBarTitle = 'Home'; // 기본값 if (_selectedIndex == 1) { appBarTitle = 'Plan'; - } else if (_selectedIndex == 2) { + } + else if (_selectedIndex == 2) { appBarTitle = 'Statistics'; - } else if (_selectedIndex == 3) { + } + else if (_selectedIndex == 3) { + appBarTitle = 'Jobs'; + } + else if (_selectedIndex == 4) { appBarTitle = 'More'; } - // --- BottomNavigationBar 크기 및 스타일 설정 --- - const double customBottomNavHeight = 75.0; // 원하는 BottomNavigationBar 높이 - const double customBottomNavIconSize = 22.0; // 내부 아이콘 크기 (선택적 조절) - const double customBottomNavSelectedFontSize = 12.0; // 선택된 레이블 폰트 크기 (선택적 조절) - const double customBottomNavUnselectedFontSize = 10.0; // 미선택 레이블 폰트 크기 (선택적 조절) - return Scaffold( appBar: AppBar( toolbarHeight: 40, backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(appBarTitle), // 동적으로 변경된 AppBar 제목 + title: Text(appBarTitle), // 동적으로 변경된 AppBar 제목\ actions: [ IconButton( icon: const Icon(Icons.notifications), @@ -125,37 +128,10 @@ class _MyHomePageState extends State { index: _selectedIndex, children: _widgetOptions, ), - bottomNavigationBar: SizedBox( - height: customBottomNavHeight, - child: BottomNavigationBar( - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.home_filled), - label: 'Home', - ), - BottomNavigationBarItem( - icon: Icon(Icons.calendar_today_outlined), - label: 'Plan', - ), - BottomNavigationBarItem( - icon: Icon(Icons.bar_chart_outlined), - label: 'Statistics', - ), - BottomNavigationBarItem( - icon: Icon(Icons.more_horiz_outlined), - label: 'More', - ), - ], - currentIndex: _selectedIndex, - selectedItemColor: Colors.amber[800], - unselectedItemColor: Colors.blue, - onTap: _onItemTapped, - type: BottomNavigationBarType.fixed, - iconSize: customBottomNavIconSize, - selectedFontSize: customBottomNavSelectedFontSize, - unselectedFontSize: customBottomNavUnselectedFontSize, - ), + bottomNavigationBar: CustomBottomNavBar( + currentIndex: _selectedIndex, + onTap: _onItemTapped, ), ); } -} \ No newline at end of file +} diff --git a/lib/plan_page.dart b/lib/plan_page.dart index 8f6f29c..3414cd8 100644 --- a/lib/plan_page.dart +++ b/lib/plan_page.dart @@ -80,8 +80,10 @@ class _PlanPageState extends State { return Center( child: Padding( padding: const EdgeInsets.all(16.0), - child: Text('Error loading PlanPage data: ${snapshot.error}', textAlign: TextAlign.center), - )); + child: Text('Error loading PlanPage data: ${snapshot.error}', textAlign: TextAlign.center, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ))); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return const Center(child: Text('No data available for PlanPage.')); } else { @@ -91,9 +93,8 @@ class _PlanPageState extends State { itemCount: plans.length, itemBuilder: (context, index) { final plan = plans[index]; - return InkWell( // <<< Card를 InkWell로 감싸서 탭 이벤트를 추가합니다. + return InkWell( onTap: () { - // plan_page_detail로 이동하면서 planId를 arguments로 전달 if (plan.planId == 'ID 없음' || plan.planId.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('유효한 Plan ID가 없어 상세 페이지로 이동할 수 없습니다.')), @@ -103,20 +104,18 @@ class _PlanPageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => const plan_page_detail(), // plan_page_detail 위젯을 생성 + builder: (context) => const PlanPageDetail(), settings: RouteSettings( - arguments: plan.planId, // 선택된 plan의 ID를 전달 + // <<< Map 형태로 planId와 planTitle을 전달 >>> + arguments: { + 'planId': plan.planId, + 'planTitle': plan.planTitle, + }, ), ), ); - // 만약 Named Route를 사용하고 main.dart에 '/plan_detail' 라우트가 정의되어 있다면: - // Navigator.pushNamed( - // context, - // '/plan_detail', // MaterialApp에 정의된 라우트 이름 - // arguments: plan.planId, - // ); }, - child: Card( // <<< 기존 Card 위젯 + child: Card( margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), elevation: 2.0, @@ -134,6 +133,7 @@ class _PlanPageState extends State { style: const TextStyle( fontSize: 17.0, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, + maxLines: 2, ), ), const SizedBox(width: 8.0), @@ -141,6 +141,8 @@ class _PlanPageState extends State { plan.planTeacher, style: const TextStyle( fontSize: 13.0, color: Colors.grey), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), @@ -150,7 +152,7 @@ class _PlanPageState extends State { borderRadius: BorderRadius.circular(8.0), child: Image.network( plan.thumbnail, - height: 140, // 약간 작은 이미지 크기 + height: 140, width: double.infinity, fit: BoxFit.cover, loadingBuilder: (BuildContext context, Widget child, @@ -184,9 +186,6 @@ class _PlanPageState extends State { child: const Center( child: Text('No Image', style: TextStyle(color: Colors.grey)))), - // const SizedBox(height: 8.0), - // // PlanPage용 ListView 아이템에 추가적인 정보를 표시하거나 다른 UI를 구성할 수 있습니다. - // Text('Plan ID: ${plan.planId}', style: TextStyle(fontSize: 12.0, color: Colors.blueGrey)), ], ), ), diff --git a/lib/plan_page_detail.dart b/lib/plan_page_detail.dart index 386c4d8..7247597 100644 --- a/lib/plan_page_detail.dart +++ b/lib/plan_page_detail.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'youtube_player_page.dart'; // YoutubePlayerPage import +import 'common/widgets/custom_bottom_nav_bar.dart'; -// PlanDetailItem 클래스 +// PlanDetailItem 클래스 (이전과 동일) class PlanDetailItem { final String lessonId; final String lessonTag; @@ -27,62 +28,82 @@ class PlanDetailItem { } } -class plan_page_detail extends StatefulWidget { - // final int currentBottomNavIndex; // HomePage 등에서 전달받을 현재 탭 인덱스 - - const plan_page_detail({ +class PlanPageDetail extends StatefulWidget { + const PlanPageDetail({ super.key, - // this.currentBottomNavIndex = 0, // 기본값 }); @override - State createState() => _plan_page_detailState(); + State createState() => _PlanPageDetailState(); } -class _plan_page_detailState extends State { +class _PlanPageDetailState extends State { String? _planId; + String? _planTitle; // <<< planTitle을 저장할 상태 변수 추가 >>> Future>? _planDetails; - late int _currentBottomNavIndex; // 하단 네비게이션 바 상태 + late int _currentBottomNavIndex; + String? _selectedYoutubeUrl; @override void initState() { super.initState(); - // 이전 페이지에서 전달받은 탭 인덱스로 초기화하거나 기본값 사용 - // _currentBottomNavIndex = widget.currentBottomNavIndex; - _currentBottomNavIndex = 0; // 예시: '홈' 탭을 기본으로 설정 + _currentBottomNavIndex = 0; } @override void didChangeDependencies() { super.didChangeDependencies(); - if (_planId == null) { + // 인자를 한 번만 처리하도록 조건 추가 + if (_planId == null && _planTitle == null) { final Object? arguments = ModalRoute.of(context)?.settings.arguments; - if (arguments is String) { - _planId = arguments; + if (arguments is Map) { // <<< 전달받은 인자가 Map인지 확인 >>> + setState(() { // <<< setState로 상태 변수 업데이트 >>> + _planId = arguments['planId']; + _planTitle = arguments['planTitle']; + }); + if (_planId != null) { _planDetails = _fetchPlanDetails(_planId!); + } else { + // Map에는 있지만 planId 키가 없는 경우 (이론상 발생하기 어려움) + setState(() { + _planTitle = arguments['planTitle'] ?? 'Error'; // planTitle은 있을 수 있음 + }); + _planDetails = + Future.error(Exception("Plan ID가 Map에 포함되지 않았습니다.")); + } } else { - _planDetails = Future.error(Exception("Plan ID not provided or invalid.")); - print("Error: Plan ID not provided or invalid."); + // 인자가 Map이 아니거나 null인 경우 + setState(() { + _planTitle = 'Error'; // AppBar에 오류 표시 + }); + _planDetails = + Future.error(Exception("전달된 인자가 올바르지 않습니다. (Map 형태여야 함)")); + } } } Future> _fetchPlanDetails(String planId) async { - final response = await http - .get(Uri.parse('https://helloworld1-ad2uqhckxq-uc.a.run.app/?id=$planId')); + final response = await http.get( + Uri.parse('https://helloworld1-ad2uqhckxq-uc.a.run.app/?id=$planId')); if (response.statusCode == 200) { final Map decodedJson = json.decode(response.body); if (decodedJson.containsKey('data') && decodedJson['data'] is List) { final List detailsJson = decodedJson['data']; - return detailsJson.map((jsonItem) => PlanDetailItem.fromJson(jsonItem as Map)).toList(); + return detailsJson + .map((jsonItem) => + PlanDetailItem.fromJson(jsonItem as Map)) + .toList(); } else { - throw Exception('Invalid API response format: "data" field is missing or not a list.'); + throw Exception( + 'Invalid API response format: "data" field is missing or not a list.'); } } else { - throw Exception('Failed to load plan details. Status code: ${response.statusCode}'); + throw Exception( + 'Failed to load plan details. Status code: ${response.statusCode}'); } } @@ -90,16 +111,26 @@ class _plan_page_detailState extends State { setState(() { _currentBottomNavIndex = index; }); - // 페이지 이동 로직 (이전 답변 참고) if (index == 0) { Navigator.of(context).popUntil((route) => route.isFirst); - } else if (index == 1) { + } else { + String tabName = ''; + switch (index) { + case 1: + tabName = 'Plan'; + break; + case 2: + tabName = 'Statistics'; + break; + case 3: + tabName = 'Jobs'; // New case for Jobs + break; + case 4: + tabName = 'More'; // Updated case for More + break; + } ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Search (Not Implemented)')), - ); - } else if (index == 2) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Profile (Not Implemented)')), + SnackBar(content: Text('$tabName 탭으로 이동 (구현 필요)')), ); } } @@ -107,17 +138,9 @@ class _plan_page_detailState extends State { @override Widget build(BuildContext context) { return Scaffold( - // 1. 상단 바 (AppBar) appBar: AppBar( - title: Text(_planId != null ? 'Plan: $_planId' : 'Plan Details'), - // 필요하다면 leading에 뒤로가기 버튼 명시적 추가 - leading: Navigator.canPop(context) - ? IconButton( - icon: const Icon(Icons.arrow_back_ios_new), // 또는 Icons.arrow_back - tooltip: 'Back', - onPressed: () => Navigator.of(context).pop(), - ) - : null, + toolbarHeight: 40, + title: Text(_planTitle.toString()), ), body: _planId == null ? const Center( @@ -212,8 +235,10 @@ class _plan_page_detailState extends State { } }, ), - // 2. 하단 바 (BottomNavigationBar) - + bottomNavigationBar: CustomBottomNavBar( + currentIndex: _currentBottomNavIndex, + onTap: _onBottomNavItemTapped, + ), ); } } \ No newline at end of file diff --git a/lib/youtube_player_page.dart b/lib/youtube_player_page.dart index c566b9d..44c7bcf 100644 --- a/lib/youtube_player_page.dart +++ b/lib/youtube_player_page.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // SystemChrome, DeviceOrientation 사용을 위해 import import 'package:youtube_player_flutter/youtube_player_flutter.dart'; +import 'common/widgets/custom_bottom_nav_bar.dart'; class YoutubePlayerPage extends StatefulWidget { final String lessonUrl; - // final int currentBottomNavIndex; + final void Function(bool isFullScreen)? onFullScreenToggle; const YoutubePlayerPage({ super.key, required this.lessonUrl, - // this.currentBottomNavIndex = 0, + this.onFullScreenToggle, }); @override @@ -23,16 +24,12 @@ class _YoutubePlayerPageState extends State { String _videoTitle = 'YouTube Video'; final int _currentBottomNavIndex = 0; bool _isSystemUiVisible = true; + bool _isLoading = true; // 로딩 상태 추가 + bool _isFullScreen = false; @override void initState() { super.initState(); - // 페이지 진입 시 기본 화면 방향 설정 (선택적) - // SystemChrome.setPreferredOrientations([ - // DeviceOrientation.portraitUp, - // DeviceOrientation.portraitDown, - // ]); - _videoId = YoutubePlayer.convertUrlToId(widget.lessonUrl); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -57,14 +54,11 @@ class _YoutubePlayerPageState extends State { _controller = YoutubePlayerController( initialVideoId: _videoId!, flags: const YoutubePlayerFlags( - autoPlay: true, + autoPlay: false, mute: false, - // <<< 동영상 재생이 끝나면 컨트롤러를 자동으로 숨기지 않도록 설정 (선택적) >>> - // hideControls: false, // 기본값은 true ), )..addListener(_playerListener); } else { - print("Error: Could not extract video ID from URL: ${widget.lessonUrl}"); _videoTitle = 'Video Error'; } } @@ -72,48 +66,30 @@ class _YoutubePlayerPageState extends State { void _playerListener() { if (_controller == null || !mounted) return; - // <<< 재생 상태 감지 >>> - if (_controller!.value.playerState == PlayerState.ended) { - // 동영상 재생이 완료되었을 때 처리할 로직 - print("Video has ended."); - if (mounted) { - // 전체 화면 모드였다면 해제 - if (_controller!.value.isFullScreen) { - _controller!.toggleFullScreenMode(); - } - // 시스템 UI를 다시 보이도록 설정 (toggleFullScreenMode가 자동으로 처리할 수도 있음) + if (_isFullScreen != _controller!.value.isFullScreen) { + setState(() { + _isFullScreen = _controller!.value.isFullScreen; + }); + widget.onFullScreenToggle?.call(_isFullScreen); + + if (_isFullScreen) { + _hideSystemUi(); + } else { _showSystemUi(); - // 세로 화면으로 복귀 (toggleFullScreenMode가 자동으로 처리할 수도 있음) SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, ]); - - // 필요하다면 페이지를 pop 하거나 다른 동작 수행 - // 예: Navigator.of(context).pop(); - // 또는 사용자에게 알림 표시 - // ScaffoldMessenger.of(context).showSnackBar( - // const SnackBar(content: Text('동영상 재생이 완료되었습니다.')), - // ); } - return; // ended 상태 처리 후 리스너의 나머지 로직은 건너뛸 수 있음 } - - if (_controller!.value.isFullScreen) { - _hideSystemUi(); - // 플레이어가 전체 화면으로 진입하면 가로 방향으로 설정 (선택적, 플레이어가 자동으로 할 수 있음) - // SystemChrome.setPreferredOrientations([ - // DeviceOrientation.landscapeLeft, - // DeviceOrientation.landscapeRight, - // ]); - } else { - _showSystemUi(); - // <<< 전체 화면이 해제되면 화면 방향을 세로로 복구 >>> - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); + if (_controller!.value.playerState == PlayerState.ended) { + if (mounted) { + if (_controller!.value.isFullScreen) { + _controller!.toggleFullScreenMode(); + } + } + return; } if (_isPlayerReady) { @@ -174,7 +150,10 @@ class _YoutubePlayerPageState extends State { tabName = 'Statistics'; break; case 3: - tabName = 'More'; + tabName = 'Jobs'; // New case for Jobs + break; + case 4: + tabName = 'More'; // Updated case for More break; } ScaffoldMessenger.of(context).showSnackBar( @@ -183,6 +162,23 @@ class _YoutubePlayerPageState extends State { } } + @override + void didUpdateWidget(covariant YoutubePlayerPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.lessonUrl != oldWidget.lessonUrl) { + _videoId = YoutubePlayer.convertUrlToId(widget.lessonUrl); + if (_videoId != null) { + _controller?.load(_videoId!); // 새 비디오 로드 + } else { + if (mounted) { + setState(() { + _videoTitle = 'Video Error'; + }); + } + } + } + } + // <<< 뒤로가기 버튼 처리 로직 >>> Future _onWillPop() async { if (_controller != null && _controller!.value.isFullScreen) { @@ -204,126 +200,59 @@ class _YoutubePlayerPageState extends State { onWillPop: _onWillPop, child: Scaffold( extendBodyBehindAppBar: isFullScreen, - appBar: isFullScreen - ? null - : AppBar( - leading: Navigator.canPop(context) - ? IconButton( - icon: const Icon(Icons.arrow_back_ios_new), - // <<< AppBar의 뒤로가기 버튼도 _onWillPop 로직을 따르도록 수정 >>> - onPressed: () async { - if (await _onWillPop()) { - // true를 반환하면 (즉, 전체화면이 아니면) 페이지 pop - Navigator.pop(context); - } - }, - ) - : null, - title: Text(_videoTitle), - actions: [ - IconButton( - icon: const Icon(Icons.notifications_none), - tooltip: '알림', - onPressed: _onNotificationTapped, - ), - Padding( - padding: const EdgeInsets.only(right: 16.0, left: 8.0), - child: InkWell( - onTap: _onProfileTapped, - customBorder: const CircleBorder(), - child: const CircleAvatar( - radius: 16, - backgroundColor: Colors.grey, - child: Icon( - Icons.person, - size: 20, - color: Colors.white, - ), - ), + + body: Column( // <--- Wrap the body content in a Column + children: [ + Expanded( // <--- Wrap the main content with Expanded + child: Center( + child: _controller == null + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, + color: Colors.red, size: 50), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Text( + '비디오를 로드할 수 없습니다.\nURL: ${widget.lessonUrl}', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ) + : YoutubePlayer( + controller: _controller!, + showVideoProgressIndicator: true, + progressIndicatorColor: Colors.amber, + progressColors: const ProgressBarColors( + playedColor: Colors.amber, + handleColor: Colors.amberAccent, + ), + onReady: () { + if (mounted) { + setState(() { + _isPlayerReady = true; + if (_controller!.metadata.title.isNotEmpty) { + _videoTitle = _controller!.metadata.title; + } + }); + } + + }, + ), ), ), ], ), - body: Center( - child: _controller == null - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error_outline, - color: Colors.red, size: 50), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Text( - '비디오를 로드할 수 없습니다.\nURL: ${widget.lessonUrl}', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16), - ), - ), - ], - ) - : YoutubePlayer( - controller: _controller!, - showVideoProgressIndicator: true, - progressIndicatorColor: Colors.amber, - progressColors: const ProgressBarColors( - playedColor: Colors.amber, - handleColor: Colors.amberAccent, - ), - onReady: () { - if (mounted) { - setState(() { - _isPlayerReady = true; - if (_controller!.metadata.title.isNotEmpty) { - _videoTitle = _controller!.metadata.title; - } - }); - } - print('Player is ready.'); - }, - // <<< 필요하다면 onEnded 콜백 직접 사용 가능 (addListener와 중복될 수 있으니 주의) >>> - // onEnded: (metadata) { - // print("Video has ended (onEnded callback)."); - // if (mounted) { - // if (_controller!.value.isFullScreen) { - // _controller!.toggleFullScreenMode(); - // } - // _showSystemUi(); - // SystemChrome.setPreferredOrientations([ - // DeviceOrientation.portraitUp, - // DeviceOrientation.portraitDown, - // ]); - // } - // }, - ), - ), - bottomNavigationBar: isFullScreen - ? null - : BottomNavigationBar( - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.home_filled), - label: 'Home', - ), - BottomNavigationBarItem( - icon: Icon(Icons.calendar_today_outlined), - label: 'Plan', - ), - BottomNavigationBarItem( - icon: Icon(Icons.bar_chart_outlined), - label: 'Statistics', - ), - BottomNavigationBarItem( - icon: Icon(Icons.more_horiz_outlined), - label: 'More', - ), - ], - currentIndex: _currentBottomNavIndex, - selectedItemColor: Colors.amber[800], - unselectedItemColor: Colors.blue, - onTap: _onBottomNavItemTapped, - type: BottomNavigationBarType.fixed, - ), + // bottomNavigationBar: isFullScreen + // ? null + // : CustomBottomNavBar( + // currentIndex: _currentBottomNavIndex, + // onTap: _onBottomNavItemTapped, + // ), ), ); }