From abeb3d899915e41143f1381fd0f14c9783926662 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:37:43 +0900 Subject: [PATCH] =?UTF-8?q?=EC=98=AE=EA=B8=B0=EA=B8=B0:=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=ED=95=98=EB=A9=B4=20=EB=B0=94=EB=A1=9C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=97=90=EC=84=9C=20=EB=B9=BC=EA=B3=A0,=20=EC=98=AE?= =?UTF-8?q?=EA=B8=B0=EC=A7=80=20=EB=AA=BB=ED=95=98=EB=A9=B4=20=EC=9B=90?= =?UTF-8?q?=EB=9E=98=20=EC=9E=90=EB=A6=AC=20=C2=B7=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=C2=B7=20=EC=84=A0=ED=83=9D=EC=9C=BC=EB=A1=9C=20=EB=90=98?= =?UTF-8?q?=EB=8F=8C=EB=A6=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 다른 드라이브 · NAS 로는 복사라 큰 동영상이 1초 넘게 목록에 남아 보였다 (1GB 시험 파일 M: → C: 1,079ms, 같은 드라이브 22ms). - 데이터 버그 고침 (moveFileInto): 다른 드라이브로 복사한 뒤 원본을 지우지 못하면 (열려 있음 등) 대상 폴더에 사본이 남아 두 벌이 됐다. 이제 방금 만든 사본을 지우고 오류를 낸다 (원본은 그대로). 같은 함수를 쓰는 app_controller 안의 다른 두 곳 (옆 자막 · jj_mkv 결과물 함께 옮기기) 도 시험으로 확인했다: 자막 · 결과물을 열어 둔 채 옮기면 동영상만 옮겨지고, 자막 · 결과물은 원래 자리에 하나만 남는다 (고치기 전 코드로는 이 시험이 실패하는 것도 확인). - move_test: 옮기는 중 목록에 없음 → 실패하면 복원 · 대상에 사본 없음, 함께 옮기는 파일의 실패. Co-Authored-By: Claude Opus 5.5 --- lib/app/app_controller.dart | 29 ++++++++++++++++--- test/move_test.dart | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index 7f2df08..87e7c47 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -525,6 +525,15 @@ class AppController extends ChangeNotifier { errors.add(trf('{0}: 이미 이동 폴더에 있습니다', [v.fileName])); continue; } + // 옮기기 시작하면 바로 목록에서 뺀다 (다른 드라이브 · NAS 로는 복사라 큰 동영상은 몇 초 넘게 남아 보였다). + // 옮기지 못하면 원래 자리 · 표시 · 선택으로 되돌린다. + final at = videos.indexOf(v); + final wasChecked = checked.contains(v); + final wasSelected = selected == v; + videos.remove(v); + checked.remove(v); + if (wasSelected) selected = videos.elementAtOrNull(at) ?? videos.lastOrNull; + notifyListeners(); try { final side = { for (final s in v.subtitles) @@ -551,11 +560,15 @@ class AppController extends ChangeNotifier { if (results > 0) trf(' (결과물 {0}개 함께)', [results]), ].join(), ])); - videos.remove(v); - checked.remove(v); moved++; } catch (e) { errors.add('${v.fileName}: $e'); + if (File(v.path).existsSync() && !videos.contains(v)) { + videos.insert(at.clamp(0, videos.length), v); + if (wasChecked) checked.add(v); + if (wasSelected) selected = v; + notifyListeners(); + } } } if (selected != null && !videos.contains(selected)) selected = videos.firstOrNull; @@ -594,8 +607,16 @@ class AppController extends ChangeNotifier { try { await File(from).rename(to); } on FileSystemException { - await File(from).copy(to); - await File(from).delete(); + try { + await File(from).copy(to); + await File(from).delete(); + } catch (_) { + // 원본을 지우지 못하면 (열려 있음 등) 옮긴 것이 아니다 - 방금 만든 사본을 지워 두 벌이 남지 않게 (원본은 그대로) + try { + if (File(from).existsSync()) await File(to).delete(); + } catch (_) {} + rethrow; + } } return to; } diff --git a/test/move_test.dart b/test/move_test.dart index b60c130..368d492 100644 --- a/test/move_test.dart +++ b/test/move_test.dart @@ -61,6 +61,61 @@ void main() { expect(c.selected, busy); }); + test('옮기기 시작하면 바로 목록에서 빠지고, 옮기지 못하면 원래 자리 · 표시 · 선택으로 돌아온다', () async { + final c = _controller(); + final dest = p.join(dir.path, 'done'); + final a = VideoItem(make('a.mp4').path); + final b = VideoItem(make('b.mp4').path); + final d = VideoItem(make('d.mp4').path); + c.videos.addAll([a, b, d]); + c.checked.add(b); + c.selected = b; + // 처음 알릴 때 (옮기는 중) 이미 목록에 없다 + List? whileMoving; + void first() => whileMoving ??= c.videos.map((v) => v.fileName).toList(); + c.addListener(first); + // Windows: 열려 있는 파일은 옮길 수 없다 + final h = File(b.path).openSync(mode: FileMode.append); + try { + final (moved, errors) = await c.moveVideos([b], dest); + expect(moved, 0); + expect(errors.single, contains('b.mp4')); + } finally { + h.closeSync(); + c.removeListener(first); + } + expect(whileMoving, ['a.mp4', 'd.mp4']); + expect(c.videos, [a, b, d]); + expect(c.checked, {b}); + expect(c.selected, b); + expect(File(b.path).existsSync(), isTrue); + expect(File(p.join(dest, 'b.mp4')).existsSync(), isFalse, reason: '옮기다 만 사본을 남기지 않는다'); + }, skip: !Platform.isWindows); + + test('함께 옮기는 자막 · 결과물을 옮기지 못하면 원본은 그대로 두고 대상에 사본을 남기지 않는다', () async { + final c = _controller(); + final dest = p.join(dir.path, 'done'); + final a = VideoItem(make('a.mp4').path) + ..subtitles.add(SubtitleEntry.external(path: make('a.ko.srt').path, language: undetermined)); + make(p.join('jj_mkv', 'a.mkv')); + c.videos.add(a); + // Windows: 열려 있는 파일은 옮길 수 없다 (옆 자막 · jj_mkv 결과물) + final h1 = File(p.join(dir.path, 'a.ko.srt')).openSync(mode: FileMode.append); + final h2 = File(p.join(dir.path, 'jj_mkv', 'a.mkv')).openSync(mode: FileMode.append); + try { + final (moved, _) = await c.moveVideos([a], dest); + expect(moved, 1, reason: '동영상은 옮긴다'); + } finally { + h1.closeSync(); + h2.closeSync(); + } + expect(File(p.join(dest, 'a.mp4')).existsSync(), isTrue); + expect(File(p.join(dir.path, 'a.ko.srt')).existsSync(), isTrue); + expect(File(p.join(dest, 'a.ko.srt')).existsSync(), isFalse, reason: '자막 사본이 두 벌로 남지 않는다'); + expect(File(p.join(dir.path, 'jj_mkv', 'a.mkv')).existsSync(), isTrue); + expect(File(p.join(dest, 'jj_mkv', 'a.mkv')).existsSync(), isFalse, reason: '결과물 사본이 두 벌로 남지 않는다'); + }, skip: !Platform.isWindows); + test('목록에 다시 넣으면 (앱을 다시 켜도) 전에 만든 자막 (jj_mkv 의 파일명_ko.srt 등) 이 다시 붙는다', () async { final c = _controller(); final video = make('강의.mp4').path;