From 81e5906f76c0a935c942ec04c11e00ff27a012d7 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:32:22 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20X-plore=20=EB=B9=84=EA=B5=90=205-1=20?= =?UTF-8?q?=E2=91=A0=20=EC=A2=85=EB=A5=98=EB=B3=84=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20(=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=A7=80?= =?UTF-8?q?=EC=8B=9C=2010/21):=20=ED=99=98=EA=B2=BD=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20>=20=ED=8C=8C=EC=9D=BC=20=ED=83=90=EC=83=89=EA=B8=B0=20"?= =?UTF-8?q?=EC=A2=85=EB=A5=98=EB=B3=84=20=EA=B8=B0=EB=B3=B8=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC"=20(=EC=B2=98=EC=9D=8C=20=EB=81=94=20-=20=EC=A7=80?= =?UTF-8?q?=EA=B8=88=20=EC=A0=95=EB=A0=AC=20=EA=B7=B8=EB=8C=80=EB=A1=9C).?= =?UTF-8?q?=20=EC=BC=9C=EB=A9=B4=20=ED=8C=8C=EC=9D=BC=20=EC=A4=91=20?= =?UTF-8?q?=EB=B0=98=20=EB=84=98=EA=B2=8C=EA=B0=80=20=EC=82=AC=EC=A7=84=20?= =?UTF-8?q?=C2=B7=20=EB=8F=99=EC=98=81=EC=83=81=EC=9D=B8=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EB=8A=94=20=EB=82=A0=EC=A7=9C=20(=EC=83=88=EA=B2=83?= =?UTF-8?q?=20=EB=A8=BC=EC=A0=80=20-=20=EB=82=B4=20=ED=8C=90=EB=8B=A8:=20?= =?UTF-8?q?=EA=B0=A4=EB=9F=AC=EB=A6=AC=EC=B2=98=EB=9F=BC),=20=EC=9D=8C?= =?UTF-8?q?=EC=95=85=20=ED=8F=B4=EB=8D=94=EB=8A=94=20=EC=9D=B4=EB=A6=84,?= =?UTF-8?q?=20=EA=B7=B8=20=EB=B0=96=EC=9D=80=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC.=20=EA=B7=B8=20=ED=8F=B4=EB=8D=94=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A0=95=EB=A0=AC=EC=9D=84=20=EB=B0=94=EA=BE=B8?= =?UTF-8?q?=EB=A9=B4=20=EA=B7=B8=20=ED=8F=B4=EB=8D=94=EB=A7=8C=20=EA=B8=B0?= =?UTF-8?q?=EC=96=B5=20(=EC=A0=84=EC=B2=B4=20=EC=A0=95=EB=A0=AC=EC=9D=84?= =?UTF-8?q?=20=EB=B0=94=EA=BE=B8=EB=A9=B4=20=EB=88=8C=EB=9F=AC=EB=8F=84=20?= =?UTF-8?q?=EC=95=88=20=EB=B0=94=EB=80=90=20=EA=B2=83=EC=B2=98=EB=9F=BC=20?= =?UTF-8?q?=EB=B3=B4=EC=9D=B4=EB=AF=80=EB=A1=9C)=20=C2=B7=20=EC=BC=9C?= =?UTF-8?q?=EA=B3=A0=20=EB=81=84=EB=A9=B4=20=EC=9D=BD=EC=96=B4=20=EB=91=94?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=EC=9D=84=20=EB=8B=A4=EC=8B=9C=20=EC=A4=84?= =?UTF-8?q?=20=EC=84=B8=EC=9B=80=20=C2=B7=20=EC=8B=9C=ED=97=98=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 Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 2 + assets/l10n/ja.json | 2 + assets/l10n/zh-Hans.json | 2 + lib/app/settings.dart | 6 ++ lib/ui/explorer_page.dart | 48 +++++++++++-- lib/ui/settings_page.dart | 9 +++ test/explorer_sort_kind_test.dart | 114 ++++++++++++++++++++++++++++++ 7 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 test/explorer_sort_kind_test.dart diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 346c9e7..293d628 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -1540,6 +1540,7 @@ "사용자가 취소했습니다.": "Cancelled by the user.", "사용할 수 있는 자막 사이트가 없습니다.": "No subtitle site is available.", "사진": "Photos", + "사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름으로. 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억합니다": "Folders that are mostly photos or videos sort by date (newest first), music folders by name. If you change the sort in such a folder, that folder remembers it", "사진 슬라이드쇼": "Photo slideshow", "사진 정보 (Exif)": "Photo info (Exif)", "사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "The 'Photos' model is better for photos, but on this device (no graphics card) it takes about 20 minutes per image.", @@ -2411,6 +2412,7 @@ "종료 창 닫힘": "Quit window closed", "종료할까요?": "Quit?", "종류 (확장자)": "Type (extension)", + "종류별 기본 정렬": "Sort by content type", "좌 ← 우": "Left ← right", "좌 → 우": "Left → right", "좌 ⇄ 우": "Left ⇄ right", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 9b1decb..bbeed8c 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -1540,6 +1540,7 @@ "사용자가 취소했습니다.": "ユーザーがキャンセルしました。", "사용할 수 있는 자막 사이트가 없습니다.": "使用できる字幕サイトがありません。", "사진": "写真", + "사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름으로. 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억합니다": "写真・動画が多いフォルダーは日付 (新しい順)、音楽が多いフォルダーは名前で並べます。そのフォルダーで並べ替えを変えると、そのフォルダーは変えた並べ替えを覚えます", "사진 슬라이드쇼": "写真スライドショー", "사진 정보 (Exif)": "写真情報 (Exif)", "사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "写真には「写真」モデルが適していますが、この端末 (グラフィックカードなし) では 1 枚に約 20 分かかります。", @@ -2411,6 +2412,7 @@ "종료 창 닫힘": "終了ウィンドウを閉じました", "종료할까요?": "終了しますか?", "종류 (확장자)": "種類 (拡張子)", + "종류별 기본 정렬": "種類別の既定の並べ替え", "좌 ← 우": "左 ← 右", "좌 → 우": "左 → 右", "좌 ⇄ 우": "左 ⇄ 右", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 45a0ce0..43a2cee 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -1540,6 +1540,7 @@ "사용자가 취소했습니다.": "用户已取消。", "사용할 수 있는 자막 사이트가 없습니다.": "没有可用的字幕网站。", "사진": "照片", + "사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름으로. 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억합니다": "照片和视频较多的文件夹按日期 (新的在前),音乐较多的文件夹按名称排序。在该文件夹中更改排序后,该文件夹会记住所选排序", "사진 슬라이드쇼": "照片幻灯片", "사진 정보 (Exif)": "照片信息 (Exif)", "사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "照片更适合使用“照片”模型,但在本设备 (无显卡) 上每张约需 20 分钟。", @@ -2411,6 +2412,7 @@ "종료 창 닫힘": "退出窗口已关闭", "종료할까요?": "要退出吗?", "종류 (확장자)": "类型 (扩展名)", + "종류별 기본 정렬": "按类型默认排序", "좌 ← 우": "左 ← 右", "좌 → 우": "左 → 右", "좌 ⇄ 우": "左 ⇄ 右", diff --git a/lib/app/settings.dart b/lib/app/settings.dart index e2ae8f1..07ef0f9 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -469,6 +469,10 @@ class AppSettings { /// 84: 정렬을 폴더마다 기억 (처음: 끔 - 모든 폴더가 같은 정렬). 켜면 열 머리 · 정렬 창이 지금 폴더에만 쓰고 [explorerFolderSorts] 에 둔다 bool explorerSortPerFolder = false; + /// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 - 사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름. + /// 처음 끔 (지금 정렬 그대로). 켜도 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억한다 ([explorerFolderSorts]) + bool explorerSortByKind = false; + /// 84: 폴더 → "기준|desc" (예: "size|1"). 오래된 것부터 지워 300개까지 Map explorerFolderSorts = {}; @@ -866,6 +870,7 @@ class AppSettings { 'wifiShareTailscale': wifiShareTailscale, 'wifiShareFolder': wifiShareFolder, 'explorerSortPerFolder': explorerSortPerFolder, + 'explorerSortByKind': explorerSortByKind, 'explorerFolderSorts': explorerFolderSorts, 'explorerShowHidden': explorerShowHidden, 'explorerHiddenItems': explorerHiddenItems, @@ -1108,6 +1113,7 @@ class AppSettings { ..explorerSortDesc = j['explorerSortDesc'] as bool? ?? false ..explorerFolderSize = const ['sort', 'always', 'off'].contains(j['explorerFolderSize']) ? j['explorerFolderSize'] as String : 'sort' ..explorerSortPerFolder = j['explorerSortPerFolder'] == true + ..explorerSortByKind = j['explorerSortByKind'] == true ..explorerProgressHidden = j['explorerProgressHidden'] == true ..zipLevel = const [1, 6, 9].contains(j['zipLevel']) ? j['zipLevel'] as int : 6 ..wifiShareMinutes = const [0, 10, 30, 60, 120].contains(j['wifiShareMinutes']) ? j['wifiShareMinutes'] as int : 30 diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 95ddffc..67348c9 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -432,6 +432,7 @@ class _ExplorerPageState extends State { } _resumeReload = AppLifecycleListener(onResume: () => unawaited(_reloadOpen(localOnly: true))); _hiddenSeen = c.settings.explorerHiddenItems; + _kindSeen = c.settings.explorerSortByKind; c.addListener(_onSettings); _init(); } @@ -988,7 +989,7 @@ class _ExplorerPageState extends State { /// [dir] 의 목록을 정렬. 84: "폴더마다 기억" 이 켜져 있으면 그 폴더의 정렬 (없으면 전체 정렬) List _sorted(List list, String dir) { - final (sortBy, desc) = _sortOf(dir); + final (sortBy, desc) = _sortOf(dir, list: list); final by = SortBy.values.firstWhere((s) => s.name == sortBy, orElse: () => SortBy.name); return sortEntries(list, by, descending: desc, dirSize: (d) => _sizes.of(d.path)?.bytes); } @@ -996,19 +997,43 @@ class _ExplorerPageState extends State { static String _sortKey(String dir) => isDav(dir) ? vNorm(dir) : (Platform.isWindows ? dir.toLowerCase() : dir); /// 84: [dir] 의 정렬 (기준, 거꾸로) - (String, bool) _sortOf(String dir) { + (String, bool) _sortOf(String dir, {List? list}) { final s = c.settings; - if (s.explorerSortPerFolder) { + // 폴더마다 기억한 것 (종류별 기본 정렬을 켰을 때 그 폴더에서 바꾼 것도) 이 먼저 + if (s.explorerSortPerFolder || s.explorerSortByKind) { final v = s.explorerFolderSorts[_sortKey(dir)]; if (v != null && v.contains('|')) return (v.split('|').first, v.endsWith('|1')); } + if (s.explorerSortByKind) { + final k = _kindSort(dir, list: list); + if (k != null) return k; + } return (s.explorerSort, s.explorerSortDesc); } + /// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 - 읽어 둔 목록의 파일 중 반 넘게가 사진 · 동영상이면 날짜 (새것 먼저 - + /// 내 판단: 갤러리처럼), 음악이면 이름. 그 밖 · 아직 읽지 않은 폴더는 null (전체 정렬). [list] 지금 줄 세우는 목록 (없으면 읽어 둔 것) + (String, bool)? _kindSort(String dir, {List? list}) { + if (list == null) { + for (final p in _panes) { + list = p.cache[dir]; + if (list != null) break; + } + } + final files = [for (final e in list ?? const []) if (!e.isDir) e]; + if (files.isEmpty) return null; + final media = files.where((e) => c.settings.imageExts.contains(e.ext) || isVideoFile(e.path)).length; + if (media * 2 > files.length) return ('date', true); + if (files.where((e) => isAudioFile(e.path)).length * 2 > files.length) return ('name', false); + return null; + } + /// 84: 정렬을 바꾼다 - "폴더마다 기억" 이면 [dir] 에만, 아니면 전체. 모든 창의 목록을 다시 줄 세운다 Future _setSort(String dir, String by, bool desc) async { + // 종류별 기본 정렬이 그 폴더에 쓰이고 있으면 바꾼 정렬은 그 폴더에 (전체 정렬을 바꾸면 이 폴더는 그대로라 눌러도 안 바뀐 것처럼 보인다) + final kindHere = c.settings.explorerSortByKind && _kindSort(dir) != null; await c.updateSettings((x) { - if (x.explorerSortPerFolder) { + if (x.explorerSortPerFolder || kindHere) { final m = Map.of(x.explorerFolderSorts)..remove(_sortKey(dir)); m[_sortKey(dir)] = '$by|${desc ? 1 : 0}'; while (m.length > 300) { @@ -1321,9 +1346,22 @@ class _ExplorerPageState extends State { /// 설정 화면에서 숨긴 목록을 바꾸면 (풀기) 바로 목록에 보이게 List? _hiddenSeen; + /// 종류별 기본 정렬 (5-1 ①) 을 켜고 끈 것 - 바뀌면 읽어 둔 목록을 다시 줄 세운다 + bool? _kindSeen; + void _onSettings() { + if (!mounted) return; + if (_kindSeen != null && _kindSeen != c.settings.explorerSortByKind) { + for (final pane in _panes) { + for (final k in pane.cache.keys.toList()) { + pane.cache[k] = _sorted(pane.cache[k]!, k); + } + pane.changed(); + } + } + _kindSeen = c.settings.explorerSortByKind; final now = c.settings.explorerHiddenItems; - if (identical(now, _hiddenSeen) || !mounted) return; + if (identical(now, _hiddenSeen)) return; _hiddenSeen = now; setState(() {}); for (final q in _panes) { diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 3708e9a..97d608e 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -795,6 +795,15 @@ class _SettingsPageState extends State { onChanged: (v) => c.updateSettings((x) => x.explorerSortPerFolder = v), ), ), + // X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 (처음: 끔) + SettingTile( + title: Text(tr('종류별 기본 정렬')), + subtitle: Text(tr('사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름으로. 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억합니다')), + trailing: Switch( + value: s.explorerSortByKind, + onChanged: (v) => c.updateSettings((x) => x.explorerSortByKind = v), + ), + ), // 80: 폴더 크기 (필요할 때 계산하고 이번 실행 동안 기억) SettingTile( title: Text(tr('폴더 크기')), diff --git a/test/explorer_sort_kind_test.dart b/test/explorer_sort_kind_test.dart new file mode 100644 index 0000000..a770392 --- /dev/null +++ b/test/explorer_sort_kind_test.dart @@ -0,0 +1,114 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/app/app_controller.dart'; +import 'package:jj_mkvmaker/app/settings.dart'; +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'; +import 'package:jj_mkvmaker/ui/explorer_page.dart'; +import 'package:path/path.dart' as p; + +/// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 - 처음 끔 (지금 정렬 그대로), 켜면 사진 · 동영상 폴더는 날짜 (새것 먼저) · +/// 음악 폴더는 이름, 그 폴더에서 정렬을 바꾸면 그 폴더만 기억 +void main() { + late Directory tmp; + late String photos, music, docs; + setUp(() { + tmp = Directory.systemTemp.createTempSync('jj_sortkind_'); + photos = p.join(tmp.path, 'photos'); + music = p.join(tmp.path, 'music'); + docs = p.join(tmp.path, 'docs'); + // 사진: 이름 순과 날짜 순이 다르게 (c 가 가장 새것, a 가 가장 옛것) + for (final (n, day) in [('a.jpg', 1), ('b.jpg', 3), ('c.mp4', 5), ('d.jpg', 2)]) { + File(p.join(photos, n)) + ..createSync(recursive: true) + ..writeAsStringSync(n) + ..setLastModifiedSync(DateTime(2026, 1, day)); + } + for (final (n, day) in [('z.mp3', 1), ('y.mp3', 5), ('x.mp3', 3)]) { + File(p.join(music, n)) + ..createSync(recursive: true) + ..writeAsStringSync(n) + ..setLastModifiedSync(DateTime(2026, 1, day)); + } + for (final (n, day) in [('q.txt', 1), ('p.txt', 5)]) { + File(p.join(docs, n)) + ..createSync(recursive: true) + ..writeAsStringSync(n) + ..setLastModifiedSync(DateTime(2026, 1, day)); + } + }); + tearDown(() => tmp.deleteSync(recursive: true)); + + Future open(WidgetTester tester, String dir, String wait, {bool kind = false, String sort = 'name'}) async { + tester.view.physicalSize = const Size(1400, 900); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + const pp = MethodChannel('plugins.flutter.io/path_provider'); + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, (_) async => tmp.path); + addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, null)); + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + c.settings + ..explorerPaths = [dir, dir] + ..explorerLayout = 'split' + ..explorerStyle = 'windows' + ..explorerSort = sort + ..explorerSortByKind = kind; + await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c)))); + for (var i = 0; i < 300 && (i < 10 || find.text(wait).evaluate().isEmpty); i++) { + await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); + await tester.pump(); + } + return c; + } + + /// 오른쪽 목록의 이름을 위에서 아래 순서로 (Windows 모양은 확장자를 붙인 이름) + List order(WidgetTester tester, List names) { + final ys = {for (final n in names) n: tester.getTopLeft(find.text(n).last).dy}; + return names.toList()..sort((a, b) => ys[a]!.compareTo(ys[b]!)); + } + + Future close(WidgetTester tester) async { + await tester.pumpWidget(const SizedBox()); + await tester.pump(const Duration(seconds: 1)); + } + + test('설정: 처음 끔 · 저장 · 읽기 · 예전 설정 (값 없음) 은 끔', () { + expect(AppSettings().explorerSortByKind, isFalse); + expect(AppSettings.fromJson((AppSettings()..explorerSortByKind = true).toJson()).explorerSortByKind, isTrue); + expect(AppSettings.fromJson({}).explorerSortByKind, isFalse); + }); + + testWidgets('끄면 (처음) 사진 폴더도 지금 정렬 (이름) 그대로', (tester) async { + await open(tester, photos, 'a.jpg'); + expect(order(tester, ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg']), ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg']); + await close(tester); + }); + + testWidgets('켜면 사진 · 동영상 폴더는 날짜 (새것 먼저) · 그 폴더에서 [이름] 을 누르면 그 폴더만 기억 (전체 정렬은 그대로)', (tester) async { + final c = await open(tester, photos, 'a.jpg', kind: true); + expect(order(tester, ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg']), ['c.mp4', 'b.jpg', 'd.jpg', 'a.jpg']); + // 열 머리 [이름] → 이 폴더는 이름 순, 전체 정렬 설정은 바뀌지 않음 + await tester.tap(find.byTooltip('이름 순으로 정렬')); + for (var i = 0; i < 10; i++) { + await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); + await tester.pump(); + } + expect(order(tester, ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg']), ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg'], reason: '누른 것이 바로 보인다'); + expect(c.settings.explorerFolderSorts.values, contains('name|0')); + expect((c.settings.explorerSort, c.settings.explorerSortDesc), ('name', false), reason: '전체 정렬은 그대로'); + await close(tester); + }); + + testWidgets('켜도 음악 폴더는 이름 · 그 밖의 폴더는 전체 정렬 (날짜로 정해 두었으면 날짜)', (tester) async { + await open(tester, music, 'x.mp3', kind: true, sort: 'date'); + expect(order(tester, ['x.mp3', 'y.mp3', 'z.mp3']), ['x.mp3', 'y.mp3', 'z.mp3'], reason: '음악 = 이름'); + await close(tester); + await open(tester, docs, 'p.txt', kind: true, sort: 'date'); + expect(order(tester, ['p.txt', 'q.txt']), ['q.txt', 'p.txt'], reason: '글 폴더는 전체 정렬 (날짜, 옛것 먼저)'); + await close(tester); + }); +}