폰 세로 모니터링 버튼 줄 넘침 (257) · 지운 쌍의 "동기화 멈춤" 알림이 남던 것 (258)

a4 의 934 기기 확인 · 관리자 지시 (10/12, 버그 수정):
- 257: 폰 세로 모니터링 > lsync 카드의 버튼 줄이 화면 밖으로 넘쳐 [고치기] · [지우기] 를 누를 수 없었다 → Wrap 으로 줄 바꿈
  (복사 카드 버튼 줄도). 같은 폭 (411) 시험에서 함께 넘치던 곳도 고침: lsync 탭 위 "다시 켤 때 동기화" 줄,
  카드 머리의 방식 칩 (남는 폭만큼 · … · 전체는 마우스), 일정 글.
- 258: 멈춘 lsync 쌍을 지워도 작업 알림 "⚠ 동기화 멈춤 1개" 가 앱을 다시 켤 때까지 남았다 → 쌍 목록이 바뀌면
  지켜보지 않는 쌍 (지움 · 끔 · 멈춤) 의 멈춤 기록을 지운다.
- 시험: 411 폭에서 넘침 없음 · 버튼이 화면 안 (고치기 전에는 실패) · 지우면 멈춤 기록이 바로 사라짐.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
통합
RyuJongJin 2026-10-12 08:15:38 +09:00
parent 4ad66dd5a1
commit 031e8d3f84
4 changed files with 68 additions and 18 deletions

View File

@ -322,6 +322,9 @@ class LiveSync extends ChangeNotifier {
if (sig == _sig) return;
_sig = sig;
_stopAll();
// 258: 지우거나 끈 · 멈춘 쌍의 "원본을 읽지 못해 멈춤" 은 지운다 (작업 알림 "⚠ 동기화 멈춤 N개" 가 앱을 다시 켤 때까지 남았다)
final keys = pairs.map(keyOf).toSet();
problems.removeWhere((k, _) => !keys.contains(k));
for (final x in pairs) {
final k = keyOf(x);
if (!isDav(x.source) && !Directory(x.source).existsSync()) {

View File

@ -517,9 +517,9 @@ class LiveSyncRunOptions extends StatelessWidget {
]),
),
),
Row(mainAxisSize: MainAxisSize.min, children: [
// 257: 폰 세로에서 넘쳤다 - 모자라면 고르기 상자를 아래 줄로
Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
Text(tr('다시 켤 때 동기화'), style: const TextStyle(fontSize: 13, color: JjColors.textDim)),
const SizedBox(width: 8),
onStart,
]),
]);

View File

@ -642,8 +642,8 @@ class _CopyCardState extends State<_CopyCard> {
]),
),
const SizedBox(height: 6),
Row(children: [
const Spacer(),
// 257: 폰 세로에서 버튼이 화면 밖으로 넘치지 않게 줄을 바꾼다 (오른쪽 정렬은 그대로)
Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, spacing: 2, runSpacing: 2, children: [
if (running)
TextButton.icon(onPressed: () => center.cancel(t.id), icon: const Icon(Icons.stop, size: 18), label: Text(tr('취소')))
else
@ -652,7 +652,6 @@ class _CopyCardState extends State<_CopyCard> {
icon: const Icon(Icons.play_arrow, size: 18),
label: Text(t.lastResult == 'cancelled' || t.lastResult == 'failed' ? tr('재개') : tr('실행')),
),
const SizedBox(width: 6),
TextButton.icon(
onPressed: () => _toLive(t),
icon: const Icon(Icons.sync_alt, size: 18),
@ -959,10 +958,17 @@ class _LiveTabState extends State<_LiveTab> {
),
),
),
Chip(
visualDensity: VisualDensity.compact,
// 243: 실시간 동기화는 늘 복사로 맞춘다 (원본은 지우지 않음 - 원본을 비우려면 [최종 정리])
label: Text([tr(CopyMethod.of(x.method).label), tr('복사로 맞춤'), if (x.delete) tr('지우기 포함')].join(' · ')),
// 257: 폰 세로에서 이 줄이 넘쳤다 - 칩은 남는 폭만큼 (넘치면 … 로 줄임, 전체는 길게 누르기 · 마우스)
Flexible(
child: Tooltip(
message: [tr(CopyMethod.of(x.method).label), tr('복사로 맞춤'), if (x.delete) tr('지우기 포함')].join(' · '),
child: Chip(
visualDensity: VisualDensity.compact,
// 243: 실시간 동기화는 늘 복사로 맞춘다 (원본은 지우지 않음 - 원본을 비우려면 [최종 정리])
label: Text([tr(CopyMethod.of(x.method).label), tr('복사로 맞춤'), if (x.delete) tr('지우기 포함')].join(' · '),
maxLines: 1, overflow: TextOverflow.ellipsis),
),
),
),
// 이번 실행 동안 멈춤 / 시작 (설정의 켜기 · 끄기는 오른쪽 스위치)
if (live != null && x.enabled)
@ -1008,12 +1014,15 @@ class _LiveTabState extends State<_LiveTab> {
child: Row(mainAxisSize: MainAxisSize.min, children: [
WeekGridPreview(lines: x.schedule, width: 120),
const SizedBox(width: 8),
Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Text(scheduleSummary(x.schedule), style: TextStyle(fontSize: 12, color: active ? JjColors.accent : Colors.orangeAccent)),
if (x.schedule.isNotEmpty)
Text(x.schedule.join(' | '), style: const TextStyle(fontSize: 11, fontFamily: 'monospace', color: JjColors.textDim)),
Text(tr('누르면 일정 바꾸기'), style: const TextStyle(fontSize: 10, color: JjColors.textDim)),
]),
// 257: 폰 세로에서 글이 줄을 바꿀 수 있게 (넘쳤다)
Flexible(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Text(scheduleSummary(x.schedule), style: TextStyle(fontSize: 12, color: active ? JjColors.accent : Colors.orangeAccent)),
if (x.schedule.isNotEmpty)
Text(x.schedule.join(' | '), style: const TextStyle(fontSize: 11, fontFamily: 'monospace', color: JjColors.textDim)),
Text(tr('누르면 일정 바꾸기'), style: const TextStyle(fontSize: 10, color: JjColors.textDim)),
]),
),
]),
),
),
@ -1060,8 +1069,8 @@ class _LiveTabState extends State<_LiveTab> {
]),
),
const SizedBox(height: 6),
Row(children: [
const Spacer(),
// 257: 폰 세로에서 [고치기] · [지우기] 가 화면 밖으로 밀려 누를 수 없었다 - 줄을 바꾼다 (오른쪽 정렬은 그대로)
Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, spacing: 2, runSpacing: 2, children: [
TextButton.icon(
onPressed: live == null ? null : () => live.refreshPending(x),
icon: const Icon(Icons.refresh, size: 18),
@ -1072,7 +1081,6 @@ class _LiveTabState extends State<_LiveTab> {
icon: const Icon(Icons.sync, size: 18),
label: Text(tr('지금 맞추기')),
),
const SizedBox(width: 6),
TextButton.icon(
onPressed: running ? null : () => _finalize(x),
icon: const Icon(Icons.cleaning_services_outlined, size: 18),

View File

@ -61,6 +61,18 @@ void main() {
live.dispose();
});
test('258: 멈춘 쌍을 지우면 "동기화 멈춤" 도 바로 사라진다 (앱을 다시 켜지 않아도)', () async {
final pair = LiveSyncPair(src, dst, delete: true).copyWith(deleteConfirmed: true);
c.settings.liveSyncPairs = [pair];
final live = LiveSync(c)..start();
final k = LiveSync.keyOf(pair);
await live.syncNow(pair);
expect(live.problems[k], isNotNull);
await c.updateSettings((s) => s.liveSyncPairs = []);
expect(live.problems, isEmpty);
live.dispose();
});
test('70: 원본이 정말 비었으면 멈추고 지울 목록 → [그래도 맞추기] 로 그 한 번만 지운다', () async {
Directory(src).createSync();
final pair = LiveSyncPair(src, dst, delete: true).copyWith(deleteConfirmed: true);
@ -138,6 +150,33 @@ void main() {
expect(find.text('원본 폴더가 비어 있어 멈췄습니다'), findsNothing);
});
testWidgets('257: 폰 세로 (411 폭) 에서 lsync 카드의 [고치기] · [지우기] 가 화면 안에 있어 누를 수 있다', (tester) async {
Directory(src).createSync();
File(p.join(src, 'x.txt')).writeAsStringSync('x');
final pair = LiveSyncPair(src, dst);
c.settings.liveSyncPairs = [pair];
final live = LiveSync(c);
LiveSync.instance = live;
addTearDown(() {
LiveSync.instance = null;
live.dispose();
});
tester.view.physicalSize = const Size(411, 800);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c, initialTab: 1)));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull, reason: '넘치지 않음');
for (final tip in ['다시 세기', '지금 맞추기', '최종 정리', '복사 · rsync 로 이동']) {
final r = tester.getRect(find.text(tip));
expect(r.right <= 411 && r.left >= 0, isTrue, reason: '$tip $r');
}
for (final tip in ['고치기', '지우기']) {
final r = tester.getRect(find.byTooltip(tip).first);
expect(r.right <= 411 && r.left >= 0, isTrue, reason: '$tip $r 이 화면 밖');
}
});
testWidgets('93: Rsync --delete 실행은 비교가 끝나 지울 목록이 보인 뒤에만 [실행] 이 켜진다', (tester) async {
final l = p.join(tmp.path, 'L'), r = p.join(tmp.path, 'R');
File(p.join(l, 'sub', 'a.txt'))