From 62e68dcfe760e37aaac198d557d0987ad2c69cec Mon Sep 17 00:00:00 2001 From: altair823 Date: Sun, 10 May 2026 03:44:09 +0900 Subject: [PATCH] feat(v030): settings.sync_repo_url + sync_auto_enabled + sync_interval_min MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - zod schema 확장: sync_repo_url (nullable), sync_auto_enabled (default true), sync_interval_min (int >= 5, default 30) - getter/setter 6개 추가 (기존 ai_enabled / onboarding_completed 패턴) - setSyncIntervalMin 은 non-integer / < 5 reject --- src/main/services/SettingsService.ts | 48 +++++++++++++++++++++++++++- tests/unit/SettingsService.test.ts | 36 +++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/main/services/SettingsService.ts b/src/main/services/SettingsService.ts index 88a320e..1c091f6 100644 --- a/src/main/services/SettingsService.ts +++ b/src/main/services/SettingsService.ts @@ -13,7 +13,11 @@ const SettingsSchema = z.object({ // true 로 fallback (기본 enabled). zod default 는 file 이 존재 + 키 부재일 때만 적용 — // load() 의 file-missing 분기에선 cache={} 라 isAiEnabled() 의 fallback 이 작동. ai_enabled: z.boolean().optional(), - onboarding_completed: z.boolean().optional() + onboarding_completed: z.boolean().optional(), + // v0.3.0 Cut E — 양방향 git sync 설정. 모두 optional — 미구성 시 sync 비활성. + sync_repo_url: z.string().nullable().optional(), + sync_auto_enabled: z.boolean().optional(), + sync_interval_min: z.number().int().min(5).optional() }).strict(); export type Settings = z.infer; @@ -81,6 +85,48 @@ export class SettingsService { await this.persist(next); } + /** + * v0.3.0 Cut E — sync 저장소 URL. null/빈 문자열 = sync 비활성. 본 메서드는 값만 저장, + * git init/remote add 는 별도 호출자 (settings:configure-sync IPC) 가 담당. + */ + async getSyncRepoUrl(): Promise { + const s = await this.load(); + return s.sync_repo_url ?? null; + } + + async setSyncRepoUrl(value: string | null): Promise { + const current = await this.load(); + const next: Settings = { ...current, sync_repo_url: value }; + await this.persist(next); + } + + /** v0.3.0 Cut E — 자동 주기 sync 활성. configured 일 때만 의미 있음. 기본 true. */ + async isAutoSyncEnabled(): Promise { + const s = await this.load(); + return s.sync_auto_enabled ?? true; + } + + async setAutoSyncEnabled(value: boolean): Promise { + const current = await this.load(); + const next: Settings = { ...current, sync_auto_enabled: value }; + await this.persist(next); + } + + /** v0.3.0 Cut E — 자동 주기 sync interval (분). 기본 30, min 5. */ + async getSyncIntervalMin(): Promise { + const s = await this.load(); + return s.sync_interval_min ?? 30; + } + + async setSyncIntervalMin(value: number): Promise { + if (!Number.isInteger(value) || value < 5) { + throw new Error(`sync_interval_min must be an integer >= 5 (got ${value})`); + } + const current = await this.load(); + const next: Settings = { ...current, sync_interval_min: value }; + await this.persist(next); + } + private async persist(next: Settings): Promise { await mkdir(dirname(this.filePath), { recursive: true }); const tmpPath = this.filePath + '.tmp'; diff --git a/tests/unit/SettingsService.test.ts b/tests/unit/SettingsService.test.ts index 923050e..89fa152 100644 --- a/tests/unit/SettingsService.test.ts +++ b/tests/unit/SettingsService.test.ts @@ -54,4 +54,40 @@ describe('SettingsService', () => { expect(existsSync(join(dir, 'settings.json.tmp'))).toBe(false); expect(existsSync(join(dir, 'settings.json'))).toBe(true); }); + + describe('v0.3.0 Cut E — sync settings', () => { + it('getSyncRepoUrl() defaults to null', async () => { + expect(await svc.getSyncRepoUrl()).toBeNull(); + }); + + it('setSyncRepoUrl() / getSyncRepoUrl() round-trip', async () => { + await svc.setSyncRepoUrl('git@gitea.example:user/notes.git'); + expect(await svc.getSyncRepoUrl()).toBe('git@gitea.example:user/notes.git'); + // setting null clears + await svc.setSyncRepoUrl(null); + expect(await svc.getSyncRepoUrl()).toBeNull(); + }); + + it('isAutoSyncEnabled() defaults to true', async () => { + expect(await svc.isAutoSyncEnabled()).toBe(true); + }); + + it('setAutoSyncEnabled() persists', async () => { + await svc.setAutoSyncEnabled(false); + expect(await svc.isAutoSyncEnabled()).toBe(false); + await svc.setAutoSyncEnabled(true); + expect(await svc.isAutoSyncEnabled()).toBe(true); + }); + + it('getSyncIntervalMin() defaults to 30', async () => { + expect(await svc.getSyncIntervalMin()).toBe(30); + }); + + it('setSyncIntervalMin() persists + rejects values < 5 / non-integer', async () => { + await svc.setSyncIntervalMin(15); + expect(await svc.getSyncIntervalMin()).toBe(15); + await expect(svc.setSyncIntervalMin(3)).rejects.toThrow(); + await expect(svc.setSyncIntervalMin(10.5)).rejects.toThrow(); + }); + }); });