네이버 이메일 클론코딩
https://sfacassignmentchallenge-default-rtdb.europe-west1.firebasedatabase.app/.json
기능 목록
이메일의 목록을 사진 디자인과 같이 구현합니다.
이메일이 도착한 날이 오늘이라면 오늘, 어제라면 어제, 올해와 년도가 같다면 MM.dd , 년도가 다르다면 yyyy.MM.dd 로 보여져야합니다.
각 리스트는 swipe가 가능하며, 우측 스와이프로 삭제를 한다면, 휴지통으로 분류가 되며 현재 화면에는 보여지지 않습니다.
우측 하단 FAB은 작성하기 버튼이지만, 휴지통 아이콘을 넣어주세요. 클릭하면 휴지통에 추가된 리스트들이 보이는 화면을 구현해주세요.
우측상단에서 메일검색이 가능하며, 메일검색을 누르면 두번째 사진, 검색하는 화면이 등장하고 최근에 검색한 목록들이 등장합니다.
메일검색은 보낸사람을 기준으로 검색이 가능하게 합니다.
우측 상단 시계버튼을 누르면 메시지를 받은 최신순으로 정렬하게되고, 한번 더 누르게되면 오래된 순으로 보여지게 됩니다.
pull to refresh를 한다면 이메일이 새로 업데이트가되며, 삭제한 이메일은 보이지 않습니다.
최상단 앱바를 클릭하면 자연스럽게 최상단으로 올라가게 됩니다.
import 'package:flutter/material.dart';
import 'page/main_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: MailPage(),
);
}
}
import 'package:intl/intl.dart';
class Email {
int emailNo;
String from;
String title;
String detail;
DateTime sendDate;
bool isDeleted;
Email({
required this.emailNo,
required this.from,
required this.title,
required this.detail,
required this.sendDate,
this.isDeleted = false,
});
factory Email.fromMap(Map<String, dynamic> map) {
return Email(
emailNo: map['emailNo'],
from: map['from'],
title: map['title'],
detail: map['detail'],
sendDate: DateFormat('yyyy.MM.dd').parse(map['sendDate']),
);
}
String toString() => 'Mail($from, 삭제($isDeleted))';
}
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../model/Email.dart';
class MailItem extends StatefulWidget {
const MailItem({Key? key, required this.email}) : super(key: key);
final Email email;
State<MailItem> createState() => _MailItemState();
}
class _MailItemState extends State<MailItem> {
var now = DateTime.now();
String dateText (DateTime dateTime) {
if (now.difference(dateTime).inDays == 0) {
return '오늘';
} else if (now.difference(dateTime).inDays == 1) {
return '어제';
} else if (now.year == dateTime.year) {
return DateFormat('MM.dd').format(dateTime);
}
return DateFormat('yyyy.MM.dd').format(dateTime);
}
Widget build(BuildContext context) {
String sendDate = dateText(widget.email.sendDate);
return Padding(
padding: const EdgeInsets.only(top: 8.0, left: 8, right: 8),
child: Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 8,
child: CircleAvatar(
backgroundColor: Colors.green,
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(widget.email.from, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),),
),
Spacer(),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(sendDate),
),
Icon(Icons.star, color: Colors.grey,)
],
),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Row(
children: [
Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(30),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
child: Text('TO', style: TextStyle(color: Colors.white, fontSize: 14),),
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(widget.email.title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
maxLines: 1,),
),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16, right: 8),
child: Text(widget.email.detail, style: TextStyle(fontSize: 18, color: Colors.grey),
overflow: TextOverflow.ellipsis,
maxLines: 1,),
),
],
),
),
),
);
}
}
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'bin_page.dart';
import 'search_page.dart';
import '../model/Email.dart';
import '../widget/mail_item.dart';
class MailPage extends StatefulWidget {
const MailPage({Key? key}) : super(key: key);
State<MailPage> createState() => _MailPageState();
}
class _MailPageState extends State<MailPage> {
Dio dio = Dio();
RefreshController refreshController = RefreshController(initialRefresh: false);
ScrollController scrollController = ScrollController();
List<Email> allEmails = []; // 모든 메일 리스트
List<Email> deletedEmails = []; // 삭제된 메일 리스트
List<Email> notDeletedEmails = []; // 삭제되지 않은 메일 리스트
bool isSortAscending = false;
void initState() {
super.initState();
getData();
}
// 데이터 가져오기
void getData() async {
final res = await dio.get('https://sfacassignmentchallenge-default-rtdb.europe-west1.firebasedatabase.app/.json');
List<dynamic> emailList = res.data['emails'];
allEmails = emailList.map((e) => Email.fromMap(e)).toList();
notDeletedEmails = allEmails;
}
// 새로고침
void onRefresh() async {
setState(() {});
refreshController.refreshCompleted();
}
// 시간순으로 정렬
void sortEmails() {
setState(() {
isSortAscending = !isSortAscending;
notDeletedEmails.sort((a, b) {
if (isSortAscending) {
return a.sendDate.compareTo(b.sendDate);
} else {
return b.sendDate.compareTo(a.sendDate);
}
});
});
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[300],
appBar: AppBar(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
centerTitle: true,
title: GestureDetector(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('프로모션 '),
Container(
width: 8,
child: CircleAvatar(
backgroundColor: Colors.green,
),
),
Text(' ${notDeletedEmails.length}', style: TextStyle(color: Colors.green),),
],
),
onTap: () {
scrollController.animateTo(
0,
duration: Duration(milliseconds: 300),
curve: Curves.linear,
);
}
),
leading: Icon(Icons.menu),
actions: [
IconButton(
icon: Icon(Icons.access_time),
onPressed: sortEmails,
)
],
),
body: FutureBuilder(
future: dio.get('https://sfacassignmentchallenge-default-rtdb.europe-west1.firebasedatabase.app/.json'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
log('deleted: $deletedEmails');
log('notDeleted: $notDeletedEmails');
log('all: $allEmails');
return SmartRefresher(
controller: refreshController,
enablePullDown: true,
onRefresh: onRefresh,
header: WaterDropHeader(),
child: ListView.builder(
controller: scrollController,
itemCount: notDeletedEmails.length + 1,
itemBuilder: (context, index) {
// 검색
if (index == 0) {
return Padding(
padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
child: GestureDetector(
child: Container(
color: Colors.grey[350],
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search, color: Colors.grey,),
Text('메일 검색', style: TextStyle(color: Colors.grey),)
],
),
)
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SearchPage(emailList: notDeletedEmails)),
);
},
),
);
}
Email email = notDeletedEmails[index-1];
print(email);
return Dismissible(
key: UniqueKey(),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30.0),
child: Icon(Icons.delete_outline, color: Colors.white,),
),
),
onDismissed: (direction) {
email.isDeleted = true;
print('${email} : ${email.isDeleted}');
deletedEmails.add(email);
notDeletedEmails = allEmails.where((email) => !email.isDeleted).toList();
},
child: MailItem(email: email)
);
}
),
);
} else {
return Center(child: CircularProgressIndicator(),);
}
},
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.green,
child: Icon(Icons.delete_outline),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => BinPage(deletedEmails: deletedEmails)),
);
},
),
);
}
}
import 'package:first_app/homework/week5/week5challenge/widget/mail_item.dart';
import 'package:flutter/material.dart';
import '../model/Email.dart';
class BinPage extends StatefulWidget {
const BinPage({Key? key, required this.deletedEmails}) : super(key: key);
final List<Email> deletedEmails;
State<BinPage> createState() => _BinPageState();
}
class _BinPageState extends State<BinPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
title: Text('휴지통'),
centerTitle: true,
),
body: ListView.builder(
itemCount: widget.deletedEmails.length,
itemBuilder: (context, index) {
return MailItem(email: widget.deletedEmails[index]);
}
),
);
}
}
import 'package:first_app/homework/week5/week5challenge/page/search_result_page.dart';
import 'package:flutter/material.dart';
import '../model/Email.dart';
class SearchPage extends StatefulWidget {
const SearchPage({Key? key, required this.emailList}) : super(key: key);
final List<Email> emailList;
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
title: Text('메일 검색'),
// TextField(
// decoration: InputDecoration(
// fillColor: Colors.grey[200],
// filled: true,
// border: OutlineInputBorder(
// borderSide: BorderSide.none,
// ),
// hintText: '메일 검색'
// ),
// ),
// actions: [
// TextButton(
// onPressed: (){},
// child: Text('상세'),
// )
// ],
),
body: ListView.builder(
itemCount: widget.emailList.length,
itemBuilder: (context, index) {
return GestureDetector(
child: ListTile(
title: Text('${widget.emailList[index].from}')
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SearchResultPage(email: widget.emailList[index]))
);
},
);
}
),
);
}
}
import 'package:first_app/homework/week5/week5challenge/widget/mail_item.dart';
import 'package:flutter/material.dart';
import '../model/Email.dart';
class SearchResultPage extends StatelessWidget {
const SearchResultPage({Key? key, required this.email}) : super(key: key);
final Email email;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
title: Text('검색 결과'),
),
body: Column(
children: [
MailItem(email: email)
],
),
);
}
}

- 7개 이상의 News Article을 포함하도록 하시오.
- 이 때 사용되는 뉴스 제목을 포함한 모든 데이터는 다음과 같다.
```
Journals adopt AI to spot duplicated images in manuscripts
/articles/d41586-021-03830-7
Smriti Mallapaty
23 Dec 2021
Fatal lab explosion in China highlights wider safety fears
/articles/d41586-021-03589-x
Andrew Silver
22 Dec 2021
Journals adopt AI to spot duplicated images in manuscripts
/articles/d41586-021-03807-6
Richard Van Noorden
21 Dec 2021
```
- 3개만 제공되며, 그 외에 본인이 원하는 기사는 다음의 URL 에서 수집할 수 있도록 한다.
- [https://www.nature.com/nature/articles?type=news](https://www.nature.com/nature/articles?type=news&year=2021)
import 'package:flutter/material.dart';
import 'article_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: ArticlePage()
);
}
}
class Article {
String title; // 제목
String img; // 이미지
String url; // 링크
String author; // 작성자
DateTime createdAt; // 작성일
String type; // 타입
Article({
required this.title,
required this.url,
required this.img,
required this.author,
required this.createdAt,
required this.type});
Article.fromMap(Map<String, dynamic> map)
: title = map['title'],
url = 'https://www.nature.com${map['url']}',
img = map['img'],
author = map['author'],
createdAt = map['createdAt'],
type = map['type'];
}
List<Map<String, dynamic>> articleData = [
{
'title': 'Journals adopt AI to spot duplicated images in manuscripts',
'url': '/articles/d41586-021-03830-7',
'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03655-4/d41586-021-03655-4_19975876.jpg?as=webp',
'author': 'Smriti Mallapaty',
'createdAt': DateTime(2021, 12, 23),
'type': 'News',
},
{
'title': 'Fatal lab explosion in China highlights wider safety fears',
'url': '/articles/d41586-021-03589-x',
'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03830-7/d41586-021-03830-7_19972468.jpg?as=webp',
'author': 'Andrew Silver',
'createdAt': DateTime(2021, 12, 22),
'type': 'News',
},
{
'title': 'Journals adopt AI to spot duplicated images in manuscripts',
'url': '/articles/d41586-021-03807-6',
'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03589-x/d41586-021-03589-x_19914054.jpg?as=webp',
'author': 'Richard Van Noorden',
'createdAt': DateTime(2021, 12, 21),
'type': 'News',
},
{
'title': 'Webb telescope blasts off successfully — launching a new era in astronomy',
'url': '/articles/d41586-021-03655-4',
'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03807-6/d41586-021-03807-6_19969476.jpg?as=webp',
'author': 'Alexandra Witze',
'createdAt': DateTime(2021, 12, 20),
'type': 'News',
},
{
'title': 'Journals adopt AI to spot duplicated images in manuscripts',
'url': '/articles/d41586-021-03807-6',
'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03589-x/d41586-021-03589-x_19914054.jpg?as=webp',
'author': 'Richard Van Noorden',
'createdAt': DateTime(2021, 12, 20),
'type': 'News',
},
{
'title': 'Webb telescope blasts off successfully — launching a new era in astronomy',
'url': '/articles/d41586-021-03655-4',
'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03807-6/d41586-021-03807-6_19969476.jpg?as=webp',
'author': 'Alexandra Witze',
'createdAt': DateTime(2021, 12, 20),
'type': 'News',
},
{
'title': 'Journals adopt AI to spot duplicated images in manuscripts',
'url': '/articles/d41586-021-03830-7',
'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03655-4/d41586-021-03655-4_19975876.jpg?as=webp',
'author': 'Smriti Mallapaty',
'createdAt': DateTime(2021, 12, 23),
'type': 'News',
},
];
import 'data.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'Article.dart';
import 'article_item.dart';
class ArticlePage extends StatefulWidget {
const ArticlePage({Key? key}) : super(key: key);
State<ArticlePage> createState() => _ArticlePageState();
}
class _ArticlePageState extends State<ArticlePage> {
Widget build(BuildContext context) {
List<Article> articleList = articleData.map((e) => Article.fromMap(e)).toList();
print(articleList.toString());
return Scaffold(
body: SafeArea(
child: ListView.separated(
itemCount: articleList.length,
itemBuilder: (context, index) {
var article = articleList[index];
return GestureDetector(
child: ArticleItem(article: article),
onTap: () {
launchUrlString(article.url);
},
);
},
separatorBuilder: (context, index) {
return Divider();
},
),
),
);
}
}
import 'package:intl/intl.dart';
import 'Article.dart';
import 'package:flutter/material.dart';
class ArticleItem extends StatelessWidget {
const ArticleItem({Key? key, required this.article}) : super(key: key);
final Article article;
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 240,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
article.title,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, decoration: TextDecoration.underline,)
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(article.author),
),
Row(
children: [
Text(article.type, style: TextStyle(fontWeight: FontWeight.bold),),
Text(' | ${DateFormat('dd MMM yyyy').format(article.createdAt)}'),
],
)
],
),
),
Container(
width: 100,
child: Image.network(article.img)
)
],
),
);
}
}

**lib/model/userdata.dart**
```dart
class UserData {
///이곳 채우기.
}
```
**lib/assignment23_page.dart**
```dart
class Assignment23 extends StatefulWidget {
const Assignment23({super.key});
@override
State<Assignment23> createState() => _Assignment23State();
}
class _Assignment23State extends State<Assignment23> {
Future<Map<String, dynamic>> getJsonData() async {
///이곳 채우기.
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('23일차 과제')),
body: Center(
child: FutureBuilder(
future: getJsonData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CupertinoActivityIndicator();
}
if (!snapshot.hasData) return const Text("데이터가 없습니다");
Map<String, dynamic> data = snapshot.data as Map<String, dynamic>;
List<dynamic> users = data['users'];
List<dynamic> dismissDuplicatedUsers = _dismissDuplicatedData(users);
return ListView.separated(
itemBuilder: (context, index) {
UserData userData = UserData.fromMap(dismissDuplicatedUsers[index]);
return _buildItemWidget(userData);
},
separatorBuilder: (context, index) {
return const Divider();
},
itemCount: dismissDuplicatedUsers.length,
);
}
)
),
);
}
Widget _buildItemWidget(UserData userData) {
return ListTile(
leading: Image.network(userData.imageUrl),
title: Text('${userData.firstName} ${userData.lastName}'),
subtitle: Text('${userData.phoneNumber}'),
);
}
List<dynamic> _dismissDuplicatedData(List<dynamic> data) {
///이곳 채우기.
}
}
```
import 'package:flutter/material.dart';
import 'assignment23_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Assignment23()
);
}
}
class UserData {
int userId;
String firstName;
String lastName;
String imageUrl;
String phoneNumber;
UserData({
required this.userId,
required this.firstName,
required this.lastName,
required this.imageUrl,
required this.phoneNumber,
});
UserData.fromMap(Map<String, dynamic> map)
: userId = map['userId'],
firstName = map['firstName'],
lastName = map['lastName'],
imageUrl = map['imageUrl'],
phoneNumber = map['phoneNumber'];
Map<String, dynamic> toMap() => {
'userId': userId,
'firstName': firstName,
'lastName': lastName,
'imageUrl': imageUrl,
'phoneNumber': phoneNumber,
};
bool operator ==(Object other) => other is UserData && userId == other.userId;
}
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'userdata.dart';
class Assignment23 extends StatefulWidget {
const Assignment23({super.key});
State<Assignment23> createState() => _Assignment23State();
}
class _Assignment23State extends State<Assignment23> {
Future<Map<String, dynamic>> getJsonData() async {
Dio dio = Dio();
String url = 'https://sfacassignment23-default-rtdb.europe-west1.firebasedatabase.app/.json';
return await dio.get(url).then((value) => value.data);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('23일차 과제')),
body: Center(
child: FutureBuilder(
future: getJsonData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (!snapshot.hasData) return const Text("데이터가 없습니다");
Map<String, dynamic> data = snapshot.data as Map<String, dynamic>;
List<dynamic> users = data['users'];
List<dynamic> dismissDuplicatedUsers = _dismissDuplicatedData(users);
return ListView.separated(
itemBuilder: (context, index) {
UserData userData = UserData.fromMap(dismissDuplicatedUsers[index]);
return _buildItemWidget(userData);
},
separatorBuilder: (context, index) {
return const Divider();
},
itemCount: dismissDuplicatedUsers.length,
);
}
)
),
);
}
Widget _buildItemWidget(UserData userData) {
return ListTile(
leading: Image.network(userData.imageUrl),
title: Text('${userData.firstName} ${userData.lastName}'),
subtitle: Text('${userData.phoneNumber}'),
);
}
List<dynamic> _dismissDuplicatedData(List<dynamic> data) {
// 전체 UserData 리스트
List<UserData> userList = data.map((e) => UserData.fromMap(e)).toList();
// 중복 제거 UserData 리스트
List<UserData> result = [];
for (var user in userList) {
// result에 user랑 같은 UserData 없으면 result에 user 추가
if (!result.any((e) => e == user)) {
result.add(user);
}
}
return result.map((e) => e.toMap()).toList();
}
}
도전하기 코드가 좀 더러운 것 같습니다..........................................
구현이 덜 된 5번 검색기능은 나중에 시간 날 때 꼭!!! 다시 해결해보겠ㅅ브니다..........