Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion myapp/.flutter-plugins-dependencies
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/codespace/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/codespace/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/codespace/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/codespace/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/codespace/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]}],"date_created":"2025-07-03 12:54:47.528311","version":"3.32.5","swift_package_manager_enabled":{"ios":false,"macos":false}}
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/codespace/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/codespace/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/codespace/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/codespace/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/codespace/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/codespace/.pub-cache/hosted/pub.dev/connectivity_plus-3.0.6/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]}],"date_created":"2025-07-06 12:59:33.626964","version":"3.32.1","swift_package_manager_enabled":{"ios":false,"macos":false}}
43 changes: 42 additions & 1 deletion myapp/lib/component/graphql_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
class GraphQLService {
static ValueNotifier<GraphQLClient>? _client;
static const String _baseUrl =
"https://humble-zebra-jww5wr76jr53jg4w-4000.app.github.dev/";
"https://redesigned-carnival-xp6v4wpj9pw2jv-4000.app.github.dev/";
/// Get or initialize GraphQL client
static ValueNotifier<GraphQLClient> getClient() {
if (_client == null) {
Expand Down Expand Up @@ -263,4 +263,45 @@ class GraphQLService {
return [];
}
}

static Future<List<dynamic>> recommendBooks(String keyword, int topN) async {
final GraphQLClient client = getClient().value;

const String recommendBooksQuery = '''
query ExampleQuery(\$request: RecommendBooksRequest) {
recommendBooks(request: \$request) {
author
category
cover
description
title
}
}
''';

final QueryOptions options = QueryOptions(
document: gql(recommendBooksQuery),
variables: {
'request': {"keyword": keyword,
"top_n": topN}
},
);

try {
final QueryResult result = await client.query(options);
if (result.hasException) {
debugPrint('GraphQL Error: ${result.exception.toString()}');
return [];
}

if (result.data != null && result.data!['recommendBooks'] != null) {
return result.data!['recommendBooks'] as List<dynamic>;
}

return [];
} catch (e) {
debugPrint('Error in recommendBooks: $e');
return [];
}
}
}
308 changes: 308 additions & 0 deletions myapp/lib/content/AIfeed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import '../component/graphql_client.dart';

void main() => runApp(const MaterialApp(home: BookRecommendationContent()));

class BookRecommendationContent extends StatefulWidget {
const BookRecommendationContent({super.key});

@override
State<BookRecommendationContent> createState() =>
_BookRecommendationContentState();
}

class _BookRecommendationContentState extends State<BookRecommendationContent>
with SingleTickerProviderStateMixin {
String selectedKeyword = '';
final List<String> keywords = [
"로맨스", "힐링", "자기계발", "과학", "주식", "코딩", "판타지",
"만화", "정치", "사회", "요리", "문학", "성장", "청소년", "세계", "미군"
];

final TextEditingController _searchController = TextEditingController();

List<dynamic> recommendedBooks = [];
bool isLoading = false;

@override
void initState() {
super.initState();

// 검색창 입력이 바뀔 때마다 setState() 호출해서 버튼 상태 업데이트
_searchController.addListener(() {
setState(() {});
});
}

@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: const Color(0xFFB7FFE3),
appBar: AppBar(
backgroundColor: const Color(0xFFB7FFE3),
elevation: 0,
centerTitle: true,
title: Text(
'오늘의 책',
style: GoogleFonts.nanumBrushScript(
fontSize: 40,
color: Colors.black,
),
),
bottom: const TabBar(
labelColor: Colors.black,
unselectedLabelColor: Colors.black45,
indicatorColor: Colors.black,
tabs: [
Tab(text: '키워드'),
Tab(text: '도서'),
],
),
),
body: TabBarView(
children: [
buildKeywordTab(),
buildBookSearchTab(),
],
),
bottomNavigationBar: BottomAppBar(
color: const Color(0xFFFDFCE5),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: const [
Icon(Icons.home, size: 28),
Icon(Icons.search, size: 28),
Icon(Icons.bookmark, size: 28),
Icon(Icons.settings, size: 28),
],
),
),
),
),
);
}

Widget buildKeywordTab() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("키워드로 추천 받기",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 10,
children: keywords.map((keyword) {
final isSelected = selectedKeyword == keyword;
return ChoiceChip(
label: Text(keyword),
selected: isSelected,
onSelected: (_) {
setState(() {
selectedKeyword = isSelected ? '' : keyword;
});
},
selectedColor: const Color(0xFF5D3A00),
backgroundColor: Colors.white,
labelStyle: TextStyle(
color: isSelected ? Colors.white : Colors.black,
),
);
}).toList(),
),
const Spacer(),
Center(
child: ElevatedButton(
onPressed: selectedKeyword.isEmpty ? null : () => fetchRecommendations(selectedKeyword),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF5D3A00),
padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 16),
),
child: isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text("책 추천 받기",
style: TextStyle(fontSize: 18, color: Colors.white)),
),
),
],
),
);
}

Widget buildBookSearchTab() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("도서 검색", style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 12),
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "검색어를 입력하세요",
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
const Spacer(),
Center(
child: ElevatedButton(
onPressed: _searchController.text.trim().isEmpty
? null
: () => fetchRecommendations(_searchController.text.trim()),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF5D3A00),
padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 16),
),
child: isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text("책 추천 받기",
style: TextStyle(fontSize: 18, color: Colors.white)),
),
),
],
),
);
}

Future<void> fetchRecommendations(String keyword) async {
setState(() {
isLoading = true;
});

final ValueNotifier<GraphQLClient> client = GraphQLService.getClient();

try {
final books = await GraphQLService.recommendBooks(keyword, 5);
setState(() {
recommendedBooks = books;
isLoading = false;
});

Navigator.push(
context,
MaterialPageRoute(builder: (_) => RecommendationResultScreen(books: recommendedBooks)),
);
} catch (e) {
setState(() {
isLoading = false;
});
debugPrint('추천 실패: $e');
}
}
}

class RecommendationResultScreen extends StatelessWidget {
final List<dynamic> books;
const RecommendationResultScreen({super.key, required this.books});

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF9AD9B8),
appBar: AppBar(
backgroundColor: const Color(0xFF9AD9B8),
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
),
body: books.isEmpty
? const Center(child: Text("추천 결과가 없습니다."))
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [Text(
"키워드를 바탕으로\n도서를 찾아보았어요",
style: GoogleFonts.nanumBrushScript(
fontSize: 40,
color: Colors.black,
),
textAlign: TextAlign.center,
),
],
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: books.length,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
itemBuilder: (context, index) {
final book = books[index];
return _buildBookCard(context, book);
},
),
),
],
),
);
}

Widget _buildBookCard(BuildContext context, dynamic book) {
return Card(
color: Color(0xFFF5F5DC),
margin: const EdgeInsets.symmetric(vertical: 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
_buildBookCover(book['author']),
const SizedBox(height: 12),
Text(
book['title'] ?? '제목 없음',
style: GoogleFonts.jua(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 6),
Text(
book['description'],
style: GoogleFonts.jua(
fontSize: 16,
),
),
Text(
book['cover'],
style: GoogleFonts.jua(
fontSize: 16,
color: Color(0xFF037549),
),
),
],
),
),
);
}

Widget _buildBookCover(String? coverUrl) {
return ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: coverUrl != null
? Image.network(
coverUrl,
width: 200,
height: 280,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.book, size: 100);
},
)
: const Icon(Icons.book, size: 100),
);
}
}
Loading