From 27259af83c58dc6f115f40d520193cc83efa51d8 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:00:47 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20[=EC=95=B1=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=84=EC=B2=B4]=20=EB=8A=94=20?= =?UTF-8?q?=EC=9D=B4=20=ED=8C=90=EC=9D=B4=20=EB=AA=A8=EB=A5=B4=EB=8A=94=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=82=A4=EB=8F=84=20=EC=A7=80=EC=9B=80=20?= =?UTF-8?q?=C2=B7=20=EB=8B=A4=EB=A5=B8=20=ED=8C=90=EC=9D=98=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=20=EA=B0=92=EC=9D=80=20[=EC=A0=80=EC=9E=A5=EB=90=9C?= =?UTF-8?q?=20=EC=8B=A4=EC=A0=9C=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=C2=B7=20API=20=ED=82=A4]=20=EB=A5=BC=20=EA=B3=A8=EB=9E=90?= =?UTF-8?q?=EC=9D=84=20=EB=95=8C=EB=A7=8C=20(=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=20=ED=8C=90=EB=8B=A8=2010/21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 까닭 (3a): 사용자가 직접 "전체" 를 골랐고, 새 판으로 돌아갔을 때 예전 값이 되살아나면 더 헷갈린다. 모르는 키를 남기는 규칙 (40) 은 보통 저장 때만 - resetAllSettings 는 AppSettings()..extra = {} 로 (뜻을 드러냄) - 안전 저장소: 이 판이 맡지 않는 열쇠 (os. · dav. · ai. · sftp. · smb. · ftp. · sec. 밖 = 다른 판이 쓴 것) 는 [저장된 실제 비밀번호 · API 키] 를 골랐을 때만 지움 (SettingsStore.clearForeignSecrets, clearStoredSecrets 끝에서). 마스터 · 최상 (sec.) 은 남김 - 초기화 창 설명에 한 줄씩: "다른 판에서 넣은 비밀번호 · 키도 지웁니다." · "다른 판에서 만든 설정도 지웁니다." (en · ja · zh, 새 줄로) - 시험: master_lock_test - 모르는 키 · 다른 판 열쇠가 있는 설정 → 보통 저장은 남김 → 전체 초기화는 키 없음 · 열쇠는 남음 → 저장된 비밀번호 지우기는 열쇠도 지움 · sec. 남김. 초기화 창 캡처 다시 (ko · en · ja × 412 · 960, 넘침 없음) 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 | 21 ++++++++++++++++++++- lib/ui/master_prompt.dart | 17 +++++++++++++---- test/master_lock_test.dart | 28 ++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 5 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index fa675dd..4c9115e 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -974,6 +974,8 @@ "다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다": "When you drop onto the other pane. Either way, the copy/move confirmation (same names etc.) still appears", "다른 창으로 복사": "Copy to other pane", "다른 창으로 이동": "Move to other pane", + "다른 판에서 넣은 비밀번호 · 키도 지웁니다.": "Passwords and keys saved by other versions are deleted too.", + "다른 판에서 만든 설정도 지웁니다.": "Settings made by other versions are deleted too.", "다른 폴더": "Other folder", "다른 폴더로 이동": "Move to another folder", "다른 프로그램이 쓰고 있거나 권한이 없을 수 있습니다. 나머지는 지웠습니다.": "Another program may be using them, or you may lack permission. The rest were deleted.", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index d61f0cc..1fec2a4 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -974,6 +974,8 @@ "다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다": "もう一方のウィンドウにドロップしたとき。どちらでもコピー・移動の確認 (同じ名前など) は表示されます", "다른 창으로 복사": "もう一方のペインへコピー", "다른 창으로 이동": "もう一方のペインへ移動", + "다른 판에서 넣은 비밀번호 · 키도 지웁니다.": "他のバージョンで保存したパスワード・キーも消します。", + "다른 판에서 만든 설정도 지웁니다.": "他のバージョンで作った設定も消します。", "다른 폴더": "別のフォルダー", "다른 폴더로 이동": "他のフォルダーに移動", "다른 프로그램이 쓰고 있거나 권한이 없을 수 있습니다. 나머지는 지웠습니다.": "他のプログラムが使用中か、権限がない可能性があります。残りは削除しました。", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 4819b3c..f426ed2 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -974,6 +974,8 @@ "다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다": "拖放到另一个窗格时。无论哪种,都会显示复制/移动确认 (同名等)", "다른 창으로 복사": "复制到另一窗格", "다른 창으로 이동": "移动到另一窗格", + "다른 판에서 넣은 비밀번호 · 키도 지웁니다.": "其他版本保存的密码和密钥也会删除。", + "다른 판에서 만든 설정도 지웁니다.": "其他版本创建的设置也会删除。", "다른 폴더": "其他文件夹", "다른 폴더로 이동": "移动到其他文件夹", "다른 프로그램이 쓰고 있거나 권한이 없을 수 있습니다. 나머지는 지웠습니다.": "可能有其他程序正在使用或没有权限。其余已删除。", diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 5ccbb90..aed7a6c 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -1682,12 +1682,31 @@ class SettingsStore { }); } + /// 이 판의 설정이 맡는 안전 저장소 열쇠 (그 밖은 마스터 · 최상 (sec.) 이나 이 판이 모르는 것 - 새 판이 쓴 열쇠) + static bool ownsSecret(String k) => + k.startsWith('os.') || k.startsWith('dav.') || k.startsWith('ai.') || k.startsWith('sftp.') || k.startsWith('smb.') || k.startsWith('ftp.'); + + /// 관리자 판단 (10/21): 이 판이 모르는 비밀 값 (다른 판이 쓴 열쇠) 을 지운다 - 사용자가 초기화 창에서 + /// [저장된 실제 비밀번호 · API 키] 를 골랐을 때만 부른다 (고르지 않았으면 남김). 마스터 · 최상 (sec.) 은 [마스터 비밀번호] 체크가 정해 여기서 남긴다. + /// 지운 열쇠 수 + Future clearForeignSecrets() async { + final all = await secrets.readAll(); + var n = 0; + for (final k in all.keys) { + if (k.startsWith('sec.') || ownsSecret(k)) continue; + await secrets.delete(k); + _known.remove(k); + n++; + } + return n; + } + /// 바뀐 비밀 값만 안전 저장소에 쓴다 (빈 값 · 지운 서버는 저장소에서도 지움). 저장소에 넣지 못한 열쇠를 돌려준다. Future> _writeSecrets(Map values) async { if (!_secretsOk) return {...values.keys.where((k) => _known[k] != values[k])}; final unsaved = {}; for (final k in {..._known.keys, ...values.keys}) { - if (!k.startsWith('os.') && !k.startsWith('dav.') && !k.startsWith('ai.') && !k.startsWith('sftp.') && !k.startsWith('smb.') && !k.startsWith('ftp.')) continue; + if (!ownsSecret(k)) continue; final v = values[k]; if (v == _known[k]) continue; try { diff --git a/lib/ui/master_prompt.dart b/lib/ui/master_prompt.dart index 2da04d7..9a0f3be 100644 --- a/lib/ui/master_prompt.dart +++ b/lib/ui/master_prompt.dart @@ -147,11 +147,17 @@ Future clearStoredSecrets(AppController c) => c.updateSettings((s) => s // AI 서비스 API 키도 (문구 "비밀번호 · API 키" 와 같게 - 예전에는 남았다, 관리자 결정 10/17) ..aiServices = [for (final x in s.aiServices) x.copyWith(apiKey: '')] ..openSubtitlesKey = '' - ..openSubtitlesPassword = ''); + ..openSubtitlesPassword = '').then((_) async { + // 관리자 판단 (10/21): 다른 판 (새 판) 이 안전 저장소에 넣은 이 판이 모르는 비밀번호 · 키도 (마스터 · 최상은 남김) + await c.settingsStore?.clearForeignSecrets(); +}); /// 앱 설정 전체를 처음 상태로 (다음에 켤 때부터 모두 적용되도록 앱을 다시 켜 달라고 한다) +/// 관리자 판단 (10/21): 이 판이 모르는 설정 키 (다른 판이 쓴 것) 도 지운다 - 사용자가 "전체" 를 골랐고, 새 판으로 돌아갔을 때 +/// 예전 값이 되살아나면 더 헷갈린다. 모르는 키를 남기는 규칙 (40) 은 보통 저장 때만 (새 AppSettings 는 [AppSettings.extra] 가 비어 있다). +/// 안전 저장소의 이 판이 모르는 열쇠는 [저장된 실제 비밀번호 · API 키] 를 골랐을 때만 지운다 ([clearStoredSecrets]) Future resetAllSettings(AppController c) async { - c.settings = AppSettings(); + c.settings = AppSettings()..extra = {}; await c.updateSettings((_) {}); } @@ -198,14 +204,17 @@ Future showSuperReset(BuildContext context, AppController c, MasterLock lo onChanged: (v) => set(() => secrets = v!), title: Text(tr('저장된 실제 비밀번호 · API 키')), // 145: 10/18 에 생긴 SFTP · SMB · FTP 비밀번호 · 키도 지운다 (clearStoredSecrets) - subtitle: Text(tr('WebDAV · SFTP · SMB · FTP · OpenSubtitles 비밀번호, SFTP 키, API 키. 서버 목록과 아이디는 남깁니다')), + subtitle: Text('${tr('WebDAV · SFTP · SMB · FTP · OpenSubtitles 비밀번호, SFTP 키, API 키. 서버 목록과 아이디는 남깁니다')}\n' + '${tr('다른 판에서 넣은 비밀번호 · 키도 지웁니다.')}'), ), CheckboxListTile( value: settings, onChanged: (v) => set(() => settings = v!), title: Text(tr('앱 설정 전체 (처음 상태로)')), // 145: 파일 탐색기 즐겨찾기 (73) 는 설정 안에 있어 함께 지워진다 - 예전 글은 "즐겨찾기는 그대로" - subtitle: Text(tr('서버 목록 (WebDAV · SFTP · SMB · FTP) 과 그 비밀번호 · API 키, 파일 탐색기 즐겨찾기도 지워집니다. 동영상 목록 · 웹 즐겨찾기 · 파일은 그대로입니다')), + subtitle: Text('${tr('서버 목록 (WebDAV · SFTP · SMB · FTP) 과 그 비밀번호 · API 키, 파일 탐색기 즐겨찾기도 지워집니다. 동영상 목록 · 웹 즐겨찾기 · 파일은 그대로입니다')}\n' + // 관리자 판단 (10/21): 이 판이 모르는 설정 키도 지운다 (resetAllSettings) + '${tr('다른 판에서 만든 설정도 지웁니다.')}'), ), ]), ), diff --git a/test/master_lock_test.dart b/test/master_lock_test.dart index b1dd73a..7137f98 100644 --- a/test/master_lock_test.dart +++ b/test/master_lock_test.dart @@ -298,6 +298,34 @@ void main() { expect((c.settings.webdavServers.single.user, c.settings.sftpServers.single.user, c.settings.aiServices.single.url), ('u', 'u', 'https://ai')); }); + test('관리자 판단 (10/21): "앱 설정 전체" 는 이 판이 모르는 설정 키도 지우고, 다른 판의 비밀 값은 "저장된 비밀번호" 를 골랐을 때만', () async { + final tmp = Directory.systemTemp.createTempSync('jj_master_full_'); + addTearDown(() => tmp.deleteSync(recursive: true)); + final file = File(p.join(tmp.path, 'settings.json')) + ..writeAsStringSync(jsonEncode({'explorerLayout': 'single', 'zz_new_version': {'keep': 1}})); + final secrets = MemorySecretStore() + ..values['nfs.pw.n1'] = 'new-version-pw' // 다른 (새) 판이 쓴 열쇠 + ..values['sec.master'] = 'hash'; + final store = SettingsStore(file.path, secrets); + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()), + settingsStore: store); + await c.init(); + // 보통 저장: 모르는 키 · 다른 판의 열쇠 남음 (40) + await c.updateSettings((s) => s.explorerLayout = 'dual'); + Map saved() => jsonDecode(file.readAsStringSync()) as Map; + expect(saved()['zz_new_version'], {'keep': 1}); + expect(secrets.values['nfs.pw.n1'], 'new-version-pw'); + // 앱 설정 전체: 모르는 키 없음 · 다른 판의 열쇠는 아직 남음 (비밀번호 체크를 안 함) + await resetAllSettings(c); + expect(saved().containsKey('zz_new_version'), isFalse); + expect(saved()['explorerLayout'], AppSettings().explorerLayout); + expect(secrets.values['nfs.pw.n1'], 'new-version-pw', reason: '[저장된 실제 비밀번호] 를 고르지 않으면 남김'); + // 저장된 실제 비밀번호 · API 키: 다른 판의 열쇠도 지움 · 마스터 (sec.) 는 남김 + await clearStoredSecrets(c); + expect(secrets.values.containsKey('nfs.pw.n1'), isFalse); + expect(secrets.values['sec.master'], 'hash'); + }); + test('최상으로 "앱 설정 전체" 를 지우면 처음 상태', () async { final c = controller()..settings.explorerLayout = 'single'; await resetAllSettings(c);