From 41d4d8afbf1a1fabc109d6c7e815f2cbade80de1 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:04:06 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=20[?= =?UTF-8?q?=EC=8B=A4=ED=96=89]=20=EB=92=A4=20=EC=98=B5=EC=85=98=20?= =?UTF-8?q?=EC=B9=B8=20=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=ED=92=80=EA=B8=B0=20?= =?UTF-8?q?=C2=B7=20=EC=8B=A4=ED=8C=A8=20=EA=B8=80=EC=97=90=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=9D=B4=EB=A6=84=20=EC=97=86=EC=9D=B4=20(236=20?= =?UTF-8?q?=C2=B7=20237)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 236 (버그, 기기 확인): 실행 뒤 포커스가 옵션 칸에 남아 키보드가 올라와 끝 알림을 가렸다 - 237 (사용자 편의, 기기 확인): 카드 실패 글에서 'ProcessException:' · Command 줄을 뺌 (userErrorText). 원문은 작업 기록에 Co-Authored-By: Claude Opus 5.5 --- lib/app/copy_center.dart | 2 +- lib/app/transfer_job.dart | 9 +++++++++ lib/ui/monitor_page.dart | 2 ++ test/dismiss_test.dart | 31 +++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/app/copy_center.dart b/lib/app/copy_center.dart index 3661db4..9060abf 100644 --- a/lib/app/copy_center.dart +++ b/lib/app/copy_center.dart @@ -142,7 +142,7 @@ class CopyCenter extends ChangeNotifier { if (job.leftInSource > 0) trf('{0}개는 원본에 남음 (건너뜀)', [job.leftInSource]), if (job.pruned > 0) trf('빈 폴더 {0}개 지움', [job.pruned]), ].join(' · ') - : '$e', + : userErrorText(e), // 237: 예외 이름 없이 (원래 글은 작업 기록에) lastFiles: job.doneFiles, lastLeft: e == null ? job.leftInSource : 0, )); diff --git a/lib/app/transfer_job.dart b/lib/app/transfer_job.dart index 7739255..5fe2d72 100644 --- a/lib/app/transfer_job.dart +++ b/lib/app/transfer_job.dart @@ -16,6 +16,15 @@ import '../l10n/tr.dart'; /// - 아래쪽: 지금 항목 (폴더) 의 파일 중 몇 개를 했는지 /// 215: rsync 가 실패했을 때 보일 글 - 원본을 못 지웠으면 쉬운 말 한 줄, 그리고 오류 · 권한 줄을 끝쪽부터 (전송한 파일 이름이 아니라). /// 예전에는 출력의 앞쪽 8줄이라 파일 이름만 보이고 "Permission denied" · "rsync error … (code 23)" 가 빠지기 쉬웠다 +/// 237: 사용자에게 보이는 실패 글 - "ProcessException:" 같은 예외 이름 · "Command:" 줄은 빼고 내용만. +/// 원래 글 ('$e') 은 작업 기록에 그대로 남긴다 +String userErrorText(Object e) => switch (e) { + ProcessException(:final message) when message.isNotEmpty => message, + FileSystemException(:final message, :final path, :final osError) => + [message, if (path != null && path.isNotEmpty) path, if (osError != null && osError.message.isNotEmpty) osError.message].join(' · '), + _ => '$e'.replaceFirst(RegExp(r'^\w*(Exception|Error)(\([^)]*\))?: '), ''), + }; + String rsyncFailureText(int code, List errLines, List allLines) { final all = [...errLines, ...allLines]; final removeFailed = all.any((l) { diff --git a/lib/ui/monitor_page.dart b/lib/ui/monitor_page.dart index 1a95528..4747310 100644 --- a/lib/ui/monitor_page.dart +++ b/lib/ui/monitor_page.dart @@ -333,6 +333,8 @@ class _CopyCardState extends State<_CopyCard> { } Future _run(CopyTask t) async { + // 236: 옵션 칸의 포커스를 푼다 - 남아 있으면 폰 · 태블릿에서 키보드가 올라와 끝 알림을 가린다 + FocusManager.instance.primaryFocus?.unfocus(); // 104: 지우기가 들어 있으면 Rsync 화면과 같은 미리 보기를 거친다 (그사이 대상에 생긴 파일을 묻지 않고 지우지 않게). // 210: 이동 (원본 지우기) 도 - 카드의 [실행] 은 따로 묻는 창이 없어 곧바로 원본을 지웠다. 복사만은 지금처럼 바로 if (!await confirmDeletingRun(context, t, includeMove: true, skipNewer: widget.c.settings.copySkipNewer) || !mounted) return; diff --git a/test/dismiss_test.dart b/test/dismiss_test.dart index 2c9762b..5e24f01 100644 --- a/test/dismiss_test.dart +++ b/test/dismiss_test.dart @@ -6,6 +6,7 @@ import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/app/copy_center.dart'; import 'package:jj_mkvmaker/app/live_sync.dart'; import 'package:jj_mkvmaker/app/settings.dart'; +import 'package:jj_mkvmaker/app/transfer_job.dart' show userErrorText; import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; @@ -217,6 +218,36 @@ void main() { await tester.pumpAndSettle(); }); + testWidgets('236: 모니터링 카드 [실행] 뒤 옵션 칸에 포커스가 남지 않는다 (키보드가 저절로 올라와 끝 알림을 가리지 않게)', skip: !Platform.isWindows, (tester) async { + final src = p.join(tmp.path, 'cp'), dst = p.join(tmp.path, 'cpd'); + File(p.join(src, 'a.txt')) + ..createSync(recursive: true) + ..writeAsStringSync('a'); + Directory(dst).createSync(); + c.settings.copyTasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'robocopy', options: '/E')]; + tester.view.physicalSize = const Size(1500, 1000); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c))); + await tester.pump(); + await tester.showKeyboard(find.widgetWithText(TextField, '/E')); // 옵션 칸을 눌러 고치는 중 (현재 방식은 칸이 꺼져 있어 robocopy) + await tester.pump(); + expect(tester.widget(find.byType(EditableText).first).focusNode.hasFocus, isTrue); + // 고치지 않았으니 묻지 않고 바로 (복사, 지우기 없음) + await tester.tap(find.widgetWithText(FilledButton, '실행').first); + await tester.pump(); + expect(tester.widget(find.byType(EditableText).first).focusNode.hasFocus, isFalse); + CopyCenter.of(c).jobs['1']?.cancel(); + await settle(tester, () => CopyCenter.of(c).jobs['1']?.finished ?? true, rounds: 200); + }); + + test('237: 사용자에게 보이는 실패 글에는 예외 이름 · 명령 줄이 없다', () { + expect(userErrorText(const ProcessException('rsync', ['-a'], '원본 파일을 지우지 못했습니다', 23)), '원본 파일을 지우지 못했습니다'); + expect(userErrorText(const FileSystemException('지울 수 없음', r'C:\x', OSError('액세스가 거부되었습니다', 5))), r'지울 수 없음 · C:\x · 액세스가 거부되었습니다'); + expect(userErrorText(Exception('끊김')), '끊김'); + expect(userErrorText('그냥 글'), '그냥 글'); + }); + testWidgets('105: 원본이 대상 안 + 창에서 [원본 파일 지우기] 로 지우기 옵션이 생기면 막는다', (tester) async { final outer = p.join(tmp.path, 'O'), inner = p.join(outer, 'in'); File(p.join(inner, 'n.txt'))