import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'main.dart'; import 'youtube_player_page.dart'; // YoutubePlayerPage import import 'common/widgets/custom_bottom_nav_bar.dart'; // PlanDetailItem 클래스 (이전과 동일) class PlanDetailItem { final String lessonId; final String lessonTag; final String lessonUrl; final String thumbnail; final String lessonName; final String lessonDescription; PlanDetailItem({ required this.lessonId, required this.lessonTag, required this.lessonUrl, required this.thumbnail, required this.lessonName, required this.lessonDescription, }); factory PlanDetailItem.fromJson(Map json) { return PlanDetailItem( lessonId: json['casestudy lesson id'] ?? 'ID 없음', lessonTag: json['lesson tag'] ?? '태그 없음', lessonUrl: json['lesson url'] ?? 'URL 없음', thumbnail: json['thumbnail'] ?? '', lessonName: json['lesson_name'] ?? '이름 없음', lessonDescription: json['lesson_description'] ?? '설명 없음', ); } } class PlanPageDetail extends StatefulWidget { const PlanPageDetail({ super.key, }); @override State createState() => _PlanPageDetailState(); } class _PlanPageDetailState extends State { String? _planId; String? _planTitle; // <<< planTitle을 저장할 상태 변수 추가 >>> Future>? _planDetails; late int _currentBottomNavIndex; String? _selectedYoutubeUrl; PlanDetailItem? _selectedItem; // <<< 선택된 아이템을 저장할 변수 추가 @override void initState() { super.initState(); _currentBottomNavIndex = 1; } @override void didChangeDependencies() { super.didChangeDependencies(); // 인자를 한 번만 처리하도록 조건 추가 if (_planId == null && _planTitle == null) { final Object? arguments = ModalRoute.of(context)?.settings.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 { // 인자가 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')); 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(); } else { 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}'); } } void _onBottomNavItemTapped(int index) { setState(() { _currentBottomNavIndex = index; }); Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute(builder: (context) => MyHomePage(initialIndex: index)), (Route route) => false, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( toolbarHeight: 40, title: Text(_planTitle.toString()), ), body: _planId == null && _planTitle == null && _planDetails == null ? Center( // 초기 로딩 상태 또는 인자 오류 child: _planTitle == 'Error' ? const Text( '플랜 정보를 불러올 수 없습니다.', style: TextStyle(fontSize: 18, color: Colors.red), textAlign: TextAlign.center, maxLines: 3, overflow: TextOverflow.ellipsis, ) : const CircularProgressIndicator(), ) : FutureBuilder>( future: _planDetails, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Text('Error loading details: ${snapshot.error}', textAlign: TextAlign.center, maxLines: 3, overflow: TextOverflow.ellipsis, ))); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return const Center(child: Text('세부 계획 데이터가 없습니다.')); } else { final details = snapshot.data!; if (_selectedItem == null && details.isNotEmpty) { _selectedItem = details.first; } // 첫 번째 비디오의 URL을 가져와 _selectedYoutubeUrl을 초기화합니다. if (_selectedYoutubeUrl == null && details.isNotEmpty) { _selectedYoutubeUrl = details.firstWhere( (item) => item.lessonUrl.isNotEmpty && (item.lessonUrl.contains('youtube.com') || item.lessonUrl.contains('youtu.be')), orElse: () => PlanDetailItem(lessonId: '', lessonTag: '', lessonUrl: '', thumbnail: '',lessonName: '', lessonDescription: ''), ).lessonUrl.isNotEmpty ? details.firstWhere( (item) => item.lessonUrl.isNotEmpty && (item.lessonUrl.contains('youtube.com') || item.lessonUrl.contains('youtu.be')), orElse: () => PlanDetailItem(lessonId: '', lessonTag: '', lessonUrl: '', thumbnail: '',lessonName: '', lessonDescription: ''), ).lessonUrl : null; } return Column( // ListView와 YoutubePlayerPage를 세로로 배치하기 위해 Column 사용 children: [ SizedBox( height: 150, // 가로 스크롤 리스트의 높이 child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 8.0), itemCount: details.length, itemBuilder: (context, index) { final item = details[index]; return GestureDetector( onTap: () { setState(() { _selectedItem = item; if (item.lessonUrl.isNotEmpty && (item.lessonUrl.contains('youtube.com') || item.lessonUrl.contains('youtu.be'))) { _selectedYoutubeUrl = item.lessonUrl; } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '유효한 YouTube URL이 아닙니다: ${item.lessonUrl}')), ); } }); }, child: Container( width: 110, margin: const EdgeInsets.symmetric( horizontal: 8.0, vertical: 8.0), decoration: BoxDecoration( border: _selectedItem == item ? Border.all(color: Colors.blueAccent, width: 3) : null, borderRadius: BorderRadius.circular(8.0), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: 100, height: 100, child: ClipRRect( borderRadius: BorderRadius.circular(8.0), child: item.thumbnail.isNotEmpty ? Image.network( item.thumbnail, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( color: Colors.grey[200], child: const Icon( Icons.broken_image, size: 40, color: Colors.grey), ); }, loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { if (loadingProgress == null) { return child; } return Center( child: CircularProgressIndicator( value: loadingProgress .expectedTotalBytes != null ? loadingProgress .cumulativeBytesLoaded / loadingProgress .expectedTotalBytes! : null, strokeWidth: 2.0, ), ); }, ) : Container( color: Colors.grey[200], child: const Icon( Icons.image_not_supported, size: 40, color: Colors.grey), ), ), ), const SizedBox(height: 6), Text( item.lessonTag, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500), maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), ], ), ), ); }, ), ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _selectedItem!.thumbnail.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(12.0), child: Image.network( _selectedItem!.thumbnail, height: 200, width: double.infinity, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( height: 200, width: double.infinity, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(12.0), ), child: const Icon(Icons.broken_image, size: 60, color: Colors.grey), ); }, ), ) : Container( height: 200, width: double.infinity, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(12.0), ), child: const Icon(Icons.image_not_supported, size: 60, color: Colors.grey), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( _selectedItem!.lessonName, style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), ), ), IconButton( icon: const Icon(Icons.play_circle_fill, size: 40, color: Colors.red), onPressed: () { if (_selectedYoutubeUrl != null && _selectedYoutubeUrl!.isNotEmpty) { Navigator.push( context, MaterialPageRoute( builder: (context) => YoutubePlayerPage(lessonUrl: _selectedYoutubeUrl!), ), ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('재생할 수 있는 영상이 없습니다.')), ); } }, ), ], ), const SizedBox(height: 12), Text( _selectedItem!.lessonDescription, style: Theme.of(context).textTheme.bodyLarge, ), ], ), ), ), ], ); } }, ), bottomNavigationBar: CustomBottomNavBar( currentIndex: _currentBottomNavIndex, onTap: _onBottomNavItemTapped, ), ); } }