diff --git a/lib/career_page.dart b/lib/career_page.dart index 39917bf..e3fe3cc 100644 --- a/lib/career_page.dart +++ b/lib/career_page.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:csp2/common/widgets/custom_bottom_nav_bar.dart'; - +import 'package:csp2/common/widgets/job_card.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:csp2/job.dart'; class JobsPage extends StatefulWidget { const JobsPage({super.key}); @@ -10,50 +12,98 @@ class JobsPage extends StatefulWidget { } class _JobsPageState extends State { - int _selectedIndex = 3; // Assuming Jobs is the 4th item (index 3) String? _selectedDropdownValue; // 드롭다운 선택 값 저장 변수 + String _selectedJobTag = 'Hair'; // 초기 탭 선택 값 + List? _jobData; // Job 모델 객체 리스트로 변경 + bool _isLoading = true; + + final List _jobTags = ['Hair', 'Skincare', 'Bodycare', 'Service', 'IT', 'Education']; @override void initState() { super.initState(); _selectedDropdownValue = 'Indonesia'; // 초기값 설정 + _fetchJobData(); } - void _onItemTapped(int index) { + Future _fetchJobData() async { setState(() { - _selectedIndex = index; + _isLoading = true; }); - // TODO: Add navigation logic here based on index - // For now, we'll just print the index - print('Tapped index: $index'); + try { + final response = await http.get(Uri.parse( + 'https://helloworld4-ad2uqhckxq-uc.a.run.app/?country=$_selectedDropdownValue&jobtag=$_selectedJobTag')); + if (response.statusCode == 200) { + setState(() { + _jobData = (json.decode(response.body) as List) + .map((item) => Job.fromJson(item)) + .toList(); + }); + } else { + _jobData = null; // Clear data on error + } + } catch (e) { + _jobData = null; // Clear data on error + } finally { + setState(() { + _isLoading = false; + }); + } } @override Widget build(BuildContext context) { - return Scaffold( - body: Padding( - padding: const EdgeInsets.all(16.0), // 상단에 패딩 추가 - child: Column( - mainAxisAlignment: MainAxisAlignment.start, // 상단 정렬 - crossAxisAlignment: CrossAxisAlignment.start, // 왼쪽 정렬 + return DefaultTabController( + length: _jobTags.length, + child: Scaffold( + body: Column( children: [ - DropdownButton( - value: _selectedDropdownValue, - onChanged: (String? newValue) { - setState(() { - _selectedDropdownValue = newValue; - }); - }, - items: ['Indonesia', 'Hongkong', 'Singapore', 'South Korea',' Japan'] - .map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: DropdownButton( + value: _selectedDropdownValue, + onChanged: (String? newValue) { + setState(() { + _selectedDropdownValue = newValue; + }); + _fetchJobData(); // Fetch data when dropdown changes + }, + items: ['Indonesia', 'Hongkong', 'Singapore', 'South Korea', 'Japan'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), + TabBar( + isScrollable: true, + tabs: _jobTags.map((tag) => Tab(text: tag)).toList(), + onTap: (index) { + setState(() { + _selectedJobTag = _jobTags[index]; + }); + _fetchJobData(); + }, + ), + Expanded( + child: TabBarView( + children: _jobTags.map((tag) { + return _isLoading + ? const Center(child: CircularProgressIndicator()) + : _jobData == null || _jobData!.isEmpty + ? Center(child: Text('No jobs found for $tag.')) + : ListView.builder( + itemCount: _jobData!.length, + itemBuilder: (context, index) { + final job = _jobData![index]; + return JobCard(job: job); + }, + ); + }).toList(), + ), ), - const SizedBox(height: 20), - Text('Selected: $_selectedDropdownValue'), ], ), ), diff --git a/lib/common/widgets/job_card.dart b/lib/common/widgets/job_card.dart new file mode 100644 index 0000000..55b3783 --- /dev/null +++ b/lib/common/widgets/job_card.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; + +import 'package:csp2/job.dart'; +class JobCard extends StatelessWidget { + final Job job; + + const JobCard({super.key, required this.job}); + + @override + Widget build(BuildContext context) { + return Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 3, + child: SizedBox( + height: 200, + child: Row( + children: [ + // 왼쪽 영역 + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + job.jobName, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + Expanded( + child: Text( + job.jobDescription, + overflow: TextOverflow.ellipsis, + maxLines: 4, + ), + ), + const SizedBox(height: 6), + Text( + "Show More", // You might want to make this clickable to show full description + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + ), + ), + const SizedBox(height: 10), + Text( + job.jobJobtag, + style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black54), + ), + ], + ), + ), + ), + // 오른쪽 영역 + Expanded( + flex: 1, + child: Container( + decoration: const BoxDecoration( + color: Color(0xFFEFF1FB), + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: Text( + "\$ ${job.jobIncome}", + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + + overflow: TextOverflow.ellipsis, + ), + ), + + Flexible( + child: Text( + job.jobIncomeType, + style: TextStyle(color: Colors.grey), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(height: 20), + Flexible( + child: Text( + job.jobLocationCity, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ), + Flexible( + child: Text( + job.jobLocationCountry, + style: TextStyle(color: Colors.grey), + overflow: TextOverflow.ellipsis, + ), + ), + const Spacer(), + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF3056D3), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + ), + child: const Text("지원"), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/home_page.dart b/lib/home_page.dart index 919a271..a45c27c 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -72,8 +72,6 @@ class _HomePageState extends State { String _currentTimeZone = 'Loading timezone...'; late Stream _clockStream; - int _selectedIndex = 0; - // --- 추천 클래스 관련 상태 변수 --- List _recommendPlans = []; int _currentRecommendIndex = 0; @@ -146,7 +144,6 @@ class _HomePageState extends State { _currentTimeZone = 'Failed to get timezone.'; }); } - print('Could not get timezone: $e'); } } @@ -186,7 +183,6 @@ class _HomePageState extends State { 'Failed to load recommend plans. Status Code: ${response.statusCode}'); } } catch (e) { - print('Error fetching recommend plans: $e'); if (mounted) { setState(() { _isLoadingRecommends = false; @@ -226,7 +222,7 @@ class _HomePageState extends State { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context).primaryColor.withAlpha(25), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -242,123 +238,6 @@ class _HomePageState extends State { ); } - // --- 추천 클래스 섹션 위젯 --- - Widget _buildRecommendSection() { - if (_isLoadingRecommends) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 20.0), - child: Center(child: CircularProgressIndicator()), - ); - } - - if (_hasRecommendError) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 16.0), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error_outline, color: Colors.red, size: 40), - const SizedBox(height: 8), - Text( - 'Failed to load recommendations.\n$_recommendErrorText', - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.redAccent), - ), - const SizedBox(height: 8), - ElevatedButton.icon( - icon: const Icon(Icons.refresh), - label: const Text('Retry'), - onPressed: _fetchRecommendPlans, - ) - ], - ), - ), - ); - } - - if (_recommendPlans.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 20.0), - child: Center(child: Text('No recommendations available at the moment.')), - ); - } - - final currentPlan = _recommendPlans[_currentRecommendIndex]; - - return Container( - margin: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 16.0), // 하단 마진 추가 - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, // 카드 색상 사용 - borderRadius: BorderRadius.circular(12.0), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.2), - spreadRadius: 1, - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '✨ Recommend Classes', // 이모지 추가 - style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - - 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)), - - AspectRatio( - aspectRatio: 16 / 9, - child: ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: currentPlan.thumbnail.isNotEmpty - ? Image.network( - currentPlan.thumbnail, - fit: BoxFit.cover, - loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { - if (loadingProgress == null) return child; - return Container( // 로딩 중 배경색 및 인디케이터 중앙 정렬 개선 - color: Colors.grey[200], - child: Center( - child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! - : null, - ), - ), - ); - }, - errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { - return Container( - color: Colors.grey[200], - child: const Center(child: Icon(Icons.broken_image, color: Colors.grey, size: 50)), - ); - }, - ) - : Container( - color: Colors.grey[200], - child: const Center(child: Text('No Image Available', style: TextStyle(color: Colors.grey))), - ), - ), - )], - ), - ); - } - // --- 추천 클래스 섹션 위젯 끝 --- - Widget _buildHomeContent() { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -515,18 +394,6 @@ class _HomePageState extends State { ); } - void _onItemTapped(int index) { - setState(() { - _selectedIndex = index; - // 홈 탭(인덱스 0)으로 돌아올 때 추천 타이머를 다시 시작할 수 있도록 고려 - if (index == 0 && _recommendPlans.isNotEmpty && (_recommendTimer == null || !_recommendTimer!.isActive)) { - _startRecommendTimer(); - } else if (index != 0) { - _recommendTimer?.cancel(); // 다른 탭으로 이동 시 타이머 일시 중지 - } - }); - } - @override Widget build(BuildContext context) { final List pageContents = _buildPageContents(); @@ -536,7 +403,7 @@ class _HomePageState extends State { // title: Text(_selectedIndex == 0 ? 'Home' : 'Plan Page'), // ), body: IndexedStack( // AppBar가 없으므로 SafeArea로 감싸는 것을 고려해볼 수 있습니다. - index: _selectedIndex, + index: 0, children: pageContents, ) ); diff --git a/lib/job.dart b/lib/job.dart new file mode 100644 index 0000000..34076b6 --- /dev/null +++ b/lib/job.dart @@ -0,0 +1,41 @@ + +class Job { + final String jobName; + final String jobJobtag; + final String jobDescription; + final double jobIncome; + final String jobIncomeType; + final String jobLocationCountry; + final String jobLocationCity; + final String jobRequirement; + final String jobTag; + final String jobEndpoint; + + Job({ + required this.jobName, + required this.jobJobtag, + required this.jobDescription, + required this.jobIncome, + required this.jobIncomeType, + required this.jobLocationCountry, + required this.jobLocationCity, + required this.jobRequirement, + required this.jobTag, + required this.jobEndpoint, + }); + + factory Job.fromJson(Map json) { + return Job( + jobName: json['Job_name'] as String, + jobJobtag: json['job_jobtag'] as String, + jobDescription: json['job_decriptopn'] as String, + jobIncome: double.tryParse(json['job_Income'].toString()) ?? 0.0, + jobIncomeType: json['job_income_type'] as String, + jobLocationCountry: json['job_location_country'] as String, + jobLocationCity: json['job_location_city'] as String, + jobRequirement: json['job_requirment'] as String, + jobTag: json['job_tag'] as String, + jobEndpoint: json['job_endpoint'] as String, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 3eeaf46..99bbc8e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -64,7 +64,6 @@ class _MyHomePageState extends State { setState(() { _selectedIndex = index; }); - print('Selected index: $_selectedIndex'); // 디버깅 출력 추가 // HomePage의 _recommendTimer 제어 로직은 HomePage 내부에서 독립적으로 관리됩니다. // 또는 필요에 따라 GlobalKey 등을 사용하여 HomePage의 상태에 접근할 수 있습니다. } diff --git a/lib/plan_page_detail.dart b/lib/plan_page_detail.dart index 4c236f2..964b960 100644 --- a/lib/plan_page_detail.dart +++ b/lib/plan_page_detail.dart @@ -210,6 +210,12 @@ class _PlanPageDetailState extends State { 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, @@ -239,8 +245,9 @@ class _PlanPageDetailState extends State { Widget child, ImageChunkEvent? loadingProgress) { - if (loadingProgress == null) + if (loadingProgress == null) { return child; + } return Center( child: CircularProgressIndicator( diff --git a/lib/youtube_player_page.dart b/lib/youtube_player_page.dart index 4f07764..d65d901 100644 --- a/lib/youtube_player_page.dart +++ b/lib/youtube_player_page.dart @@ -24,7 +24,6 @@ class _YoutubePlayerPageState extends State { String _videoTitle = 'YouTube Video'; final int _currentBottomNavIndex = 0; bool _isSystemUiVisible = true; - bool _isLoading = true; // 로딩 상태 추가 bool _isFullScreen = false; @override @@ -124,18 +123,6 @@ class _YoutubePlayerPageState extends State { } } - void _onNotificationTapped() { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('알림 아이콘 클릭됨')), - ); - } - - void _onProfileTapped() { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('프로필 아이콘 클릭됨')), - ); - } - void _onBottomNavItemTapped(int index) { if (_currentBottomNavIndex == index && index != 0) return; if (index == 0) { @@ -195,9 +182,18 @@ class _YoutubePlayerPageState extends State { Widget build(BuildContext context) { final bool isFullScreen = _controller?.value.isFullScreen ?? false; - // <<< WillPopScope로 Scaffold를 감싸서 뒤로가기 이벤트 가로채기 >>> - return WillPopScope( - onWillPop: _onWillPop, + // <<< PopScope로 Scaffold를 감싸서 뒤로가기 이벤트 가로채기 >>> + return PopScope( + canPop: false, + onPopInvoked: (didPop) async { + if (didPop) { + return; + } + final bool shouldPop = await _onWillPop(); + if (shouldPop) { + Navigator.of(context).pop(); + } + }, child: Scaffold( extendBodyBehindAppBar: isFullScreen,