From 7c914a28c2bc0d08415be8ae9b6c81d940eec0ae Mon Sep 17 00:00:00 2001 From: Dohyun Kim <62280486+Dohyun-s@users.noreply.github.com> Date: Sun, 6 Jul 2025 14:55:32 +0000 Subject: [PATCH 1/2] Add AIFeed --- myapp/.flutter-plugins-dependencies | 6 +- myapp/lib/component/graphql_client.dart | 43 +++- myapp/lib/content/AIfeed.dart | 291 ++++++++++++++++++++++++ myapp/lib/content/bookmark.dart | 2 +- 4 files changed, 335 insertions(+), 7 deletions(-) create mode 100644 myapp/lib/content/AIfeed.dart diff --git a/myapp/.flutter-plugins-dependencies b/myapp/.flutter-plugins-dependencies index aa5e037..2c6e88e 100644 --- a/myapp/.flutter-plugins-dependencies +++ b/myapp/.flutter-plugins-dependencies @@ -1,5 +1 @@ -<<<<<<< HEAD -{"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-01 13:27:30.031751","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-06-22 07:08:45.354337","version":"3.32.1","swift_package_manager_enabled":{"ios":false,"macos":false}} ->>>>>>> refs/remotes/origin/main +{"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}} \ No newline at end of file diff --git a/myapp/lib/component/graphql_client.dart b/myapp/lib/component/graphql_client.dart index 6b03f55..f3e0683 100644 --- a/myapp/lib/component/graphql_client.dart +++ b/myapp/lib/component/graphql_client.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; class GraphQLService { static ValueNotifier? _client; static const String _baseUrl = - "https://cuddly-eureka-v77v7pr94qg36xrx-4000.app.github.dev/"; + "https://redesigned-carnival-xp6v4wpj9pw2jv-4000.app.github.dev/"; /// Get or initialize GraphQL client static ValueNotifier getClient() { if (_client == null) { @@ -263,4 +263,45 @@ class GraphQLService { return []; } } + + static Future> recommendBooks(String keyword, String 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; + } + + return []; + } catch (e) { + debugPrint('Error in recommendBooks: $e'); + return []; + } + } } diff --git a/myapp/lib/content/AIfeed.dart b/myapp/lib/content/AIfeed.dart new file mode 100644 index 0000000..f92baac --- /dev/null +++ b/myapp/lib/content/AIfeed.dart @@ -0,0 +1,291 @@ +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 createState() => + _BookRecommendationContentState(); +} + +class _BookRecommendationContentState extends State + with SingleTickerProviderStateMixin { + String selectedKeyword = ''; + final List keywords = [ + "로맨스", "힐링", "자기계발", "과학", "주식", "코딩", "판타지", + "만화", "정치", "사회", "요리", "문학", "성장", "청소년", "세계", "미군" + ]; + + final TextEditingController _searchController = TextEditingController(); + + List 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 fetchRecommendations(String keyword) async { + setState(() { + isLoading = true; + }); + + final ValueNotifier 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 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: [ + const Text( + "키워드를 바탕으로\n도서를 찾아보았어요", + style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 16), + Expanded( + child: ListView.builder( + itemCount: books.length, + itemBuilder: (_, index) { + final book = books[index]; + return _buildBookCard(context, book); + }, + ), + ), + ], + ), + ); + } + + Widget _buildBookCard(BuildContext context, dynamic book) { + return Card( + color: Colors.white, + margin: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _buildBookCover(book['cover']), + const SizedBox(height: 12), + Text( + book['title'] ?? '제목 없음', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + Text( + book['author'] != null && book['category'] != null + ? "${book['author']} - ${book['category']}" + : book['author'] ?? '작자 미상', + style: const TextStyle(fontSize: 14), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + + Widget _buildBookCover(String? coverUrl) { + return ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: coverUrl != null + ? Image.network( + coverUrl, + width: 150, + height: 220, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.book, size: 100); + }, + ) + : const Icon(Icons.book, size: 100), + ); + } +} diff --git a/myapp/lib/content/bookmark.dart b/myapp/lib/content/bookmark.dart index b3423e3..6b96a08 100644 --- a/myapp/lib/content/bookmark.dart +++ b/myapp/lib/content/bookmark.dart @@ -97,7 +97,7 @@ class _BookmarkContentState extends State { } Widget _buildBookCard(dynamic book, BuildContext context) { - String bookId = book['isbn']; + String bookId = book['isbn13']; return Consumer( builder: (context, bookmarksProvider, child) { From ff09e25778caca1df6c95b1308bfdd47125eb83d Mon Sep 17 00:00:00 2001 From: Dohyun Kim <62280486+Dohyun-s@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:03:29 +0000 Subject: [PATCH 2/2] Add AIFeed graphql API --- myapp/lib/component/graphql_client.dart | 2 +- myapp/lib/content/AIfeed.dart | 57 ++++++++++++++++--------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/myapp/lib/component/graphql_client.dart b/myapp/lib/component/graphql_client.dart index f3e0683..64700ce 100644 --- a/myapp/lib/component/graphql_client.dart +++ b/myapp/lib/component/graphql_client.dart @@ -264,7 +264,7 @@ class GraphQLService { } } - static Future> recommendBooks(String keyword, String topN) async { + static Future> recommendBooks(String keyword, int topN) async { final GraphQLClient client = getClient().value; const String recommendBooksQuery = ''' diff --git a/myapp/lib/content/AIfeed.dart b/myapp/lib/content/AIfeed.dart index f92baac..365f811 100644 --- a/myapp/lib/content/AIfeed.dart +++ b/myapp/lib/content/AIfeed.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; -import 'component/graphql_client.dart'; +import '../component/graphql_client.dart'; void main() => runApp(const MaterialApp(home: BookRecommendationContent())); @@ -183,7 +183,7 @@ class _BookRecommendationContentState extends State final ValueNotifier client = GraphQLService.getClient(); try { - final books = await GraphQLService.recommendBooks(keyword, '5'); + final books = await GraphQLService.recommendBooks(keyword, 5); setState(() { recommendedBooks = books; isLoading = false; @@ -223,15 +223,24 @@ class RecommendationResultScreen extends StatelessWidget { : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "키워드를 바탕으로\n도서를 찾아보았어요", - style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600), + 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, - itemBuilder: (_, index) { + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + itemBuilder: (context, index) { final book = books[index]; return _buildBookCard(context, book); }, @@ -244,27 +253,35 @@ class RecommendationResultScreen extends StatelessWidget { Widget _buildBookCard(BuildContext context, dynamic book) { return Card( - color: Colors.white, - margin: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: Color(0xFFF5F5DC), + margin: const EdgeInsets.symmetric(vertical: 50), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(20.0), child: Column( children: [ - _buildBookCover(book['cover']), + _buildBookCover(book['author']), const SizedBox(height: 12), Text( book['title'] ?? '제목 없음', - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - textAlign: TextAlign.center, + style: GoogleFonts.jua( + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 6), Text( - book['author'] != null && book['category'] != null - ? "${book['author']} - ${book['category']}" - : book['author'] ?? '작자 미상', - style: const TextStyle(fontSize: 14), - textAlign: TextAlign.center, + book['description'], + style: GoogleFonts.jua( + fontSize: 16, + ), + ), + Text( + book['cover'], + style: GoogleFonts.jua( + fontSize: 16, + color: Color(0xFF037549), + ), ), ], ), @@ -278,8 +295,8 @@ class RecommendationResultScreen extends StatelessWidget { child: coverUrl != null ? Image.network( coverUrl, - width: 150, - height: 220, + width: 200, + height: 280, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) { return const Icon(Icons.book, size: 100);