From 64e44466b1b9a771970abc11b1906fa9929ce136 Mon Sep 17 00:00:00 2001 From: dev_jaeho <33758013+jaeho0718@users.noreply.github.com> Date: Sat, 21 Jun 2025 13:11:33 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20OCR=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20-=20.env=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20a?= =?UTF-8?q?ssets=EC=97=90=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=20=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?-=20OCR=20=EC=B2=98=EB=A6=AC=20=EC=8B=9C=20=EB=A9=94=ED=83=80?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=97=90=EC=84=9C=20author=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20filename?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20-=20BBox=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=EC=9D=98=20JSON=20=ED=8C=8C=EC=8B=B1=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9D=84=20Map=EC=97=90=EC=84=9C=20List=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20-=20OCR=20API=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=8B=9C=20=EB=94=94=EB=B2=84=EA=B7=B8=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=98=A4=EB=A5=98=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0=20-=20PDFProviderImpl=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=98=A4=EB=A5=98=20=EB=B0=9C=EC=83=9D=20=EC=8B=9C?= =?UTF-8?q?=20=EB=94=94=EB=B2=84=EA=B7=B8=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EC=99=80=20OCR=20=EA=B2=B0=EA=B3=BC=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EC=9D=98=20=ED=95=84=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- integration_test/pdf_provider_test.dart | 2 +- lib/main.dart | 10 ++ .../ocr_core/models/bounding_box.dart | 10 +- .../services/ocr_core/models/metadata.dart | 14 +- .../services/ocr_core/models/ocr_result.dart | 14 +- .../services/ocr_core/ocr_provider_impl.dart | 134 +++++++++++++++--- .../pdf_core/provider/pdf_provider_impl.dart | 2 + pubspec.yaml | 1 + 8 files changed, 148 insertions(+), 39 deletions(-) diff --git a/integration_test/pdf_provider_test.dart b/integration_test/pdf_provider_test.dart index 17949a0..b5c9baf 100644 --- a/integration_test/pdf_provider_test.dart +++ b/integration_test/pdf_provider_test.dart @@ -21,7 +21,7 @@ class DummyOcrService implements OCRProvider { @override Future process(String pdfPath) async { return const OCRResult( - metadata: Metadata(title: 'test', author: 'test', pages: 0), + metadata: Metadata(title: 'test', pages: 0), pages: [], figures: [], ); diff --git a/lib/main.dart b/lib/main.dart index 6143671..c8d0fbe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,8 +2,10 @@ // lib/main.dart +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:snapfig/features/home/screens/home_widget.dart'; import 'package:snapfig/features/settings/ai_settings_screen.dart'; @@ -14,6 +16,14 @@ import 'core/theme/theme.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + if (kDebugMode) { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + debugPrint( + '(${record.time}) ${record.level.name} - ${record.loggerName}: ${record.message}', + ); + }); + } await dotenv.load(fileName: '.env'); final dir = await getApplicationCacheDirectory(); final pdfProvider = await PDFProviderImpl.load( diff --git a/lib/shared/services/ocr_core/models/bounding_box.dart b/lib/shared/services/ocr_core/models/bounding_box.dart index 7d81aec..1476cf8 100644 --- a/lib/shared/services/ocr_core/models/bounding_box.dart +++ b/lib/shared/services/ocr_core/models/bounding_box.dart @@ -15,12 +15,12 @@ class BBox { return {'x': x, 'y': y, 'width': width, 'height': height}; } - factory BBox.fromJson(Map json) { + factory BBox.fromJson(List json) { return BBox( - x: (json['x'] as num).toDouble(), - y: (json['y'] as num).toDouble(), - width: (json['width'] as num).toDouble(), - height: (json['height'] as num).toDouble(), + x: (json[0] as num).toDouble(), + y: (json[1] as num).toDouble(), + width: (json[2] as num).toDouble(), + height: (json[3] as num).toDouble(), ); } diff --git a/lib/shared/services/ocr_core/models/metadata.dart b/lib/shared/services/ocr_core/models/metadata.dart index 10474d0..beee608 100644 --- a/lib/shared/services/ocr_core/models/metadata.dart +++ b/lib/shared/services/ocr_core/models/metadata.dart @@ -1,23 +1,17 @@ class Metadata { final String title; - final String author; final int pages; - const Metadata({ - required this.title, - required this.author, - required this.pages, - }); + const Metadata({required this.title, required this.pages}); factory Metadata.fromJson(Map json) { return Metadata( - title: json['title'] as String, - author: json['author'] as String, - pages: (json['pages'] as num).toInt(), + title: json['filename'] as String, + pages: (json['total_pages'] as num).toInt(), ); } Map toJson() { - return {'title': title, 'author': author, 'pages': pages}; + return {'title': title, 'pages': pages}; } } diff --git a/lib/shared/services/ocr_core/models/ocr_result.dart b/lib/shared/services/ocr_core/models/ocr_result.dart index ae0cb8c..f9b9a33 100644 --- a/lib/shared/services/ocr_core/models/ocr_result.dart +++ b/lib/shared/services/ocr_core/models/ocr_result.dart @@ -85,7 +85,7 @@ class TextResult { factory TextResult.fromJson(Map json) { return TextResult( text: json['text'] as String, - bbox: BBox.fromJson(json['bbox'] as Map), + bbox: BBox.fromJson(json['bbox'] as List), ); } @@ -109,7 +109,7 @@ class Reference { factory Reference.fromJson(Map json) { return Reference( - bbox: BBox.fromJson(json['bbox'] as Map), + bbox: BBox.fromJson(json['bbox'] as List), text: json['text'] as String, figureId: json['figure_id'] as String? ?? '', notMatched: json['not_matched'] as bool? ?? false, @@ -143,10 +143,10 @@ class Figure { factory Figure.fromJson(Map json) { return Figure( - bbox: BBox.fromJson(json['bbox'] as Map), + bbox: BBox.fromJson(json['bbox'] as List), pageIdx: (json['page_idx'] as num).toInt(), - figureId: json['figure_id'] as String? ?? '', - type: json['type'] as String, + figureId: json['id'] as String? ?? '', + type: json['block_type'] as String, text: json['text'] as String, ); } @@ -155,8 +155,8 @@ class Figure { return { 'bbox': bbox.toJson(), 'page_idx': pageIdx, - 'figure_id': figureId, - 'type': type, + 'id': figureId, + 'block_type': type, 'text': text, }; } diff --git a/lib/shared/services/ocr_core/ocr_provider_impl.dart b/lib/shared/services/ocr_core/ocr_provider_impl.dart index bae2e02..c172765 100644 --- a/lib/shared/services/ocr_core/ocr_provider_impl.dart +++ b/lib/shared/services/ocr_core/ocr_provider_impl.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:http/io_client.dart'; +import 'package:snapfig/shared/services/ocr_core/ocr_core.dart'; import 'package:snapfig/shared/services/ocr_core/ocr_provider.dart'; import 'package:snapfig/shared/services/ocr_core/models/ocr_result.dart'; @@ -21,28 +23,128 @@ class OCRProviderImpl extends OCRProvider { static http.Client _createHttpClient() { final httpClient = HttpClient(); httpClient.badCertificateCallback = (cert, host, port) => true; + + // 연결 안정성을 위한 설정 + httpClient.connectionTimeout = const Duration(seconds: 30); + httpClient.idleTimeout = const Duration(seconds: 60); + + // User-Agent 설정 + httpClient.userAgent = 'Snapfig/1.0'; + return IOClient(httpClient); } @override Future process(String pdfPath) async { - // 1) 분석 요청 - final analyzeUri = Uri.parse('$baseUrl/api/v1/analyze'); - final pdfBytes = await File(pdfPath).readAsBytes(); - final request = http.MultipartRequest('POST', analyzeUri) - ..files.add( - http.MultipartFile.fromBytes( - 'file', - pdfBytes, - filename: path_lib.basename(pdfPath), - ), + debugPrint('OCR 처리 시작: $pdfPath'); + debugPrint('OCR API URL: $baseUrl/analyze'); + + try { + // 1) 분석 요청 + final analyzeUri = Uri.parse('$baseUrl/analyze'); + final pdfBytes = await File(pdfPath).readAsBytes(); + debugPrint('PDF 파일 크기: ${pdfBytes.length} bytes'); + + final request = + http.MultipartRequest('POST', analyzeUri) + ..headers['Accept'] = 'text/event-stream' + ..headers['Cache-Control'] = 'no-cache' + ..files.add( + http.MultipartFile.fromBytes( + 'file', + pdfBytes, + filename: path_lib.basename(pdfPath), + ), + ); + + debugPrint('OCR API 요청 전송 중...'); + final response = await httpClient.send(request); + + debugPrint('OCR API 응답 상태: ${response.statusCode}'); + debugPrint('OCR API 응답 헤더: ${response.headers}'); + + if (response.statusCode != 200) { + debugPrint('OCR API 요청 실패: ${response.statusCode}'); + throw Exception('OCR API 요청 실패: HTTP ${response.statusCode}'); + } + + debugPrint('OCR Stream 수신 시작'); + + String buffer = ''; + int chunkCount = 0; + + await for (final chunk in response.stream.transform(utf8.decoder)) { + chunkCount++; + debugPrint('청크 #$chunkCount 수신: ${chunk.length} characters'); + debugPrint('청크 내용: "$chunk"'); + + buffer += chunk; + + // 줄바꿈으로 분리하여 각 메시지 처리 + final lines = buffer.split('\n'); + buffer = lines.removeLast(); // 마지막 불완전한 라인은 버퍼에 보관 + + for (final line in lines) { + final trimmed = line.trim(); + if (trimmed.isEmpty) { + debugPrint('빈 라인 건너뛰기'); + continue; + } + + debugPrint('라인 처리: "$trimmed"'); + + if (!trimmed.startsWith('data: ')) { + debugPrint('올바르지 않은 메시지 형식: $trimmed'); + continue; + } + + final jsonStr = trimmed.substring(6); + if (jsonStr.isEmpty) { + debugPrint('빈 JSON 문자열 건너뛰기'); + continue; + } + + debugPrint('JSON 파싱 시도: $jsonStr'); + + try { + final json = jsonDecode(jsonStr) as Map; + final status = json['status'] as String; + + debugPrint('JSON 파싱 성공, 상태: $status'); + + if (status == 'completed') { + final data = json['data'] as Map; + debugPrint('OCR 처리 완료'); + final result = OCRResult.fromJson(data); + return result; + } else if (status == 'error') { + final message = json['message'] as String? ?? '알 수 없는 오류'; + debugPrint('OCR 처리 실패: $message'); + throw Exception('OCR 처리 실패: $message'); + } else { + debugPrint('OCR 진행 중: $status'); + } + } catch (e) { + debugPrint('JSON 파싱 오류: $e'); + debugPrint('문제가 된 데이터: "$jsonStr"'); + // JSON 파싱 오류가 있어도 계속 진행 + continue; + } + } + } + + debugPrint('스트림 처리 완료, 총 $chunkCount 개 청크 처리'); + return const OCRResult( + metadata: Metadata(title: '', pages: 0), + pages: [], + figures: [], ); - final response = await httpClient.send(request); - final analyzeRes = await http.Response.fromStream(response); - if (analyzeRes.statusCode != 200) { - throw Exception('OCR API 요청 실패: HTTP ${analyzeRes.statusCode}'); + } catch (e) { + debugPrint('OCR 처리 중 오류 발생: $e'); + if (e.toString().contains('Connection closed')) { + throw Exception('서버 연결이 끊어졌습니다. 서버 상태를 확인해주세요.'); + } + rethrow; } - final bodyJson = json.decode(analyzeRes.body) as Map; - return OCRResult.fromJson(bodyJson); } } diff --git a/lib/shared/services/pdf_core/provider/pdf_provider_impl.dart b/lib/shared/services/pdf_core/provider/pdf_provider_impl.dart index 6644db1..20b7676 100644 --- a/lib/shared/services/pdf_core/provider/pdf_provider_impl.dart +++ b/lib/shared/services/pdf_core/provider/pdf_provider_impl.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'dart:ui'; +import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:snapfig/shared/services/ocr_core/ocr_core.dart'; import 'package:snapfig/shared/services/pdf_core/models/models.dart'; @@ -110,6 +111,7 @@ class PDFProviderImpl extends PDFProvider { final ocrResult = await ocrProvider.process(pdfPath); sendPort.send(ocrResult); } catch (e) { + debugPrint('Fail: $e'); sendPort.send(null); } } diff --git a/pubspec.yaml b/pubspec.yaml index afb66b3..8d11cda 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,6 +86,7 @@ flutter: - assets/sample.pdf - assets/sample_ocr_test.pdf - assets/ocr_result.json + - .env # An image asset can refer to one or more resolution-specific "variants", see From c9d8aa69aac643e30926809fed0a05aaebbfb6ed Mon Sep 17 00:00:00 2001 From: dev_jaeho <33758013+jaeho0718@users.noreply.github.com> Date: Sat, 21 Jun 2025 20:49:10 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20PDF=20=EB=B7=B0=EC=96=B4=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9D=B4=EB=8F=99=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20BBox=20=EB=AA=A8=EB=8D=B8=20JSON=20=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=20=EA=B0=9C=EC=84=A0=20-=20PDF=20=EB=B7=B0=EC=96=B4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EC=8B=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EB=A5=BC=201=20=EC=A6=9D=EA=B0=80=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20-=20BBox=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EC=9D=98=20JSON=20=ED=8C=8C=EC=8B=B1=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9D=84=20=EA=B0=9C=EC=84=A0=ED=95=98=EC=97=AC=20x,=20y=20?= =?UTF-8?q?=EC=A2=8C=ED=91=9C=EC=99=80=20=EB=84=88=EB=B9=84,=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=EB=A5=BC=20=EA=B3=84=EC=82=B0=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/pdf_viewer/screens/pdf_viewer.dart | 2 +- lib/shared/services/ocr_core/models/bounding_box.dart | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/features/pdf_viewer/screens/pdf_viewer.dart b/lib/features/pdf_viewer/screens/pdf_viewer.dart index eb33eaa..416ebd6 100644 --- a/lib/features/pdf_viewer/screens/pdf_viewer.dart +++ b/lib/features/pdf_viewer/screens/pdf_viewer.dart @@ -60,7 +60,7 @@ class _PDFViewerState extends State { if (layout.type != LayoutType.figure) return; final pageNumber = _viewModel!.getPageNumberForFigure(layout); if (pageNumber == null) return; - _pdfController.goToPage(pageNumber: pageNumber); + _pdfController.goToPage(pageNumber: pageNumber + 1); } void _showFigurePopover(BaseLayout reference, Offset tapPosition) { diff --git a/lib/shared/services/ocr_core/models/bounding_box.dart b/lib/shared/services/ocr_core/models/bounding_box.dart index 1476cf8..86440a4 100644 --- a/lib/shared/services/ocr_core/models/bounding_box.dart +++ b/lib/shared/services/ocr_core/models/bounding_box.dart @@ -16,12 +16,11 @@ class BBox { } factory BBox.fromJson(List json) { - return BBox( - x: (json[0] as num).toDouble(), - y: (json[1] as num).toDouble(), - width: (json[2] as num).toDouble(), - height: (json[3] as num).toDouble(), - ); + final x1 = (json[0] as num).toDouble(); + final y1 = (json[1] as num).toDouble(); + final x2 = (json[2] as num).toDouble(); + final y2 = (json[3] as num).toDouble(); + return BBox(x: x1, y: y1, width: x2 - x1, height: y2 - y1); } @override From 4a4bddb750e6ab0e77dd0f48b2bb089075bfbfbe Mon Sep 17 00:00:00 2001 From: dev_jaeho <33758013+jaeho0718@users.noreply.github.com> Date: Sat, 21 Jun 2025 21:15:22 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20gpt=5Fmarkdown=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20UI=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20-=20pubspec.yaml=EC=97=90=20gpt=5Fmarkdown=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=B6=94=EA=B0=80=20-=20SimpleDr?= =?UTF-8?q?aggableAIChat=20=EC=9C=84=EC=A0=AF=EC=97=90=EC=84=9C=20GptMarkd?= =?UTF-8?q?own=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=ED=91=9C=EC=8B=9C=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20-=20FigureSidebar=EC=97=90=EC=84=9C=20figureId=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=EB=A5=BC=20GptMarkdown=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20-=20SimpleDraggableAIChat=EC=9D=98=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EC=B0=BD=20=ED=81=AC=EA=B8=B0=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=ED=8C=A8=EB=94=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pdf_viewer/widgets/sidebar/figure_sidebar.dart | 3 ++- .../pdf_viewer/widgets/simple_draggable_ai_chat.dart | 9 +++++---- pubspec.yaml | 4 ++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/features/pdf_viewer/widgets/sidebar/figure_sidebar.dart b/lib/features/pdf_viewer/widgets/sidebar/figure_sidebar.dart index d42eac4..29ddbd1 100644 --- a/lib/features/pdf_viewer/widgets/sidebar/figure_sidebar.dart +++ b/lib/features/pdf_viewer/widgets/sidebar/figure_sidebar.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:snapfig/shared/services/pdf_core/pdf_core.dart'; +import 'package:gpt_markdown/gpt_markdown.dart'; class FigureSidebar extends StatelessWidget { final List figures; @@ -18,7 +19,7 @@ class FigureSidebar extends StatelessWidget { itemBuilder: (context, index) { return ListTile( leading: _buildThumbnail(figures[index], context: context), - title: Text(figures[index].figureId ?? ''), + title: GptMarkdown(figures[index].figureId ?? '', maxLines: 2), trailing: const Icon(Icons.chevron_right), onTap: () => onFigureSelected(figures[index]), ); diff --git a/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart b/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart index 49cbd01..3412b5d 100644 --- a/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart +++ b/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart @@ -3,6 +3,7 @@ import 'package:snapfig/shared/services/ai_service/ai_service.dart'; import 'package:snapfig/shared/services/ai_service/models/ai_provider.dart'; import 'package:snapfig/shared/services/pdf_core/models/models.dart'; import 'package:snapfig/features/pdf_viewer/models/pdf_data_viewmodel.dart'; +import 'package:gpt_markdown/gpt_markdown.dart'; class Message { final String content; @@ -50,8 +51,8 @@ class _SimpleDraggableAIChatState extends State late Animation _scaleAnimation; bool _isDragging = false; - static const double _chatWidth = 320.0; - static const double _chatHeight = 400.0; + static const double _chatWidth = 400.0; + static const double _chatHeight = 500.0; @override void initState() { @@ -245,7 +246,7 @@ class _SimpleDraggableAIChatState extends State final configurations = AIService.instance.configurations; return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: _isDragging @@ -410,7 +411,7 @@ class _SimpleDraggableAIChatState extends State ), ], ) - : Text( + : GptMarkdown( message.content, style: theme.textTheme.bodyMedium?.copyWith( color: diff --git a/pubspec.yaml b/pubspec.yaml index 8d11cda..9666555 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: http: ^1.4.0 flutter_client_sse: ^2.0.3 flutter_dotenv: ^5.1.0 + gpt_markdown: ^1.0.20 dev_dependencies: flutter_test: @@ -68,6 +69,9 @@ dev_dependencies: build_runner: ^2.4.13 mockito: ^5.4.4 +dependency_overrides: + flutter_math_fork: 0.7.3 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From fdb9374b525892701a1e25ad01dab1fd400c84e9 Mon Sep 17 00:00:00 2001 From: dev_jaeho <33758013+jaeho0718@users.noreply.github.com> Date: Sat, 21 Jun 2025 21:26:39 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20PDF=20=EB=B7=B0=EC=96=B4=20UI=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=B1=84=ED=8C=85=20=EC=9C=84?= =?UTF-8?q?=EC=A0=AF=20=ED=81=AC=EA=B8=B0=20=EC=A1=B0=EC=A0=95=20-=20PDF?= =?UTF-8?q?=20=EB=B7=B0=EC=96=B4=EC=9D=98=20AppBar=20=EA=B5=AC=EC=84=B1=20?= =?UTF-8?q?=EB=8B=A8=EC=88=9C=ED=99=94=20-=20SimpleDraggableAIChat=20?= =?UTF-8?q?=EC=9C=84=EC=A0=AF=EC=97=90=20=ED=81=AC=EA=B8=B0=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EC=B5=9C=EC=86=8C/=EC=B5=9C=EB=8C=80=20=ED=81=AC=EA=B8=B0?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20-=20FigureSidebar=EC=97=90=EC=84=9C=20f?= =?UTF-8?q?igureId=EB=A5=BC=20text=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=ED=91=9C=EC=8B=9C=20=EB=B0=A9=EC=8B=9D=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pdf_viewer/screens/pdf_viewer.dart | 13 +- .../widgets/sidebar/figure_sidebar.dart | 2 +- .../widgets/simple_draggable_ai_chat.dart | 163 ++++++++++++------ 3 files changed, 114 insertions(+), 64 deletions(-) diff --git a/lib/features/pdf_viewer/screens/pdf_viewer.dart b/lib/features/pdf_viewer/screens/pdf_viewer.dart index 416ebd6..0f96139 100644 --- a/lib/features/pdf_viewer/screens/pdf_viewer.dart +++ b/lib/features/pdf_viewer/screens/pdf_viewer.dart @@ -167,18 +167,7 @@ class _PDFViewerState extends State { listenable: _viewModel!, builder: (context, child) { return Scaffold( - appBar: AppBar( - title: Text(_viewModel!.pdfTitle), - actions: [ - IconButton( - onPressed: () { - Navigator.of(context).pushNamed('/ai-settings'); - }, - icon: const Icon(Icons.smart_toy), - tooltip: 'AI 설정', - ), - ], - ), + appBar: AppBar(title: Text(_viewModel!.pdfTitle)), body: Row( children: [ Expanded( diff --git a/lib/features/pdf_viewer/widgets/sidebar/figure_sidebar.dart b/lib/features/pdf_viewer/widgets/sidebar/figure_sidebar.dart index 29ddbd1..86d77ed 100644 --- a/lib/features/pdf_viewer/widgets/sidebar/figure_sidebar.dart +++ b/lib/features/pdf_viewer/widgets/sidebar/figure_sidebar.dart @@ -19,7 +19,7 @@ class FigureSidebar extends StatelessWidget { itemBuilder: (context, index) { return ListTile( leading: _buildThumbnail(figures[index], context: context), - title: GptMarkdown(figures[index].figureId ?? '', maxLines: 2), + title: GptMarkdown(figures[index].text ?? '', maxLines: 2), trailing: const Icon(Icons.chevron_right), onTap: () => onFigureSelected(figures[index]), ); diff --git a/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart b/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart index 3412b5d..a947020 100644 --- a/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart +++ b/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart @@ -50,9 +50,17 @@ class _SimpleDraggableAIChatState extends State late AnimationController _animationController; late Animation _scaleAnimation; bool _isDragging = false; + bool _isResizing = false; - static const double _chatWidth = 400.0; - static const double _chatHeight = 500.0; + // Size variables + double _chatWidth = 400.0; + double _chatHeight = 500.0; + + // Size constraints + static const double _minWidth = 300.0; + static const double _maxWidth = 600.0; + static const double _minHeight = 400.0; + static const double _maxHeight = 800.0; @override void initState() { @@ -204,36 +212,109 @@ class _SimpleDraggableAIChatState extends State _position = _snapToEdges(_position, screenSize); }); }, - child: Container( - width: _chatWidth, - height: _chatHeight, - decoration: BoxDecoration( - color: theme.colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: - _isDragging - ? theme.colorScheme.primary.withValues(alpha: 0.5) - : theme.colorScheme.outline.withValues(alpha: 0.2), - width: _isDragging ? 2 : 1, + child: Stack( + children: [ + Container( + width: _chatWidth, + height: _chatHeight, + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: + _isDragging || _isResizing + ? theme.colorScheme.primary.withValues(alpha: 0.5) + : theme.colorScheme.outline.withValues( + alpha: 0.2, + ), + width: _isDragging || _isResizing ? 2 : 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues( + alpha: _isDragging || _isResizing ? 0.3 : 0.15, + ), + blurRadius: _isDragging || _isResizing ? 20 : 10, + offset: Offset(0, _isDragging || _isResizing ? 8 : 4), + ), + ], + ), + child: Column( + children: [ + _buildDraggableHeader(context), + Expanded(child: _buildMessageList(context)), + _buildInputSection(context), + ], + ), ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues( - alpha: _isDragging ? 0.3 : 0.15, + // Resize handle + Positioned( + right: 0, + bottom: 0, + child: GestureDetector( + onPanStart: (details) { + setState(() { + _isResizing = true; + }); + }, + onPanUpdate: (details) { + setState(() { + // Update size based on drag delta + _chatWidth = (_chatWidth + details.delta.dx).clamp( + _minWidth, + _maxWidth, + ); + _chatHeight = (_chatHeight + details.delta.dy).clamp( + _minHeight, + _maxHeight, + ); + + // Ensure window stays within screen bounds after resizing + final screenSize = MediaQuery.of(context).size; + if (_position.dx + _chatWidth > screenSize.width) { + _position = Offset( + screenSize.width - _chatWidth, + _position.dy, + ); + } + if (_position.dy + _chatHeight > screenSize.height) { + _position = Offset( + _position.dx, + screenSize.height - _chatHeight, + ); + } + }); + }, + onPanEnd: (details) { + setState(() { + _isResizing = false; + }); + }, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: + _isResizing + ? theme.colorScheme.primary + : theme.colorScheme.surfaceVariant, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + bottomRight: Radius.circular(16), + ), + ), + child: Icon( + Icons.drag_handle, + size: 12, + color: + _isResizing + ? theme.colorScheme.onPrimary + : theme.colorScheme.onSurfaceVariant, + ), ), - blurRadius: _isDragging ? 20 : 10, - offset: Offset(0, _isDragging ? 8 : 4), ), - ], - ), - child: Column( - children: [ - _buildDraggableHeader(context), - Expanded(child: _buildMessageList(context)), - _buildInputSection(context), - ], - ), + ), + ], ), ), ), @@ -271,17 +352,8 @@ class _SimpleDraggableAIChatState extends State ), Row( children: [ - Icon( - Icons.smart_toy, - color: - _isDragging - ? theme.colorScheme.onPrimaryContainer - : theme.colorScheme.primary, - size: 20, - ), - const SizedBox(width: 8), Text( - 'AI Assistant', + 'Assistant', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: @@ -560,18 +632,7 @@ class _SimpleDraggableAIChatState extends State ), Row( children: [ - Icon( - Icons.smart_toy, - color: theme.colorScheme.primary, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'AI Assistant', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - ), + Text('Assistant', style: theme.textTheme.titleMedium), const Spacer(), IconButton( onPressed: _closeWithAnimation, From 08b39c570510f8c54065281df7bba6fdec2b8554 Mon Sep 17 00:00:00 2001 From: dev_jaeho <33758013+jaeho0718@users.noreply.github.com> Date: Sat, 21 Jun 2025 22:59:33 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20PDF=20=EB=B7=B0=EC=96=B4=20UI=20?= =?UTF-8?q?=EB=B0=8F=20OCR=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0=20-?= =?UTF-8?q?=20SimpleDraggableAIChat=20=EC=9C=84=EC=A0=AF=EC=9D=98=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=83=89=EC=83=81=EC=9D=84=20onS?= =?UTF-8?q?econdaryContainer=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EA=B0=80=EB=8F=85=EC=84=B1=20=ED=96=A5=EC=83=81=20?= =?UTF-8?q?-=20PDFSideBar=EC=97=90=EC=84=9C=20Outline=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=9D=84=20=EC=A1=B0=EA=B1=B4=EB=B6=80=EB=A1=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=ED=95=98=EC=97=AC=20UI=20=EA=B0=9C=EC=84=A0=20-=20OCR?= =?UTF-8?q?ProviderImpl=EC=97=90=EC=84=9C=20=EC=97=B0=EA=B2=B0=20=EC=95=88?= =?UTF-8?q?=EC=A0=95=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=ED=83=80?= =?UTF-8?q?=EC=9E=84=EC=95=84=EC=9B=83=20=EB=B0=8F=20=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A6=BC=20=EC=84=A4=EC=A0=95=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pdf_viewer/widgets/sidebar/pdf_sidebar.dart | 10 +++++++--- .../widgets/simple_draggable_ai_chat.dart | 4 ++-- .../services/ocr_core/ocr_provider_impl.dart | 17 ++++++++++++++--- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/features/pdf_viewer/widgets/sidebar/pdf_sidebar.dart b/lib/features/pdf_viewer/widgets/sidebar/pdf_sidebar.dart index e79aae7..cbb9de0 100644 --- a/lib/features/pdf_viewer/widgets/sidebar/pdf_sidebar.dart +++ b/lib/features/pdf_viewer/widgets/sidebar/pdf_sidebar.dart @@ -41,9 +41,13 @@ class _PDFSideBarState extends State { children: [ SegmentedButton<_PDFSideBarTab>( showSelectedIcon: true, - segments: const [ - ButtonSegment(value: _PDFSideBarTab.page, label: Text('Outline')), - ButtonSegment( + segments: [ + if (widget.viewModel.outlines.isNotEmpty) + const ButtonSegment( + value: _PDFSideBarTab.page, + label: Text('Outline'), + ), + const ButtonSegment( value: _PDFSideBarTab.figure, label: Text('Figure'), ), diff --git a/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart b/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart index a947020..f4c910e 100644 --- a/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart +++ b/lib/features/pdf_viewer/widgets/simple_draggable_ai_chat.dart @@ -478,7 +478,7 @@ class _SimpleDraggableAIChatState extends State Text( 'Thinking...', style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + color: theme.colorScheme.onSecondaryContainer, ), ), ], @@ -489,7 +489,7 @@ class _SimpleDraggableAIChatState extends State color: message.isUser ? theme.colorScheme.onPrimary - : theme.colorScheme.onSurface, + : theme.colorScheme.onSecondaryContainer, ), ), ), diff --git a/lib/shared/services/ocr_core/ocr_provider_impl.dart b/lib/shared/services/ocr_core/ocr_provider_impl.dart index c172765..90644e5 100644 --- a/lib/shared/services/ocr_core/ocr_provider_impl.dart +++ b/lib/shared/services/ocr_core/ocr_provider_impl.dart @@ -25,8 +25,12 @@ class OCRProviderImpl extends OCRProvider { httpClient.badCertificateCallback = (cert, host, port) => true; // 연결 안정성을 위한 설정 - httpClient.connectionTimeout = const Duration(seconds: 30); - httpClient.idleTimeout = const Duration(seconds: 60); + httpClient.connectionTimeout = const Duration(minutes: 5); + httpClient.idleTimeout = const Duration(minutes: 35); + + // 긴 스트림 처리를 위한 추가 설정 + httpClient.maxConnectionsPerHost = 10; + httpClient.autoUncompress = false; // 압축 해제 비활성화로 스트림 안정성 향상 // User-Agent 설정 httpClient.userAgent = 'Snapfig/1.0'; @@ -49,6 +53,9 @@ class OCRProviderImpl extends OCRProvider { http.MultipartRequest('POST', analyzeUri) ..headers['Accept'] = 'text/event-stream' ..headers['Cache-Control'] = 'no-cache' + ..headers['Connection'] = 'keep-alive' + ..headers['Keep-Alive'] = + 'timeout=2100, max=1000' // 35분 타임아웃 ..files.add( http.MultipartFile.fromBytes( 'file', @@ -72,10 +79,14 @@ class OCRProviderImpl extends OCRProvider { String buffer = ''; int chunkCount = 0; + DateTime lastChunkTime = DateTime.now(); await for (final chunk in response.stream.transform(utf8.decoder)) { chunkCount++; - debugPrint('청크 #$chunkCount 수신: ${chunk.length} characters'); + lastChunkTime = DateTime.now(); + debugPrint( + '청크 #$chunkCount 수신: ${chunk.length} characters (${lastChunkTime.toIso8601String()})', + ); debugPrint('청크 내용: "$chunk"'); buffer += chunk;