From 3ad8becafa535dbf69a5526c4bb799f2554c5097 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:09:32 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EC=95=88=EC=9D=98=20?= =?UTF-8?q?=EA=B2=83=EC=9D=84=20=EB=B0=94=EB=A1=9C=20=EC=9C=84=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EB=A1=9C=20=EC=9D=B4=EB=8F=99=EC=9D=B4=20=EC=A1=B0?= =?UTF-8?q?=EC=9A=A9=ED=9E=88=20=EC=95=88=20=EB=90=98=EB=8D=98=20=EA=B2=83?= =?UTF-8?q?=20(205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지적 "rsync 로 이동이 안 되는 경우가 있어" (버그 수정): Rsync 화면 (원본/ → 대상/) 에서 바로 위 폴더로 이동하면 실행 때의 검사가 폴더째 옮기는 규칙 ("이미 이 폴더에 있습니다") 으로 막아, 알림 없이 아무 일도 하지 않았다. 안의 것을 옮길 때 (contents) 는 이 검사를 하지 않는다. 그래도 시작하지 못하면 이유를 화면 · 작업 기록에 알린다. Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 3 +++ assets/l10n/ja.json | 3 +++ assets/l10n/zh-Hans.json | 3 +++ lib/app/copy_center.dart | 2 +- lib/core/file_ops.dart | 7 +++++-- lib/ui/explorer_page.dart | 9 ++++++++- lib/ui/monitor_page.dart | 2 +- test/monitor_test.dart | 16 ++++++++++++++++ 8 files changed, 40 insertions(+), 5 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 84d4c8f..6372734 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -285,9 +285,11 @@ "rsync 내려받기": "Download rsync", "rsync 는 처음 쓸 때 내려받습니다 (MSYS2 공식 저장소의 rsync 3.5.1 · 약 8MB, 파일마다 SHA256 확인).\n설정 폴더의 rsync 에 두어 업데이트해도 남습니다. 지금 받을까요?": "rsync is downloaded on first use (rsync 3.5.1 from the official MSYS2 repository · about 8MB, SHA256 checked for each file).\nIt is kept in the rsync folder of the settings folder and survives updates. Download now?", "rsync 를 받지 못했습니다: {0}": "Could not download rsync: {0}", + "rsync 를 실행할 수 없습니다": "rsync cannot be run", "rsync 를 항목마다 따로 실행하거나, 한 번에 실행 (robocopy 는 늘 폴더마다)": "Run rsync separately for each item, or once for all (robocopy always runs per folder)", "rsync 받는 중": "Downloading rsync", "rsync 시작 ({0}): {1} → {2}": "rsync started ({0}): {1} → {2}", + "rsync 시작하지 못함: {0}": "rsync did not start: {0}", "rsync 실행 파일": "rsync executable", "rsync 실행 파일을 찾을 수 없습니다. 환경 설정 > Rsync 에서 확인하세요.": "Cannot find the rsync executable. Check Settings > Rsync.", "rsync 실행 파일을 찾을 수 없습니다. 환경 설정 > 파일 탐색기에서 경로를 확인하세요.": "rsync executable not found. Check the path in Settings > File explorer.", @@ -1149,6 +1151,7 @@ "시작 안 함 (모니터링에서 시작)": "Don't start (start from Monitor)", "시작: {0} → {1}\\{2}{3}": "Started: {0} → {1}\\{2}{3}", "시작하는 중…": "Starting…", + "시작하지 못했습니다: {0}": "Could not start: {0}", "시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "Choose which syncs to start. The others stay paused for this session (start them in Monitor > lsync).", "시작할 때 새 버전 확인 (하루 한 번)": "Check for a new version at startup (once a day)", "시작할 때마다 설정 창 보기": "Show the setup window at every start", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 2d68ded..d60b7f6 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -285,9 +285,11 @@ "rsync 내려받기": "rsync をダウンロード", "rsync 는 처음 쓸 때 내려받습니다 (MSYS2 공식 저장소의 rsync 3.5.1 · 약 8MB, 파일마다 SHA256 확인).\n설정 폴더의 rsync 에 두어 업데이트해도 남습니다. 지금 받을까요?": "rsync は初めて使うときにダウンロードします (MSYS2 公式リポジトリの rsync 3.5.1 · 約 8MB、ファイルごとに SHA256 を確認)。\n設定フォルダーの rsync に置くため、更新しても残ります。今ダウンロードしますか?", "rsync 를 받지 못했습니다: {0}": "rsync をダウンロードできませんでした: {0}", + "rsync 를 실행할 수 없습니다": "rsync を実行できません", "rsync 를 항목마다 따로 실행하거나, 한 번에 실행 (robocopy 는 늘 폴더마다)": "rsync を項目ごとに実行するか、まとめて 1 回で実行 (robocopy は常にフォルダーごと)", "rsync 받는 중": "rsync をダウンロード中", "rsync 시작 ({0}): {1} → {2}": "rsync 開始 ({0}): {1} → {2}", + "rsync 시작하지 못함: {0}": "rsync を開始できませんでした: {0}", "rsync 실행 파일": "rsync 実行ファイル", "rsync 실행 파일을 찾을 수 없습니다. 환경 설정 > Rsync 에서 확인하세요.": "rsync 実行ファイルが見つかりません。環境設定 > Rsync を確認してください。", "rsync 실행 파일을 찾을 수 없습니다. 환경 설정 > 파일 탐색기에서 경로를 확인하세요.": "rsync 実行ファイルが見つかりません。環境設定 > ファイルエクスプローラーでパスを確認してください。", @@ -1149,6 +1151,7 @@ "시작 안 함 (모니터링에서 시작)": "開始しない (モニタリングで開始)", "시작: {0} → {1}\\{2}{3}": "開始: {0} → {1}\\{2}{3}", "시작하는 중…": "開始中…", + "시작하지 못했습니다: {0}": "開始できませんでした: {0}", "시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "開始する同期を選んでください。選ばなかったものは今回の実行中は停止します (モニタリング > lsync で開始)。", "시작할 때 새 버전 확인 (하루 한 번)": "起動時に新しいバージョンを確認(1日1回)", "시작할 때마다 설정 창 보기": "起動するたびに設定ウィンドウを表示", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 066f1a3..bd42ed7 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -285,9 +285,11 @@ "rsync 내려받기": "下载 rsync", "rsync 는 처음 쓸 때 내려받습니다 (MSYS2 공식 저장소의 rsync 3.5.1 · 약 8MB, 파일마다 SHA256 확인).\n설정 폴더의 rsync 에 두어 업데이트해도 남습니다. 지금 받을까요?": "rsync 在首次使用时下载 (MSYS2 官方仓库的 rsync 3.5.1 · 约 8MB,逐个文件校验 SHA256)。\n保存在设置文件夹的 rsync 中,更新后仍保留。现在下载吗?", "rsync 를 받지 못했습니다: {0}": "无法下载 rsync: {0}", + "rsync 를 실행할 수 없습니다": "无法运行 rsync", "rsync 를 항목마다 따로 실행하거나, 한 번에 실행 (robocopy 는 늘 폴더마다)": "rsync 按项目分别执行,或一次执行 (robocopy 总是按文件夹)", "rsync 받는 중": "正在下载 rsync", "rsync 시작 ({0}): {1} → {2}": "rsync 开始 ({0}): {1} → {2}", + "rsync 시작하지 못함: {0}": "rsync 未能启动:{0}", "rsync 실행 파일": "rsync 可执行文件", "rsync 실행 파일을 찾을 수 없습니다. 환경 설정 > Rsync 에서 확인하세요.": "找不到 rsync 可执行文件。请检查 设置 > Rsync。", "rsync 실행 파일을 찾을 수 없습니다. 환경 설정 > 파일 탐색기에서 경로를 확인하세요.": "找不到 rsync 可执行文件。请在 设置 > 文件浏览器 中检查路径。", @@ -1149,6 +1151,7 @@ "시작 안 함 (모니터링에서 시작)": "不开始 (在监控中开始)", "시작: {0} → {1}\\{2}{3}": "开始: {0} → {1}\\{2}{3}", "시작하는 중…": "正在开始…", + "시작하지 못했습니다: {0}": "无法开始:{0}", "시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "请选择要开始的同步。未选的在本次运行期间暂停 (可在 监控 > lsync 中开始)。", "시작할 때 새 버전 확인 (하루 한 번)": "启动时检查新版本(每天一次)", "시작할 때마다 설정 창 보기": "每次启动时显示设置窗口", diff --git a/lib/app/copy_center.dart b/lib/app/copy_center.dart index 4934a43..9bf81ca 100644 --- a/lib/app/copy_center.dart +++ b/lib/app/copy_center.dart @@ -95,7 +95,7 @@ class CopyCenter extends ChangeNotifier { /// 실행 · 재개. 원본 · 대상이 없거나 폴더를 자기 안으로 넣는 것이면 시작하지 않고 실패로 남긴다 (null). Future start(CopyTask t, {String? rsyncExe, NameConflict conflict = NameConflict.rename}) async { if (isRunning(t.id)) return jobs[t.id]; - final problem = transferProblem(t.sources, t.dest, move: t.move); + final problem = transferProblem(t.sources, t.dest, move: t.move, contents: t.contents); if (problem != null) { if (tasks.any((x) => x.id == t.id)) { await update(t.copyWith(lastRun: DateTime.now().toIso8601String(), lastResult: 'failed', lastMessage: problem)); diff --git a/lib/core/file_ops.dart b/lib/core/file_ops.dart index 8a15e63..69e47dc 100644 --- a/lib/core/file_ops.dart +++ b/lib/core/file_ops.dart @@ -152,7 +152,8 @@ bool isSameOrInside(String a, String b) { /// - 원본이 없음 (다른 곳에서 지워졌거나 옮겨짐) · 대상 폴더가 없음 /// - 폴더를 자기 자신 (또는 그 안) 으로 복사 · 이동 /// - 이미 그 폴더에 있는 것을 같은 폴더로 이동 -String? transferProblem(List sources, String dest, {required bool move}) { +/// [contents]: 폴더 "안의 것" 을 옮긴다 (Rsync 화면 · 원본/ → 대상/) - 바로 위 폴더로 옮기는 것도 된다 (205) +String? transferProblem(List sources, String dest, {required bool move, bool contents = false}) { // WebDAV 경로는 네트워크라 여기서 있는지 확인하지 않는다 (없으면 복사할 때 알림) final missing = [for (final s in sources) if (vMissingSync(s)) s]; if (missing.isNotEmpty) { @@ -163,7 +164,9 @@ String? transferProblem(List sources, String dest, {required bool move}) if ((isDav(s) || FileSystemEntity.isDirectorySync(s)) && isSameOrInside(dest, s)) { return trf('폴더를 자기 자신 안으로 {0} 수 없습니다: {1}', [move ? tr('옮길') : tr('복사할'), vBasename(s)]); } - if (move && samePath(vDirname(s), dest)) return trf('이미 이 폴더에 있습니다: {0}', [vBasename(s)]); + // 폴더째 옮길 때만: 이미 그 폴더 안에 있으면 옮길 것이 없다. + // 205: 안의 것을 바로 위 폴더로 올리는 이동 (Down\Movie/ → Down) 은 막지 않는다 (예전에는 조용히 시작하지 않았다) + if (move && !contents && samePath(vDirname(s), dest)) return trf('이미 이 폴더에 있습니다: {0}', [vBasename(s)]); } return null; } diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 245c0ef..576ed60 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -2236,7 +2236,14 @@ class _ExplorerPageState extends State { for (var i = 0; i < run.length; i++) { c.note(trf('rsync 시작 ({0}): {1} → {2}', [tr('Rsync 화면 확인 창의 [실행]'), run[i].sources.first, run[i].dest])); final j = await center.start(run[i].copyWith(options: shownOpts[i]), rsyncExe: dav ? null : exe); - if (j == null || !mounted) break; + if (!mounted) break; + if (j == null) { + // 205: 시작하지 못하면 이유를 알린다 (예전에는 알림 없이 끝나 "이동이 안 됨" 으로만 보였다) + final why = transferProblem(run[i].sources, run[i].dest, move: run[i].move, contents: run[i].contents); + _snack(trf('시작하지 못했습니다: {0}', [why ?? tr('rsync 를 실행할 수 없습니다')])); + c.note(trf('rsync 시작하지 못함: {0}', [why ?? ''])); + break; + } jobs.add(j); setState(() => _jobs = [..._jobs.where((x) => !x.finished), j]); await j.done; diff --git a/lib/ui/monitor_page.dart b/lib/ui/monitor_page.dart index 1e32cd6..607cb3f 100644 --- a/lib/ui/monitor_page.dart +++ b/lib/ui/monitor_page.dart @@ -348,7 +348,7 @@ class _CopyCardState extends State<_CopyCard> { return; } } - final problem = transferProblem(t.sources, t.dest, move: t.move); + final problem = transferProblem(t.sources, t.dest, move: t.move, contents: t.contents); await center.start(t, rsyncExe: exe); if (problem != null && mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(problem))); } diff --git a/test/monitor_test.dart b/test/monitor_test.dart index 76f54dc..0e16449 100644 --- a/test/monitor_test.dart +++ b/test/monitor_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/app/copy_center.dart'; +import 'package:jj_mkvmaker/core/file_ops.dart' show transferProblem; import 'package:jj_mkvmaker/app/live_sync.dart'; import 'package:jj_mkvmaker/app/settings.dart'; import 'package:jj_mkvmaker/core/cron_window.dart'; @@ -67,6 +68,21 @@ void main() { }); tearDown(() => tmp.deleteSync(recursive: true)); + test('205: 폴더 안의 것을 바로 위 폴더로 이동 (Rsync 화면 · 원본/ → 대상/) 은 막지 않고 옮긴다', () async { + final up = p.dirname(src); + // 폴더째 옮기기는 그대로 "이미 이 폴더에 있습니다", 안의 것은 된다 + expect(transferProblem([src], up, move: true), contains('이미 이 폴더에')); + expect(transferProblem([src], up, move: true, contents: true), isNull); + final center = CopyCenter(c); + final t = await center.remember([src], up, contents: true, method: 'builtin', move: true); + final job = await center.start(t); + expect(job, isNotNull, reason: '예전에는 여기서 null (조용히 시작하지 않음)'); + await job!.done; + expect(job.error, isNull); + expect(File(p.join(up, 'sub', 'b.txt')).readAsStringSync(), 'b'); + expect(Directory(src).listSync(recursive: true).whereType(), isEmpty, reason: '원본의 파일은 옮겨짐'); + }); + test('같은 복사는 기억한 옵션 · 실행 결과 기록 · lsync 로 이동 · 다시 복사 목록으로', () async { c.settings.copyMethodFolder = 'builtin'; // rsync 실행은 sync_tools_test (시험 환경에서는 path_provider 없음) final center = CopyCenter(c);