Compare commits

...

3 Commits

Author SHA1 Message Date
girinb
2209ec64e3 테마 기능 분리
구글 가변 폰트 추가
업커밍 존 카드 추출
2025-07-14 21:20:10 +09:00
girinb
2e825bbae2 스크롤 초기화 기능 추가 2025-07-14 16:28:11 +09:00
girinb
05f56e91cc 릴리즈 1차 2025-07-14 15:34:37 +09:00
9 changed files with 315 additions and 135 deletions

View File

@@ -1,8 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="csp2"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"

View File

@@ -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, // 그림자 제거
),
);
}

View File

@@ -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: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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)))
),
),
],
),
),
),
),
);
}
}

View File

@@ -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<HomePage> {
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<HomePage> {
);
}
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: <Widget>[
_buildLocalTimeBar(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
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<List<CaseStudyPlan>>(
future: _caseStudyPlans,
builder: (context, snapshot) {
@@ -306,79 +330,16 @@ class _HomePageState extends State<HomePage> {
} 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: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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)))),
],
),
),
),
);
},
);

View File

@@ -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<MyHomePage> {
@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: <Widget>[
IconButton(
icon: const Icon(Icons.notifications),

View File

@@ -51,11 +51,12 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
late int _currentBottomNavIndex;
String? _selectedYoutubeUrl;
PlanDetailItem? _selectedItem; // <<< 선택된 아이템을 저장할 변수 추가
final ScrollController _scrollController = ScrollController(); // 스크롤 컨트롤러 추가
@override
void initState() {
super.initState();
_currentBottomNavIndex = 0;
_currentBottomNavIndex = 1;
}
@override
@@ -124,6 +125,12 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
);
}
@override
void dispose() {
_scrollController.dispose(); // 스크롤 컨트롤러 해제
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -191,6 +198,7 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
final item = details[index];
return GestureDetector(
onTap: () {
_scrollController.jumpTo(0); // 스크롤 맨 위로 이동
setState(() {
_selectedItem = item;
if (item.lessonUrl.isNotEmpty &&
@@ -292,6 +300,7 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
),
Expanded(
child: SingleChildScrollView(
controller: _scrollController, // 스크롤 컨트롤러 연결
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -337,7 +346,7 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
),
),
IconButton(
icon: const Icon(Icons.play_circle_fill, size: 40, color: Colors.red),
icon: const Icon(Icons.play_circle_fill, size: 40, color: Colors.blue),
onPressed: () {
if (_selectedYoutubeUrl != null && _selectedYoutubeUrl!.isNotEmpty) {
Navigator.push(

View File

@@ -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"))
}

View File

@@ -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"

View File

@@ -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: