Compare commits

..

10 Commits

Author SHA1 Message Date
girinb
a926d9c7bc 플랜 프토토타입 완료 2025-07-16 00:23:54 +09:00
girinb
5f79863698 플랜 태그 변경 2025-07-15 22:23:43 +09:00
girinb
bdcfd8497d 전체적인 시스템 추가.2 2025-07-15 21:19:11 +09:00
girinb
d42fcb7102 전체적인 시스템 추가. 2025-07-15 21:18:57 +09:00
girinb
a75ba845e4 하단 무한 조닝 추가., 2025-07-15 01:45:19 +09:00
girinb
5fbabe9238 하단 광고 추가 2025-07-15 01:17:16 +09:00
girinb
deda09ff84 대학연결 2025-07-15 00:55:21 +09:00
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
72 changed files with 1268 additions and 382 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item> <item>
<bitmap <bitmap android:gravity="center" android:src="@drawable/splash"/>
android:gravity="center" </item>
android:src="@mipmap/launch_image" />
</item> -->
</layer-list> </layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item> <item>
<bitmap <bitmap android:gravity="center" android:src="@drawable/splash"/>
android:gravity="center" </item>
android:src="@mipmap/launch_image" />
</item> -->
</layer-list> </layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when <!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame --> the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style> </style>
<!-- Theme applied to the Android Window as soon as the process has started. <!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your This theme determines the color of the Android Window while your

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when <!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame --> the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style> </style>
<!-- Theme applied to the Android Window as soon as the process has started. <!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your This theme determines the color of the Android Window while your

BIN
assets/icon/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
assets/splash/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -427,7 +427,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@@ -484,7 +484,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "background.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -1,23 +1,23 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal",
"filename" : "LaunchImage.png", "filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@2x.png", "filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@3x.png", "filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "xcode",
"author" : "xcode" "version" : 1
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -16,13 +16,19 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
</imageView> <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
</subviews> </subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/> <constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/> <constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
</constraints> </constraints>
</view> </view>
</viewController> </viewController>
@@ -32,6 +38,7 @@
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<image name="LaunchImage" width="168" height="185"/> <image name="LaunchImage" width="1500" height="500"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources> </resources>
</document> </document>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@@ -45,5 +45,7 @@
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
</dict> <key>UIStatusBarHidden</key>
<false/>
</dict>
</plist> </plist>

View File

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:csp2/common/widgets/job_card.dart'; import 'package:csp2/common/widgets/job_card.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import 'package:csp2/job.dart'; import 'package:csp2/common/data/job.dart';
class JobsPage extends StatefulWidget { class JobsPage extends StatefulWidget {
const JobsPage({super.key}); const JobsPage({super.key});

View File

@@ -0,0 +1,23 @@
class CaseStudyPlan {
final String planId;
final String planTitle;
final String planTeacher;
final String thumbnail;
CaseStudyPlan({
required this.planId,
required this.planTitle,
required this.planTeacher,
required this.thumbnail,
});
factory CaseStudyPlan.fromJson(Map<String, dynamic> json) {
return CaseStudyPlan(
planId: json['casestudy lesson id'] ?? '아이디 없음',
planTitle: json['course_name'] ?? '제목 없음',
planTeacher: json['course_description'] ?? '',
thumbnail: json['course_thumbnail'] ?? '',
);
}
}

View File

@@ -0,0 +1,32 @@
class Course {
final String affiliateNumber;
final String affiliateName;
final String affiliateType;
final String affiliateResult;
final String affiliateDescription;
final String affiliateOrg;
final String affiliateOrgIcon;
Course({
required this.affiliateNumber,
required this.affiliateName,
required this.affiliateType,
required this.affiliateResult,
required this.affiliateDescription,
required this.affiliateOrg,
required this.affiliateOrgIcon,
});
factory Course.fromJson(Map<String, dynamic> json) {
return Course(
affiliateNumber: json['affliate_number'] as String,
affiliateName: json['affliate_name'] as String,
affiliateType: json['affliate_type'] as String,
affiliateResult: json['affliate_result'] as String,
affiliateDescription: json['affliate_discription'] as String,
affiliateOrg: json['affliate_org'] as String,
affiliateOrgIcon: json['affliate_org_icon'] as String,
);
}
}

View File

@@ -0,0 +1,22 @@
class NewStudy {
final String planId;
final String planTitle;
final String planTeacher;
final String thumbnail;
NewStudy({
required this.planId,
required this.planTitle,
required this.planTeacher,
required this.thumbnail,
});
factory NewStudy.fromJson(Map<String, dynamic> json) {
return NewStudy(
planId: json['casestudy lesson id'] ?? '아이디 없음',
planTitle: json['course_name'] ?? '제목 없음',
planTeacher: json['course_description'] ?? '선생님',
thumbnail: json['course_thumbnail'] ?? '',
);
}
}

View File

@@ -0,0 +1,29 @@
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<String, dynamic> 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'] ?? '설명 없음',
);
}
}

View File

@@ -0,0 +1,23 @@
class UpcomingStudy {
final String planId;
final String planTitle;
final String planTeacher;
final String thumbnail;
UpcomingStudy({
required this.planId,
required this.planTitle,
required this.planTeacher,
required this.thumbnail,
});
factory UpcomingStudy.fromJson(Map<String, dynamic> json) {
return UpcomingStudy(
planId: json['casestudy lesson id'] ?? '아이디 없음',
planTitle: json['course_name'] ?? '제목 없음',
planTeacher: json['course_description'] ?? '선생님',
thumbnail: json['course_thumbnail'] ?? '',
);
}
}

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,56 @@
import 'package:flutter/material.dart';
import 'package:csp2/common/data/course.dart';
class CourseCard extends StatelessWidget {
final Course course;
const CourseCard({super.key, required this.course});
@override
Widget build(BuildContext context) {
return Container(
width: 150, // 각 항목의 너비
margin: const EdgeInsets.only(right: 12.0),
padding: const EdgeInsets.all(8.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: [
ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.network(
course.affiliateOrgIcon,
width: double.infinity, // 가로 사이즈에 비례하여 채우도록 변경
height: 100,
fit: BoxFit.contain,
),
),
const SizedBox(height: 8.0),
Text(
course.affiliateName,
style: const TextStyle(fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
course.affiliateDescription,
style: const TextStyle(color: Colors.grey),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:csp2/job.dart'; import 'package:csp2/common/data/job.dart';
class JobCard extends StatelessWidget { class JobCard extends StatelessWidget {
final Job job; final Job job;

View File

@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import '../data/new_study.dart'; // NewStudy 모델을 import 합니다.
class StudyClassCard extends StatelessWidget {
final NewStudy plan;
final VoidCallback onTap;
const StudyClassCard({
super.key,
required this.plan,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width / 2, // 화면 너비의 절반으로 설정
child: InkWell(
onTap: onTap,
child: Card(
margin: const EdgeInsets.only(right: 12.0), // 항목 간의 간격 조정
elevation: 2.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5.0)),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (plan.thumbnail.isNotEmpty)
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.network(
plan.thumbnail,
width: double.infinity,
fit: BoxFit.contain,
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)))
),
),
// const SizedBox(height: 5.0),
SizedBox(
height: 70, // 텍스트 영역의 고정 높이 설정 (두 줄 텍스트를 위한 충분한 공간)
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 48.0, // plan.planTitle이 항상 두 줄 공간을 차지하도록 고정
child: Text(
plan.planTitle,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
const SizedBox(height: 4.0),
Text(
plan.planTeacher,
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[600]),
maxLines: 1,
),
],
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import '../data/upcoming_study.dart'; // UpcomingStudy 모델을 import 합니다.
class UpcomingClassCard extends StatelessWidget {
final UpcomingStudy plan;
final VoidCallback onTap;
const UpcomingClassCard({
super.key,
required this.plan,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width / 2, // 화면 너비의 절반으로 설정
child: InkWell(
onTap: onTap,
child: Card(
margin: const EdgeInsets.only(right: 12.0), // 항목 간의 간격 조정
elevation: 2.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5.0)),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (plan.thumbnail.isNotEmpty)
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.network(
plan.thumbnail,
width: double.infinity,
fit: BoxFit.contain,
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)))
),
),
// const SizedBox(height: 5.0),
SizedBox(
height: 70, // 텍스트 영역의 고정 높이 설정 (두 줄 텍스트를 위한 충분한 공간)
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 48.0, // plan.planTitle이 항상 두 줄 공간을 차지하도록 고정
child: Text(
plan.planTitle,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
const SizedBox(height: 4.0),
Text(
plan.planTeacher,
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[600]),
maxLines: 1,
),
],
),
),
],
),
),
),
),
);
}
}

View File

@@ -3,32 +3,13 @@ import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; // Timer를 사용하기 위해 추가 import 'dart:async'; // Timer를 사용하기 위해 추가
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
// import 'package:flutter_native_timezone/flutter_native_timezone.dart'; // 주석 처리된 상태 유지 import 'common/widgets/upcoming_class_card.dart';
import '../plan_page.dart'; // PlanPage import import 'common/widgets/course_card.dart';
import 'common/data/course.dart'; // Course 클래스 import
// CaseStudyPlan 클래스 (변경 없음) import 'plan_page.dart'; // PlanPage import
class CaseStudyPlan { import 'common/widgets/now_study_class_card.dart';
final String planId; import 'common/data/upcoming_study.dart';
final String planTitle; import 'common/data/new_study.dart';
final String planTeacher;
final String thumbnail;
CaseStudyPlan({
required this.planId,
required this.planTitle,
required this.planTeacher,
required this.thumbnail,
});
factory CaseStudyPlan.fromJson(Map<String, dynamic> json) {
return CaseStudyPlan(
planId: json['casestudy lesson id'] ?? '아이디 없음',
planTitle: json['course_name'] ?? '제목 없음',
planTeacher: json['planTeacher'] ?? '',
thumbnail: json['course_thumbnail'] ?? '',
);
}
}
// 새로운 추천 플랜 모델 // 새로운 추천 플랜 모델
class RecommendPlan { class RecommendPlan {
@@ -67,7 +48,9 @@ class HomePage extends StatefulWidget {
} }
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
late Future<List<CaseStudyPlan>> _caseStudyPlans; late Future<List<UpcomingStudy>> _upcomingStudies;
late Future<List<NewStudy>> _newStudies;
late Future<List<Course>> _newCoursesFuture; // New: Future for new courses
DateTime _currentTime = DateTime.now(); DateTime _currentTime = DateTime.now();
String _currentTimeZone = 'Loading timezone...'; String _currentTimeZone = 'Loading timezone...';
late Stream<DateTime> _clockStream; late Stream<DateTime> _clockStream;
@@ -91,7 +74,9 @@ class _HomePageState extends State<HomePage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_caseStudyPlans = _fetchCaseStudyPlans(); _upcomingStudies = _fetchUpcomingStudies();
_newStudies = _fetchNewStudies();
_newCoursesFuture = _fetchNewCourses(); // Initialize new courses future
_fetchTimezone(); _fetchTimezone();
_clockStream = Stream.periodic(const Duration(seconds: 1), (_) { _clockStream = Stream.periodic(const Duration(seconds: 1), (_) {
return DateTime.now(); return DateTime.now();
@@ -109,7 +94,28 @@ class _HomePageState extends State<HomePage> {
super.dispose(); super.dispose();
} }
Future<List<CaseStudyPlan>> _fetchCaseStudyPlans() async { Future<List<UpcomingStudy>> _fetchUpcomingStudies() async {
final response = await http
.get(Uri.parse('https://helloworld6-ad2uqhckxq-uc.a.run.app'));
if (response.statusCode == 200) {
final Map<String, dynamic> decodedJson = json.decode(response.body);
if (decodedJson.containsKey('data') && decodedJson['data'] is List) {
final List<dynamic> plansJson = decodedJson['data'];
return plansJson
.map((jsonItem) =>
UpcomingStudy.fromJson(jsonItem as Map<String, dynamic>))
.toList();
} else {
throw Exception(
'Invalid data format: "data" field is missing or not a list.');
}
} else {
throw Exception(
'Failed to load upcoming studies. Status Code: ${response.statusCode}');
}
}
Future<List<NewStudy>> _fetchNewStudies() async {
final response = await http final response = await http
.get(Uri.parse('https://helloworld2-ad2uqhckxq-uc.a.run.app')); .get(Uri.parse('https://helloworld2-ad2uqhckxq-uc.a.run.app'));
if (response.statusCode == 200) { if (response.statusCode == 200) {
@@ -118,7 +124,7 @@ class _HomePageState extends State<HomePage> {
final List<dynamic> plansJson = decodedJson['data']; final List<dynamic> plansJson = decodedJson['data'];
return plansJson return plansJson
.map((jsonItem) => .map((jsonItem) =>
CaseStudyPlan.fromJson(jsonItem as Map<String, dynamic>)) NewStudy.fromJson(jsonItem as Map<String, dynamic>))
.toList(); .toList();
} else { } else {
throw Exception( throw Exception(
@@ -126,7 +132,26 @@ class _HomePageState extends State<HomePage> {
} }
} else { } else {
throw Exception( throw Exception(
'Failed to load case study plans. Status Code: ${response.statusCode}'); 'Failed to load new studies. Status Code: ${response.statusCode}');
}
}
// New: Fetch new courses from API
Future<List<Course>> _fetchNewCourses() async {
final response = await http.get(Uri.parse('https://helloworld5-ad2uqhckxq-uc.a.run.app'));
if (response.statusCode == 200) {
final Map<String, dynamic> decodedJson = json.decode(response.body);
if (decodedJson.containsKey('data') && decodedJson['data'] is List) {
final List<dynamic> coursesJson = decodedJson['data'];
return coursesJson
.map((jsonItem) => Course.fromJson(jsonItem as Map<String, dynamic>))
.toList();
} else {
throw Exception('Invalid data format for new courses: "data" field is missing or not a list.');
}
}
else {
throw Exception('Failed to load new courses. Status Code: ${response.statusCode}');
} }
} }
@@ -220,17 +245,20 @@ class _HomePageState extends State<HomePage> {
final String formattedDate = DateFormat.yMMMMd().format(displayTime); final String formattedDate = DateFormat.yMMMMd().format(displayTime);
final String formattedTime = DateFormat.jms().format(displayTime); final String formattedTime = DateFormat.jms().format(displayTime);
return Container( return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withAlpha(25), color: Theme.of(context).primaryColor.withAlpha(25),
borderRadius: BorderRadius.circular(8.0),
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, // 세로 중앙 정렬
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text('Your Local Time', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)), const Text('Your Local Time', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)),
const SizedBox(height: 6.0), const SizedBox(height: 6.0),
Text('$formattedDate, $formattedTime', style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.w500)), Text('$formattedTime', style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.w500)),
const SizedBox(height: 4.0), // const SizedBox(height: 4.0),
Text('Timezone: $_currentTimeZone', style: TextStyle(fontSize: 13.0, color: Colors.grey[700])), // Text('Timezone: $_currentTimeZone', style: TextStyle(fontSize: 13.0, color: Colors.grey[700])),
], ],
), ),
); );
@@ -238,61 +266,82 @@ class _HomePageState extends State<HomePage> {
); );
} }
Widget _buildHomeContent() { Widget _buildButtons() {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, // 세로 중앙 정렬
children: <Widget>[ crossAxisAlignment: CrossAxisAlignment.stretch, // 버튼이 가로로 꽉 차게 설정
_buildLocalTimeBar(), children: [
Padding( Expanded( // 버튼이 할당된 세로 공간을 모두 차지하도록 설정
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: const Icon(Icons.add_circle_outline, size: 20), icon: const Icon(Icons.add_circle_outline, size: 20),
label: const Text('Book Class', style: TextStyle(fontSize: 14)), label: const Text('Book Class', style: TextStyle(fontSize: 20)),
onPressed: () { onPressed: () {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Book Class 버튼 기능 구현 예정')), const SnackBar(content: Text('Book Class 버튼 기능 구현 예정')),
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12.0), backgroundColor: Colors.blue, // 배경색을 파란색으로 설정
foregroundColor: Colors.white, // 글자색을 흰색으로 설정
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0), 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 SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 1,
child: _buildLocalTimeBar(),
),
const SizedBox(width: 12.0), const SizedBox(width: 12.0),
Expanded( Expanded(
child: ElevatedButton.icon( flex: 1,
icon: const Icon(Icons.schedule, size: 20), child: _buildButtons(),
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 Padding( const Padding(
padding: EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0), // vertical을 위아래 다르게 조정 padding: EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0), // vertical을 위아래 다르게 조정
child: Text('Upcoming Classes', style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold)), child: Text('Upcoming Study', style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold)),
), ),
Expanded( SizedBox(
child: FutureBuilder<List<CaseStudyPlan>>( height: 200, // 가로 리스트의 높이를 줄입니다.
future: _caseStudyPlans, child: FutureBuilder<List<UpcomingStudy>>(
future: _upcomingStudies,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@@ -306,78 +355,18 @@ class _HomePageState extends State<HomePage> {
} else { } else {
final plans = snapshot.data!; final plans = snapshot.data!;
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.only(top: 8.0), scrollDirection: Axis.horizontal, // 가로 스크롤로 변경
padding: const EdgeInsets.only(left: 10.0, right: 5.0, top: 8.0, bottom: 5.0),
itemCount: plans.length, itemCount: plans.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final plan = plans[index]; final plan = plans[index];
return InkWell( return Align(
alignment: Alignment.topCenter, // 항목을 상단 중앙에 정렬
child: UpcomingClassCard(
plan: plan,
onTap: () { onTap: () {
// *** 부모 위젯(MyHomePage)에게 Plan 탭으로 이동하라고 알림 *** widget.onNavigateToPlanTab(1);
// *** PlanPage가 _widgetOptions 리스트에서 두 번째(인덱스 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)))),
],
),
),
), ),
); );
}, },
@@ -386,11 +375,89 @@ class _HomePageState extends State<HomePage> {
}, },
), ),
), ),
// --- ▼▼▼ Find Your New Course 섹션 ▼▼▼ ---
const Padding(
padding: EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0),
child: Text('Find Your New Course', style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold)),
),
SizedBox(
height: 180,
child: FutureBuilder<List<Course>>(
future: _newCoursesFuture,
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 new courses: ${snapshot.error}', textAlign: TextAlign.center),
));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No new courses available.'));
} else {
final courses = snapshot.data!;
return ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0, bottom: 5.0),
itemCount: courses.length,
itemBuilder: (context, index) {
final course = courses[index];
return CourseCard(course: course);
},
);
}
},
),
),
// --- ▲▲▲ Find Your New Course 섹션 끝 ▲▲▲ ---
// --- ▼▼▼ 추천 클래스 섹션 호출 ▼▼▼ --- // --- ▼▼▼ 추천 클래스 섹션 호출 ▼▼▼ ---
// _buildRecommendSection(), _buildRecommendSection(),
// --- ▲▲▲ 추천 클래스 섹션 호출 끝 ▲▲▲ --- // --- ▲▲▲ 추천 클래스 섹션 호출 끝 ▲▲▲ ---
const Padding(
padding: EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0),
child: Text('Trending On New Study', style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold)),
),
FutureBuilder<List<NewStudy>>(
future: _newStudies,
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 new studies: ${snapshot.error}', textAlign: TextAlign.center),
));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No new studies available.'));
} else {
final plans = snapshot.data!;
return GridView.builder(
shrinkWrap: true, // Column 안에 GridView를 넣을 때 필요
physics: const NeverScrollableScrollPhysics(), // 부모 SingleChildScrollView와 스크롤 충돌 방지
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 8.0),
itemCount: plans.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 한 줄에 2개씩
crossAxisSpacing: 10.0, // 가로 간격
mainAxisSpacing: 10.0, // 세로 간격
childAspectRatio: 1.0, // 아이템의 가로세로 비율 (조정 필요 시 변경)
),
itemBuilder: (context, index) {
final plan = plans[index];
return StudyClassCard(
plan: plan,
onTap: () {
// widget.onNavigateToPlanTab(1);
},
);
},
);
}
},
),
], ],
),
); );
} }
@@ -408,4 +475,101 @@ class _HomePageState extends State<HomePage> {
) )
); );
} }
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(0.0, 12.0, 0.0, 0.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: [
AspectRatio(
aspectRatio: 6 / 1,
child: ClipRRect(
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))),
),
),
)],
),
);
}
} }

View File

@@ -1,5 +1,6 @@
// main.dart // main.dart
import 'package:csp2/common/theme/app_theme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// 새로 만든 페이지 파일들을 import 합니다. // 새로 만든 페이지 파일들을 import 합니다.
@@ -21,10 +22,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Case Study', title: 'Case Study',
theme: ThemeData( theme: AppTheme.lightTheme,
colorScheme: ColorScheme.fromSeed(seedColor: Color(0xFF2005E6)),
useMaterial3: true,
),
home: const MyHomePage(), home: const MyHomePage(),
); );
} }
@@ -80,25 +78,24 @@ class _MyHomePageState extends State<MyHomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// AppBar의 제목을 현재 탭에 따라 동적으로 변경 // AppBar의 제목을 현재 탭에 따라 동적으로 변경
String appBarTitle = 'Home'; // 기본값 // String appBarTitle = 'Home'; // 기본값
if (_selectedIndex == 1) { // if (_selectedIndex == 1) {
appBarTitle = 'Plan'; // appBarTitle = 'Plan';
} // }
else if (_selectedIndex == 2) { // else if (_selectedIndex == 2) {
appBarTitle = 'Statistics'; // appBarTitle = 'Statistics';
} // }
else if (_selectedIndex == 3) { // else if (_selectedIndex == 3) {
appBarTitle = 'Career'; // appBarTitle = 'Career';
} // }
else if (_selectedIndex == 4) { // else if (_selectedIndex == 4) {
appBarTitle = 'More'; // appBarTitle = 'More';
} // }
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
toolbarHeight: 40, toolbarHeight: 40,
backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text("Case Study"), // 동적으로 변경된 AppBar 제목\
title: Text(appBarTitle), // 동적으로 변경된 AppBar 제목\
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: const Icon(Icons.notifications), icon: const Icon(Icons.notifications),
@@ -115,11 +112,7 @@ class _MyHomePageState extends State<MyHomePage> {
onTap: _navigateToProfileTab, onTap: _navigateToProfileTab,
customBorder: const CircleBorder(), customBorder: const CircleBorder(),
child: const CircleAvatar( child: const CircleAvatar(
backgroundColor: Colors.grey, backgroundImage: NetworkImage('https://manostmboy.github.io/temp/dumass.png'),
child: Icon(
Icons.person,
color: Colors.white,
),
), ),
), ),
), ),

View File

@@ -3,15 +3,40 @@ import 'package:flutter/material.dart';
class MorePage extends StatelessWidget { class MorePage extends StatelessWidget {
const MorePage({super.key}); const MorePage({super.key});
@override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Scaffold( return Scaffold(
body: Center( // appBar: AppBar(
child: Text( // title: const Text('My Page'),
'More Page', // ),
style: TextStyle(fontSize: 24), body: Column(
children: [
const ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage('https://manostmboy.github.io/temp/dumass.png'),
), ),
title: Text('김보통'),
subtitle: Text('normalkim@manos.kr'),
),
const Divider(),
// 메뉴 항목
_buildMenuItem(Icons.subscriptions, 'My Subscriptions'),
_buildMenuItem(Icons.bookmark, 'Saved Lessons'),
_buildMenuItem(Icons.card_membership, 'Certificates'),
_buildMenuItem(Icons.notifications, 'Notifications'),
],
), ),
); );
} }
Widget _buildMenuItem(IconData icon, String title) {
return ListTile(
leading: Icon(icon),
title: Text(title),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
// 탭 시 동작을 여기에 정의할 수 있어요.
},
);
}
} }

View File

@@ -2,32 +2,7 @@ import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import 'plan_page_detail.dart'; // <<< plan_page_detail.dart 파일을 import 합니다. import 'plan_page_detail.dart'; // <<< plan_page_detail.dart 파일을 import 합니다.
import 'common/data/case_study_plan.dart';
// HomePage에서 사용하던 CaseStudyPlan 모델을 PlanPage에서도 사용하거나,
// PlanPage에 필요한 별도의 데이터 모델을 정의할 수 있습니다.
// 여기서는 동일한 모델을 재사용한다고 가정합니다.
class CaseStudyPlan {
final String planId;
final String planTitle;
final String planTeacher;
final String thumbnail;
CaseStudyPlan({
required this.planId,
required this.planTitle,
required this.planTeacher,
required this.thumbnail,
});
factory CaseStudyPlan.fromJson(Map<String, dynamic> json) {
return CaseStudyPlan(
planId: json['casestudy lesson id'] ?? '아이디 없음',
planTitle: json['course_name'] ?? '제목 없음',
planTeacher: json['planTeacher'] ?? '',
thumbnail: json['course_thumbnail'] ?? '',
);
}
}
class PlanPage extends StatefulWidget { class PlanPage extends StatefulWidget {
const PlanPage({super.key}); const PlanPage({super.key});
@@ -49,7 +24,7 @@ class _PlanPageState extends State<PlanPage> {
Future<List<CaseStudyPlan>> _fetchPlanData() async { Future<List<CaseStudyPlan>> _fetchPlanData() async {
// HomePage와 동일한 API 주소를 사용합니다. // HomePage와 동일한 API 주소를 사용합니다.
final response = await http final response = await http
.get(Uri.parse('https://helloworld2-ad2uqhckxq-uc.a.run.app')); .get(Uri.parse('https://helloworld6-ad2uqhckxq-uc.a.run.app'));
if (response.statusCode == 200) { if (response.statusCode == 200) {
final Map<String, dynamic> decodedJson = json.decode(response.body); final Map<String, dynamic> decodedJson = json.decode(response.body);
@@ -94,27 +69,8 @@ class _PlanPageState extends State<PlanPage> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final plan = plans[index]; final plan = plans[index];
return InkWell( return InkWell(
onTap: () { // onTap: () {
if (plan.planId == 'ID 없음' || plan.planId.isEmpty) { // },
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('유효한 Plan ID가 없어 상세 페이지로 이동할 수 없습니다.')),
);
return;
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const PlanPageDetail(),
settings: RouteSettings(
// <<< Map 형태로 planId와 planTitle을 전달 >>>
arguments: {
'planId': plan.planId,
'planTitle': plan.planTitle,
},
),
),
);
},
child: Card( child: Card(
margin: margin:
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
@@ -124,28 +80,7 @@ class _PlanPageState extends State<PlanPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Text(
plan.planTitle,
style: const TextStyle(
fontSize: 17.0, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
const SizedBox(width: 8.0),
Text(
plan.planTeacher,
style: const TextStyle(
fontSize: 13.0, color: Colors.grey),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
const SizedBox(height: 8.0), const SizedBox(height: 8.0),
if (plan.thumbnail.isNotEmpty) if (plan.thumbnail.isNotEmpty)
@@ -153,7 +88,7 @@ class _PlanPageState extends State<PlanPage> {
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
child: Image.network( child: Image.network(
plan.thumbnail, plan.thumbnail,
height: 120, height: 200,
width: double.infinity, width: double.infinity,
fit: BoxFit.contain, fit: BoxFit.contain,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
@@ -188,6 +123,92 @@ class _PlanPageState extends State<PlanPage> {
child: const Center( child: const Center(
child: Text('No Image', child: Text('No Image',
style: TextStyle(color: Colors.grey)))), style: TextStyle(color: Colors.grey)))),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
plan.planTitle,
style: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
const SizedBox(height: 5.0),
// Text(
// plan.planTeacher,
// style: const TextStyle(
// fontSize: 15.0, color: Colors.black45),
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Text(
"Progress",
style: const TextStyle(
fontSize: 15.0, color: Colors.black45),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
const SizedBox(width: 5.0),
Text(
"50%",
style: const TextStyle(
fontSize: 15.0, color: Colors.black45),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
const SizedBox(height: 8.0),
LinearProgressIndicator(
value: 0.5, // TODO: Replace with actual progress value from plan object
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
const SizedBox(height: 16.0),
Column(
mainAxisAlignment: MainAxisAlignment.center, // 세로 중앙 정렬
crossAxisAlignment: CrossAxisAlignment.stretch, // 버튼이 가로로 꽉 차게 설정
children: [ElevatedButton(
onPressed: () {
// TODO: Implement navigation or action for Continue Learning
if (plan.planId == 'ID 없음' || plan.planId.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('유효한 Plan ID가 없어 상세 페이지로 이동할 수 없습니다.')),
);
return;
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const PlanPageDetail(),
settings: RouteSettings(
// <<< Map 형태로 planId와 planTitle을 전달 >>>
arguments: {
'planId': plan.planId,
'planTitle': plan.planTitle,
},
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xB91459DB),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(horizontal: 20),
),
child: const Text('Continue Learning',style: TextStyle(color: Colors.white)),
),],),
],
),
], ],
), ),
), ),

View File

@@ -4,36 +4,7 @@ import 'dart:convert';
import 'main.dart'; import 'main.dart';
import 'youtube_player_page.dart'; // YoutubePlayerPage import import 'youtube_player_page.dart'; // YoutubePlayerPage import
import 'common/widgets/custom_bottom_nav_bar.dart'; import 'common/widgets/custom_bottom_nav_bar.dart';
import 'common/data/plan_detail_item.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<String, dynamic> 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 { class PlanPageDetail extends StatefulWidget {
const PlanPageDetail({ const PlanPageDetail({
@@ -51,11 +22,12 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
late int _currentBottomNavIndex; late int _currentBottomNavIndex;
String? _selectedYoutubeUrl; String? _selectedYoutubeUrl;
PlanDetailItem? _selectedItem; // <<< 선택된 아이템을 저장할 변수 추가 PlanDetailItem? _selectedItem; // <<< 선택된 아이템을 저장할 변수 추가
final ScrollController _scrollController = ScrollController(); // 스크롤 컨트롤러 추가
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_currentBottomNavIndex = 0; _currentBottomNavIndex = 1;
} }
@override @override
@@ -124,6 +96,12 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
); );
} }
@override
void dispose() {
_scrollController.dispose(); // 스크롤 컨트롤러 해제
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -191,6 +169,7 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
final item = details[index]; final item = details[index];
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_scrollController.jumpTo(0); // 스크롤 맨 위로 이동
setState(() { setState(() {
_selectedItem = item; _selectedItem = item;
if (item.lessonUrl.isNotEmpty && if (item.lessonUrl.isNotEmpty &&
@@ -275,7 +254,7 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
item.lessonTag, item.lessonName,
style: const TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500), fontWeight: FontWeight.w500),
@@ -292,6 +271,7 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
), ),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _scrollController, // 스크롤 컨트롤러 연결
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -337,7 +317,7 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
), ),
), ),
IconButton( 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: () { onPressed: () {
if (_selectedYoutubeUrl != null && _selectedYoutubeUrl!.isNotEmpty) { if (_selectedYoutubeUrl != null && _selectedYoutubeUrl!.isNotEmpty) {
Navigator.push( Navigator.push(

View File

@@ -6,7 +6,9 @@ import FlutterMacOS
import Foundation import Foundation
import flutter_inappwebview_macos import flutter_inappwebview_macos
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
} }

View File

@@ -1,6 +1,30 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
archive:
dependency: transitive
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
url: "https://pub.dev"
source: hosted
version: "4.0.7"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@@ -25,6 +49,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -41,6 +81,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -57,6 +113,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.3" version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -126,6 +190,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.6.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
url: "https://pub.dev"
source: hosted
version: "0.13.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -134,6 +206,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_native_splash:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "8321a6d11a8d13977fa780c89de8d257cce3d841eecfb7a4cadffcc4f12d82dc"
url: "https://pub.dev"
source: hosted
version: "2.4.6"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -144,6 +224,22 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -160,6 +256,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -168,6 +272,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.20.2" version: "0.20.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -232,6 +344,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" 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"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -240,6 +416,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -301,6 +485,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -325,6 +517,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
youtube_player_flutter: youtube_player_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -335,4 +551,4 @@ packages:
version: "9.1.1" version: "9.1.1"
sdks: sdks:
dart: ">=3.8.1 <4.0.0" dart: ">=3.8.1 <4.0.0"
flutter: ">=3.24.0" flutter: ">=3.27.0"

View File

@@ -6,8 +6,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application. # The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43 # A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +. # followed by an optional build number separated by a +.# Both the version and the builder number may be overridden in flutter
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively. # build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode. # In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning # Read more about Android versioning at https://developer.android.com/studio/publish/versioning
@@ -37,10 +36,13 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
google_fonts: ^6.2.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_native_splash: ^2.3.11 # 최신 버전으로 추가
flutter_launcher_icons: ^0.13.1 # 이 줄을 추가합니다.
# The "flutter_lints" package below contains a set of recommended lints to # The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is # encourage good coding practices. The lint set provided by the package is
@@ -61,9 +63,8 @@ flutter:
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
# assets: assets:
# - images/a_dot_burr.jpeg - assets/splash/ # 스플래시 이미지 폴더 추가
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images # https://flutter.dev/to/resolution-aware-images
@@ -90,3 +91,16 @@ flutter:
# #
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package # see https://flutter.dev/to/font-from-package
flutter_native_splash:
color: "#ffffff"
image: assets/splash/splash.png
android: true
ios: true
web: false
flutter_launcher_icons:
android: "launcher_icon"
ios: true
image_path: "assets/icon/icon.png"
min_sdk_android: 21 # android min SDK