diff --git a/lib/common/theme/app_theme.dart b/lib/common/theme/app_theme.dart new file mode 100644 index 0000000..f524b23 --- /dev/null +++ b/lib/common/theme/app_theme.dart @@ -0,0 +1,32 @@ + +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppTheme { + static final ThemeData lightTheme = ThemeData( + useMaterial3: true, + // ColorScheme을 사용하여 전체적인 색상 톤을 설정합니다. + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.blue, // 버튼 등 강조 색상은 파란색 계열로 유지합니다. + brightness: Brightness.light, // 밝은 테마로 설정합니다. + background: Colors.white, // 앱의 전체 배경을 흰색으로 지정합니다. + surface: Colors.white, // 카드 등 UI 요소의 표면 색을 흰색으로 지정합니다. + ), + // Scaffold의 기본 배경색을 흰색으로 명확하게 지정합니다. + scaffoldBackgroundColor: Colors.white, + // 폰트 테마를 설정하고, 기본 텍스트 색상을 검은색으로 지정합니다. + textTheme: GoogleFonts.hedvigLettersSansTextTheme( + ThemeData.light().textTheme, + ).apply( + bodyColor: Colors.black, // 일반 텍스트 색상 + displayColor: Colors.black, // 제목 등 큰 텍스트 색상 + ), + // 앱 바 테마를 흰색 배경, 검은색 아이콘/글씨로 설정합니다. + appBarTheme: const AppBarTheme( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 0, // 그림자 제거 + ), + ); +} + diff --git a/lib/common/widgets/upcoming_class_card.dart b/lib/common/widgets/upcoming_class_card.dart new file mode 100644 index 0000000..753a116 --- /dev/null +++ b/lib/common/widgets/upcoming_class_card.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import '../../home_page.dart'; // CaseStudyPlan 모델을 import 합니다. + +class UpcomingClassCard extends StatelessWidget { + final CaseStudyPlan plan; + final VoidCallback onTap; + + const UpcomingClassCard({ + super.key, + required this.plan, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 300, // 각 항목의 너비를 지정합니다. + child: InkWell( + onTap: onTap, + child: Card( + margin: const EdgeInsets.only(right: 12.0), // 항목 간의 간격 조정 + elevation: 2.0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + plan.planTitle, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), + const SizedBox(width: 8.0), + Text( + // plan.planTeacher, + "", + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[600]), + ), + ], + ), + const SizedBox(height: 8.0), + if (plan.thumbnail.isNotEmpty) + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image.network( + plan.thumbnail, + width: double.infinity, + fit: BoxFit.cover, + 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, + ), + ); + }, + errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { + return const Center(child: Icon(Icons.broken_image, color: Colors.grey, size: 50)); + }, + ), + ), + ) + else + Expanded( + child: Container( + color: Colors.grey[200], + child: const Center(child: Text('No Image', style: TextStyle(color: Colors.grey))) + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/home_page.dart b/lib/home_page.dart index a45c27c..59033a6 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -3,6 +3,7 @@ import 'package:http/http.dart' as http; import 'dart:convert'; import 'dart:async'; // Timer를 사용하기 위해 추가 import 'package:intl/intl.dart'; +import 'package:csp2/common/widgets/upcoming_class_card.dart'; // import 'package:flutter_native_timezone/flutter_native_timezone.dart'; // 주석 처리된 상태 유지 import '../plan_page.dart'; // PlanPage import @@ -24,7 +25,7 @@ class CaseStudyPlan { return CaseStudyPlan( planId: json['casestudy lesson id'] ?? '아이디 없음', planTitle: json['course_name'] ?? '제목 없음', - planTeacher: json['planTeacher'] ?? '', + planTeacher: json['planTeacher'] ?? '선생님', thumbnail: json['course_thumbnail'] ?? '', ); } @@ -220,17 +221,20 @@ class _HomePageState extends State { final String formattedDate = DateFormat.yMMMMd().format(displayTime); final String formattedTime = DateFormat.jms().format(displayTime); return Container( - width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - color: Theme.of(context).primaryColor.withAlpha(25), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withAlpha(25), + borderRadius: BorderRadius.circular(8.0), + ), child: Column( + mainAxisAlignment: MainAxisAlignment.center, // 세로 중앙 정렬 crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Your Local Time', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)), const SizedBox(height: 6.0), - Text('$formattedDate, $formattedTime', style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.w500)), - const SizedBox(height: 4.0), - Text('Timezone: $_currentTimeZone', style: TextStyle(fontSize: 13.0, color: Colors.grey[700])), + Text('$formattedTime', style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.w500)), + // const SizedBox(height: 4.0), + // Text('Timezone: $_currentTimeZone', style: TextStyle(fontSize: 13.0, color: Colors.grey[700])), ], ), ); @@ -238,59 +242,79 @@ class _HomePageState extends State { ); } + Widget _buildButtons() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, // 세로 중앙 정렬 + crossAxisAlignment: CrossAxisAlignment.stretch, // 버튼이 가로로 꽉 차게 설정 + children: [ + Expanded( // 버튼이 할당된 세로 공간을 모두 차지하도록 설정 + child: ElevatedButton.icon( + icon: const Icon(Icons.add_circle_outline, size: 20), + label: const Text('Book Class', style: TextStyle(fontSize: 20)), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Book Class 버튼 기능 구현 예정')), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, // 배경색을 파란색으로 설정 + foregroundColor: Colors.white, // 글자색을 흰색으로 설정 + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + ), + // const SizedBox(height: 8.0), + // ElevatedButton.icon( + // icon: const Icon(Icons.schedule, size: 20), + // label: const Text('Schedule', style: TextStyle(fontSize: 14)), + // onPressed: () { + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar(content: Text('Schedule 버튼 기능 구현 예정')), + // ); + // }, + // style: ElevatedButton.styleFrom( + // padding: const EdgeInsets.symmetric(vertical: 12.0), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(8.0), + // ), + // ), + // ), + ], + ); + } + Widget _buildHomeContent() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildLocalTimeBar(), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: ElevatedButton.icon( - icon: const Icon(Icons.add_circle_outline, size: 20), - label: const Text('Book Class', style: TextStyle(fontSize: 14)), - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Book Class 버튼 기능 구현 예정')), - ); - }, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + flex: 1, + child: _buildLocalTimeBar(), ), - ), - const SizedBox(width: 12.0), - Expanded( - child: ElevatedButton.icon( - icon: const Icon(Icons.schedule, size: 20), - label: const Text('Schedule', style: TextStyle(fontSize: 14)), - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Schedule 버튼 기능 구현 예정')), - ); - }, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), + const SizedBox(width: 12.0), + Expanded( + flex: 1, + child: _buildButtons(), ), - ), - ], + ], + ), ), ), const Padding( padding: EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0), // vertical을 위아래 다르게 조정 child: Text('Upcoming Classes', style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold)), ), - Expanded( + SizedBox( + height: 250, // 가로 리스트의 높이를 지정합니다. child: FutureBuilder>( future: _caseStudyPlans, builder: (context, snapshot) { @@ -306,79 +330,16 @@ class _HomePageState extends State { } else { final plans = snapshot.data!; return ListView.builder( - padding: const EdgeInsets.only(top: 8.0), + scrollDirection: Axis.horizontal, // 가로 스크롤로 변경 + padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 8.0), itemCount: plans.length, itemBuilder: (context, index) { final plan = plans[index]; - return InkWell( + return UpcomingClassCard( + plan: plan, onTap: () { - // *** 부모 위젯(MyHomePage)에게 Plan 탭으로 이동하라고 알림 *** - // *** PlanPage가 _widgetOptions 리스트에서 두 번째(인덱스 1)라고 가정 *** - widget.onNavigateToPlanTab(1); // *** 전달받은 콜백 호출 *** + widget.onNavigateToPlanTab(1); }, - child: Card( - margin: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 16.0), - elevation: 2.0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - plan.planTitle, - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - ), - const SizedBox(width: 8.0), - Text( - // plan.planTeacher, - "", - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[600]), - ), - ], - ), - const SizedBox(height: 8.0), - if (plan.thumbnail.isNotEmpty) - ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: Image.network( - plan.thumbnail, - height: 150, - width: double.infinity, - fit: BoxFit.cover, - loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { - if (loadingProgress == null) return child; - return Container( - height: 150, - 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(height: 150, color: Colors.grey[200], child: const Center(child: Icon(Icons.broken_image, color: Colors.grey, size: 50))); - }, - ), - ) - else - Container(height: 150, color: Colors.grey[200], child: const Center(child: Text('No Image', style: TextStyle(color: Colors.grey)))), - ], - ), - ), - ), ); }, ); diff --git a/lib/main.dart b/lib/main.dart index deacbab..81474c9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ // main.dart +import 'package:csp2/common/theme/app_theme.dart'; import 'package:flutter/material.dart'; // 새로 만든 페이지 파일들을 import 합니다. @@ -21,10 +22,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Case Study', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Color(0xFF2005E6)), - useMaterial3: true, - ), + theme: AppTheme.lightTheme, home: const MyHomePage(), ); } @@ -80,25 +78,24 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { // AppBar의 제목을 현재 탭에 따라 동적으로 변경 - String appBarTitle = 'Home'; // 기본값 - if (_selectedIndex == 1) { - appBarTitle = 'Plan'; - } - else if (_selectedIndex == 2) { - appBarTitle = 'Statistics'; - } - else if (_selectedIndex == 3) { - appBarTitle = 'Career'; - } - else if (_selectedIndex == 4) { - appBarTitle = 'More'; - } + // String appBarTitle = 'Home'; // 기본값 + // if (_selectedIndex == 1) { + // appBarTitle = 'Plan'; + // } + // else if (_selectedIndex == 2) { + // appBarTitle = 'Statistics'; + // } + // else if (_selectedIndex == 3) { + // appBarTitle = 'Career'; + // } + // else if (_selectedIndex == 4) { + // appBarTitle = 'More'; + // } return Scaffold( appBar: AppBar( toolbarHeight: 40, - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(appBarTitle), // 동적으로 변경된 AppBar 제목\ + title: Text("Case Study"), // 동적으로 변경된 AppBar 제목\ actions: [ IconButton( icon: const Icon(Icons.notifications), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 738fc0a..f4894ad 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,9 @@ import FlutterMacOS import Foundation import flutter_inappwebview_macos +import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 61ac6ed..98190ee 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -57,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" flutter: dependency: "direct main" description: flutter @@ -144,6 +160,14 @@ packages: description: flutter source: sdk version: "0.0.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" http: dependency: "direct main" description: @@ -232,6 +256,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -325,6 +405,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" youtube_player_flutter: dependency: "direct main" description: @@ -335,4 +423,4 @@ packages: version: "9.1.1" sdks: dart: ">=3.8.1 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5d3bc93..1a6242e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + google_fonts: ^6.2.1 dev_dependencies: flutter_test: