import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../models/article.dart';
class ArticleScreen extends StatefulWidget {
@override
_ArticleScreenState createState() => _ArticleScreenState();
}
class _ArticleScreenState extends State<ArticleScreen> {
late Future<List<Article>> _articles;
@override
void initState() {
super.initState();
_articles = fetchArticles();
}
Future<List<Article>> fetchArticles() async {
final response =
await http.get(Uri.parse('<http://127.0.0.1:8000/community/>'));
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => Article.fromJson(json)).toList();
} else {
throw Exception('Failed to load articles');
}
}
void _showArticleOptions(int articleId) {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(Icons.edit),
title: Text('Edit'),
onTap: () {
Navigator.pushNamed(context, '/edit_article',
arguments: articleId);
},
),
ListTile(
leading: Icon(Icons.delete),
title: Text('Delete'),
onTap: () {
_deleteArticle(articleId);
},
),
],
);
},
);
}
Future<void> _deleteArticle(int articleId) async {
final response = await http
.delete(Uri.parse('<http://127.0.0.1:8000/community/$articleId/>'));
if (response.statusCode == 204) {
setState(() {
_articles = fetchArticles();
});
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Article deleted')));
} else {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Failed to delete article')));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Community Posts'),
),
body: FutureBuilder<List<Article>>(
future: _articles,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No posts available'));
}
final articles = snapshot.data!;
return ListView.separated(
separatorBuilder: (context, index) =>
Divider(color: Colors.grey[300]),
itemCount: articles.length,
itemBuilder: (context, index) {
final article = articles[index];
return ListTile(
contentPadding: EdgeInsets.all(16),
title: Text(article.user),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(article.content),
if (article.image != null) Image.network(article.image!),
SizedBox(height: 8),
Text('${article.createdAt} | ${article.updatedAt}'),
Row(
children: [
Icon(Icons.comment, size: 16),
SizedBox(width: 4),
Text('${article.commentCount}'),
SizedBox(width: 16),
Icon(Icons.thumb_up, size: 16),
SizedBox(width: 4),
Text('${article.likesCount}'),
],
),
],
),
trailing: IconButton(
icon: Icon(Icons.more_vert),
onPressed: () => _showArticleOptions(article.articleId),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.pushNamed(context, '/create_article');
},
child: Icon(Icons.add),
tooltip: 'Create Article',
),
);
}
}
import 'package:flutter/material.dart';
import 'package:fithubfe/screens/article_screen.dart';
import 'package:fithubfe/screens/comment_screen.dart';
import 'package:fithubfe/screens/create_article_screen.dart';
import 'package:fithubfe/screens/edit_article_screen.dart';
import 'package:fithubfe/screens/signup_screen.dart';
import 'package:fithubfe/screens/user_profile_screen.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: '/signup',
onGenerateRoute: (settings) {
// 여기서 경로를 다이나믹하게 처리
switch (settings.name) {
case '/signup':
return MaterialPageRoute(builder: (_) => SignupScreen());
case '/':
return MaterialPageRoute(builder: (_) => ArticleScreen());
case '/create_article':
return MaterialPageRoute(builder: (_) => CreateArticleScreen());
case '/edit_article':
final articleId = settings.arguments as int;
return MaterialPageRoute(
builder: (_) => EditArticleScreen(articleId: articleId),
);
case '/comments':
final articleId = settings.arguments as int;
return MaterialPageRoute(
builder: (_) => CommentScreen(articleId: articleId),
);
case '/profile':
return MaterialPageRoute(builder: (_) => UserProfileScreen(id: 4));
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(child: Text('404 Not Found')),
),
);
}
},
);
}
}
// lib/screens/login_screen.dart
import 'package:fithubfe/widgets/custom_textfield.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../services/api_service.dart';
import '../widgets/custom_textfield.dart';
import '../widgets/toggle_button.dart';
class LoginScreen extends StatefulWidget {
final VoidCallback onSignUpPressed;
LoginScreen({required this.onSignUpPressed});
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
final ApiService _apiService = ApiService();
Future<void> _login() async {
if (_formKey.currentState?.validate() ?? false) {
final email = _emailController.text;
final password = _passwordController.text;
final response = await _apiService.login(email, password);
if (response.containsKey('access')) {
// Handle successful login
Navigator.pushReplacementNamed(
context, '/home'); // Navigate to home screen
} else {
// Handle login failure
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('로그인 실패: ${response['message']}')));
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('로그인'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomTextField(
hintText: '이메일',
controller: _emailController,
),
SizedBox(height: 16),
CustomTextField(
hintText: '비밀번호',
controller: _passwordController,
obscureText: true,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _login,
child: Text('로그인'),
),
SizedBox(height: 16),
ToggleButton(
text: '처음이에요? 회원가입',
onPressed: widget.onSignUpPressed,
),
],
),
),
),
);
}
}
// lib/screens/signup_screen.dart
import 'package:fithubfe/widgets/custom_textfield.dart';
import 'package:flutter/material.dart';
import '../services/api_service.dart';
import '../widgets/custom_textfield.dart';
import '../widgets/toggle_button.dart';
class SignUpScreen extends StatefulWidget {
final VoidCallback onLoginPressed;
SignUpScreen({required this.onLoginPressed});
@override
_SignUpScreenState createState() => _SignUpScreenState();
}
class _SignUpScreenState extends State<SignUpScreen> {
final _emailController = TextEditingController();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
final ApiService _apiService = ApiService();
Future<void> _signup() async {
if (_formKey.currentState?.validate() ?? false) {
final email = _emailController.text;
final username = _usernameController.text;
final password = _passwordController.text;
final response = await _apiService.signup(email, password, username);
if (response.containsKey('access')) {
// Handle successful signup
Navigator.pushReplacementNamed(
context, '/home'); // Navigate to home screen
} else {
// Handle signup failure
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('회원가입 실패: ${response['message']}')));
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('회원가입'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomTextField(
hintText: '이메일',
controller: _emailController,
),
SizedBox(height: 16),
CustomTextField(
hintText: '사용자 이름',
controller: _usernameController,
),
SizedBox(height: 16),
CustomTextField(
hintText: '비밀번호',
controller: _passwordController,
obscureText: true,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _signup,
child: Text('회원가입'),
),
SizedBox(height: 16),
ToggleButton(
text: '계정이 있어요? 로그인',
onPressed: widget.onLoginPressed,
),
],
),
),
),
);
}
}
전문가 구분 o
import 'dart:convert';
import 'package:fithubfe/screens/comment_screen.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import '../models/article.dart';
import '../models/user.dart';
import 'package:flutter/material.dart';
class ArticleScreen extends StatefulWidget {
@override
_ArticleScreenState createState() => _ArticleScreenState();
}
class _ArticleScreenState extends State<ArticleScreen> {
List<Article> _articles = [];
List<Article> _expertArticles = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
_fetchArticles();
}
Future<void> _fetchArticles() async {
setState(() {
_isLoading = true;
});
// API 호출을 통해 게시물 불러오기
// 일반 게시물과 전문가 게시물을 구분하여 리스트에 추가
final response =
await http.get(Uri.parse('<http://127.0.0.1:8000/community/>'));
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
setState(() {
_articles = data.map((item) => Article.fromJson(item)).toList();
_expertArticles =
_articles.where((article) => article.user.isExpert).toList();
_isLoading = false;
});
} else {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to load articles')),
);
}
}
Future<void> _likeArticle(int articleId, bool isLikedByUser) async {
final response = await http.post(
Uri.parse('<http://127.0.0.1:8000/community/$articleId/like/>'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
);
if (response.statusCode == 200) {
setState(() {
_articles = _fetchArticles() as List<Article>;
});
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Article liked')));
} else {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Failed to like article')));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Articles'),
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: ListView(
children: [
if (_expertArticles.isNotEmpty) _buildExpertArticles(),
_buildGeneralArticles(),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await Navigator.pushNamed(context, '/create_article');
if (result == true) {
_fetchArticles(); // 새 게시물이 추가된 후 데이터를 다시 불러옴
}
},
child: Icon(Icons.add),
tooltip: 'Create Article',
),
);
}
Widget _buildExpertArticles() {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
elevation: 5,
margin: EdgeInsets.all(10.0),
child: Container(
height: 200, // 카드 높이 설정
child: PageView.builder(
itemCount: _expertArticles.length,
itemBuilder: (context, index) {
final article = _expertArticles[index];
return GestureDetector(
onTap: () {
Navigator.pushNamed(
context,
'/article_detail',
arguments: article.articleId,
);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
article.user.username,
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Text(article.content),
if (article.image != null) Image.network(article.image!),
],
),
),
);
},
),
),
);
}
Widget _buildGeneralArticles() {
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: _articles.length,
itemBuilder: (context, index) {
final article = _articles[index];
return Column(
children: [
ListTile(
leading: CircleAvatar(
backgroundImage: article.user.profileImage != null
? NetworkImage(article.user.profileImage)
: AssetImage('assets/default_profile.png'),
),
title: Text(article.user.username),
subtitle: Text(
'@${article.user.id} · ${_calculateElapsedTime(article.createdAt)}'),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(article.content),
if (article.image != null)
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Image.network(article.image!),
),
SizedBox(height: 10),
Text('Updated at: ${_formatDate(article.updatedAt)}'),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(Icons.comment),
onPressed: () {
_showCommentBottomSheet(context, article.articleId);
},
),
Text('${article.commentCount}'),
IconButton(
icon: Icon(
article.isLikedByUser
? Icons.favorite
: Icons.favorite_border,
color: article.isLikedByUser ? Colors.red : null,
),
onPressed: () => _likeArticle(
article.articleId, article.isLikedByUser),
),
Text('${article.likesCount}'),
],
),
],
),
),
Divider(),
],
);
},
);
}
String _calculateElapsedTime(DateTime createdAt) {
final difference = DateTime.now().difference(createdAt);
if (difference.inDays > 0) return '${difference.inDays}d';
if (difference.inHours > 0) return '${difference.inHours}h';
return '${difference.inMinutes}m';
}
String _formatDate(DateTime date) {
return DateFormat('yyyy-MM-dd HH:mm').format(date);
}
void _showCommentBottomSheet(BuildContext context, int articleId) {
showModalBottomSheet(
context: context,
builder: (context) {
return CommentScreen(articleId: articleId);
},
);
}
}
shared perferences 없는 버전
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class LoginScreen extends StatefulWidget {
final VoidCallback onSignUpPressed;
const LoginScreen({super.key, required this.onSignUpPressed});
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
Future<void> _login() async {
if (_formKey.currentState?.validate() ?? false) {
final email = _emailController.text;
final password = _passwordController.text;
try {
final response = await http.post(
Uri.parse('<http://10.0.2.2:8000/users/login/>'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'email': email,
'password': password,
}),
);
if (response.statusCode == 200) {
const snackBar = SnackBar(content: Text('로그인 성공'));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.pushReplacementNamed(context, '/home');
}
} else {
final errorMessage = json.decode(response.body)['message'];
final snackBar = SnackBar(content: Text('Error: $errorMessage'));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
} catch (e) {
final snackBar = SnackBar(content: Text('Exception: ${e.toString()}'));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('로그인'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: '이메일'),
validator: (value) {
if (value == null || value.isEmpty) {
return '이메일을 입력해 주세요';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: '비밀번호'),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return '비밀번호를 입력해 주세요';
}
if (value.length < 8) {
return '비밀번호는 최소 8자리 이상이어야 합니다';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _login,
child: const Text('로그인'),
),
TextButton(
onPressed: widget.onSignUpPressed,
child: const Text('처음이에요? 회원가입'),
),
],
),
),
),
);
}
}