From 6366a2f880274fdb442cb45ec6ce259cb59445b7 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:50:59 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20=EA=B8=80=20=EB=B7=B0=EC=96=B4:=20?= =?UTF-8?q?=EC=A4=84=EB=B0=94=EA=BF=88=20=EC=97=86=EB=8A=94=20=ED=81=B0=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=9D=80=20=ED=99=94=EB=A9=B4=20=ED=8F=AD?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=EB=82=98=EB=88=A0=20=EA=B7=B8?= =?UTF-8?q?=EB=A0=A4=20=EB=A9=88=EC=B6=94=EC=A7=80=20=EC=95=8A=EA=B2=8C=20?= =?UTF-8?q?(=EA=B2=80=ED=86=A0=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 관리자 지시 (10/19, 사용자 "오늘 못했던 것 진행" 2번 - 데이터 검토에서 남긴 작은 점): 줄바꿈 없는 큰 파일 (한 줄 수십만 자) 을 한 Text 로 그려 잠깐 멈췄다. - 화면 한 줄에 드는 글자 수 (고정폭 글꼴 · 글자 크기 · 줄 번호 칸) 를 보고, 그 8배보다 긴 줄만 4배 (화면 네 줄쯤) 씩 나눠 그린다. 보통 줄은 그대로 (화면에서 저절로 줄바꿈). 줄 번호는 첫 조각에만. 이모지 같은 두 단위 글자는 반으로 자르지 않는다. - 나눈 줄은 글 · 한 줄 글자 수가 같으면 다시 만들지 않는다 (viewerRows). 시험: xplore_tools_test (새 2: viewerRows - 글 그대로 · 줄 번호 · 8배까지 그대로 · 이모지 / 200KB 한 줄 파일을 412 폭에서 여러 짧은 조각으로) 4 통과, 임시 폴더 흔적 0 (tool/test_temp_check.ps1), analyze 문제 없음. Co-Authored-By: Claude Opus 5.5 --- lib/ui/text_viewer.dart | 55 +++++++++++++++++++++++++++++++++---- test/xplore_tools_test.dart | 35 +++++++++++++++++++++++ 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/lib/ui/text_viewer.dart b/lib/ui/text_viewer.dart index 9097737..444709c 100644 --- a/lib/ui/text_viewer.dart +++ b/lib/ui/text_viewer.dart @@ -84,9 +84,22 @@ class _TextViewerPageState extends State { _text = t; } + /// 그릴 줄 (줄 번호 · 글) - 글 · 한 줄 글자 수가 같으면 다시 만들지 않는다 + List<(int, String)> _rows = const []; + String? _rowsText; + int _rowsPer = 0; + + List<(int, String)> _rowsFor(int perRow) { + if (!identical(_rowsText, _text) || _rowsPer != perRow) { + _rowsText = _text; + _rowsPer = perRow; + _rows = viewerRows(_text, perRow); + } + return _rows; + } + @override Widget build(BuildContext context) { - final lines = _text.split('\n'); // 좁은 화면 (폰 세로) 에서는 줄 번호 · 글자 크기를 ⋮ 메뉴로 (412 에서 파일 이름이 다 가려졌다) final narrow = MediaQuery.sizeOf(context).width < 600; return Scaffold( @@ -150,23 +163,29 @@ class _TextViewerPageState extends State { Expanded( // 고르기 · 복사는 화면 전체에서 (줄마다 고르는 칸을 두면 큰 파일에서 무겁다) child: SelectionArea( - child: Scrollbar( + child: LayoutBuilder(builder: (context, box) { + // 관리자 지시 (10/19): 줄바꿈 없는 큰 파일 (한 줄이 수십만 자) 을 한 Text 로 그리면 잠깐 멈췄다 - + // 화면 한 줄에 드는 글자 수 (고정폭 글꼴) 를 보고 아주 긴 줄은 나눠 그린다 + final room = box.maxWidth - 16 - (_lineNumbers ? 56 : 0); + final rows = _rowsFor((room / (_font * 0.62)).floor().clamp(20, 4000)); + return Scrollbar( child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - itemCount: lines.length, + itemCount: rows.length, itemBuilder: (_, i) => Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_lineNumbers) SizedBox( width: 48, - child: Text('${i + 1}', + child: Text(rows[i].$1 > 0 ? '${rows[i].$1}' : '', textAlign: TextAlign.right, style: TextStyle(fontSize: _font - 2, color: JjColors.textDim, fontFamily: 'monospace')), ), if (_lineNumbers) const SizedBox(width: 8), - Expanded(child: Text(lines[i], style: TextStyle(fontSize: _font, fontFamily: 'monospace'))), + Expanded(child: Text(rows[i].$2, style: TextStyle(fontSize: _font, fontFamily: 'monospace'))), ]), ), - ), + ); + }), ), ), if (!_complete) @@ -187,3 +206,27 @@ class _TextViewerPageState extends State { ); } } + +/// 글 뷰어가 그릴 줄: (줄 번호 - 나눈 조각의 둘째부터는 0, 글). 화면 한 줄에 [perRow] 자가 들 때, +/// 그보다 8배 넘게 긴 줄은 4배 (화면 네 줄쯤) 씩 나눈다 - 보통 줄은 그대로 (화면에서 저절로 줄바꿈) +List<(int, String)> viewerRows(String text, int perRow) { + final out = <(int, String)>[]; + final lines = text.split('\n'); + final piece = perRow * 4; + for (var i = 0; i < lines.length; i++) { + final line = lines[i]; + if (line.length <= perRow * 8) { + out.add((i + 1, line)); + continue; + } + var at = 0; + while (at < line.length) { + var end = (at + piece).clamp(0, line.length); + // 두 단위 글자 (이모지 등) 를 반으로 자르지 않게 + if (end < line.length && end > at + 1 && (line.codeUnitAt(end) & 0xFC00) == 0xDC00) end--; + out.add((at == 0 ? i + 1 : 0, line.substring(at, end))); + at = end; + } + } + return out; +} diff --git a/test/xplore_tools_test.dart b/test/xplore_tools_test.dart index 24d1119..8cccfca 100644 --- a/test/xplore_tools_test.dart +++ b/test/xplore_tools_test.dart @@ -35,6 +35,41 @@ void main() { expect(find.text('CP949 (EUC-KR)'), findsOneWidget); }); + test('h 글 뷰어: 아주 긴 줄만 나눠 그린다 (줄 번호는 첫 조각에만, 이모지를 반으로 자르지 않음)', () { + expect(viewerRows('a\nbb', 10), [(1, 'a'), (2, 'bb')]); + final long = 'x' * 1000; + final rows = viewerRows('first\n$long\nlast', 10); + expect(rows.first, (1, 'first')); + expect(rows.last, (3, 'last')); + final mid = rows.sublist(1, rows.length - 1); + expect(mid.map((r) => r.$2).join(), long, reason: '글은 그대로 (빠지거나 겹치지 않음)'); + expect(mid.first.$1, 2); + expect(mid.skip(1).every((r) => r.$1 == 0), isTrue); + expect(mid.every((r) => r.$2.length <= 40), isTrue, reason: '화면 네 줄쯤씩'); + expect(viewerRows('y' * 80, 10).length, 1, reason: '8배까지는 그대로 (화면에서 줄바꿈)'); + // 나누는 자리에 두 단위 글자 (😀) 가 걸리면 앞으로 당긴다 + final emoji = '${'z' * 39}😀${'z' * 100}'; + final cut = viewerRows(emoji, 10); + expect(cut.map((r) => r.$2).join(), emoji); + expect(cut.every((r) => !r.$2.endsWith('\uD83D')), isTrue); + }); + + testWidgets('h 글 뷰어: 줄바꿈 없는 큰 파일 (200KB 한 줄) 도 나눠 그려 멈추지 않는다', (t) async { + final f = File(p.join(tmp.path, 'one.txt'))..writeAsStringSync('${'0123456789' * 20000}END'); + t.view.physicalSize = const Size(412, 900); + t.view.devicePixelRatio = 1; + addTearDown(t.view.reset); + await t.pumpWidget(MaterialApp(home: TextViewerPage(path: f.path))); + for (var i = 0; i < 40 && find.textContaining('0123456789').evaluate().isEmpty; i++) { + await t.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); + await t.pump(); + } + final texts = t.widgetList(find.textContaining('0123456789')).toList(); + expect(texts.length, greaterThan(1), reason: '한 줄을 여러 조각으로'); + expect(texts.every((x) => (x.data ?? '').length < 2000), isTrue, reason: '조각마다 짧게'); + expect(find.text('1'), findsOneWidget, reason: '줄 번호는 첫 조각에만'); + }); + testWidgets('g 격자 보기: 그림 · 동영상만, 누르면 열기', (t) async { final png = File(p.join(tmp.path, 'a.png'))..writeAsBytesSync([0x89, 0x50]); final opened = [];