Compare commits

...

7 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
70 changed files with 1004 additions and 298 deletions

View File

@@ -3,7 +3,7 @@
<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"

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,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

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../home_page.dart'; // CaseStudyPlan 모델을 import 합니다. import '../data/upcoming_study.dart'; // UpcomingStudy 모델을 import 합니다.
class UpcomingClassCard extends StatelessWidget { class UpcomingClassCard extends StatelessWidget {
final CaseStudyPlan plan; final UpcomingStudy plan;
final VoidCallback onTap; final VoidCallback onTap;
const UpcomingClassCard({ const UpcomingClassCard({
@@ -14,39 +14,18 @@ class UpcomingClassCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
width: 300, // 각 항목의 너비를 지정합니다. width: MediaQuery.of(context).size.width / 2, // 화면 너비의 절반으로 설정
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
child: Card( child: Card(
margin: const EdgeInsets.only(right: 12.0), // 항목 간의 간격 조정 margin: const EdgeInsets.only(right: 12.0), // 항목 간의 간격 조정
elevation: 2.0, elevation: 2.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5.0)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(5.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ 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) if (plan.thumbnail.isNotEmpty)
Expanded( Expanded(
child: ClipRRect( child: ClipRRect(
@@ -54,7 +33,7 @@ class UpcomingClassCard extends StatelessWidget {
child: Image.network( child: Image.network(
plan.thumbnail, plan.thumbnail,
width: double.infinity, width: double.infinity,
fit: BoxFit.cover, fit: BoxFit.contain,
loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) return child;
return Center( return Center(
@@ -78,6 +57,30 @@ class UpcomingClassCard extends StatelessWidget {
child: const Center(child: Text('No Image', style: TextStyle(color: Colors.grey))) 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,33 +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:csp2/common/widgets/upcoming_class_card.dart'; import 'common/widgets/upcoming_class_card.dart';
// import 'package:flutter_native_timezone/flutter_native_timezone.dart'; // 주석 처리된 상태 유지 import 'common/widgets/course_card.dart';
import '../plan_page.dart'; // PlanPage import import 'common/data/course.dart'; // Course 클래스 import
import 'plan_page.dart'; // PlanPage import
// CaseStudyPlan 클래스 (변경 없음) import 'common/widgets/now_study_class_card.dart';
class CaseStudyPlan { import 'common/data/upcoming_study.dart';
final String planId; import 'common/data/new_study.dart';
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 RecommendPlan { class RecommendPlan {
@@ -68,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;
@@ -92,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();
@@ -110,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) {
@@ -119,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(
@@ -127,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}');
} }
} }
@@ -287,7 +311,8 @@ class _HomePageState extends State<HomePage> {
} }
Widget _buildHomeContent() { Widget _buildHomeContent() {
return Column( return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Padding( Padding(
@@ -311,12 +336,12 @@ class _HomePageState extends State<HomePage> {
), ),
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)),
), ),
SizedBox( SizedBox(
height: 250, // 가로 리스트의 높이를 지정합니다. height: 200, // 가로 리스트의 높이를 줄입니다.
child: FutureBuilder<List<CaseStudyPlan>>( child: FutureBuilder<List<UpcomingStudy>>(
future: _caseStudyPlans, 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());
@@ -331,15 +356,18 @@ class _HomePageState extends State<HomePage> {
final plans = snapshot.data!; final plans = snapshot.data!;
return ListView.builder( return ListView.builder(
scrollDirection: Axis.horizontal, // 가로 스크롤로 변경 scrollDirection: Axis.horizontal, // 가로 스크롤로 변경
padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 8.0), 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 UpcomingClassCard( return Align(
alignment: Alignment.topCenter, // 항목을 상단 중앙에 정렬
child: UpcomingClassCard(
plan: plan, plan: plan,
onTap: () { onTap: () {
widget.onNavigateToPlanTab(1); widget.onNavigateToPlanTab(1);
}, },
),
); );
}, },
); );
@@ -347,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);
},
);
},
);
}
},
),
], ],
),
); );
} }
@@ -369,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

@@ -112,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({
@@ -283,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),

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:
@@ -49,6 +89,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" 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:
@@ -142,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:
@@ -150,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
@@ -168,6 +232,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.2.1" 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:
@@ -184,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:
@@ -192,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:
@@ -304,6 +392,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@@ -320,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
@@ -381,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:
@@ -413,6 +525,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" 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:

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
@@ -42,6 +41,8 @@ dependencies:
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
@@ -62,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
@@ -91,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