Skip to content

Latest commit

ย 

History

History
261 lines (198 loc) ยท 14.1 KB

File metadata and controls

261 lines (198 loc) ยท 14.1 KB

์ง„ํ–‰ ์ƒํ™ฉ

ํ˜„์žฌ: Phase 3 ์™„๋ฃŒ โ€” ๋ฐฐํฌ ์ค€๋น„ + iOS ์ง€์› + ์•ˆ์ •์„ฑ

148 ํ…Œ์ŠคํŠธ ํ†ต๊ณผ. typecheck ํ†ต๊ณผ. npm run build ์„ฑ๊ณต.

Phase 1.1: scaffolding + ์ธํ„ฐํŽ˜์ด์Šค + hash โœ…

  • package.json, tsconfig.json, vitest.config.ts, esbuild.config.mjs
  • manifest.json, .gitignore
  • CLAUDE.md, PROGRESS.md
  • src/types.ts, src/hash.ts, src/adapters/interfaces.ts
  • npm install + typecheck ํ†ต๊ณผ

Phase 1.2: In-Memory Mock + content_hash ํ…Œ์ŠคํŠธ โœ…

  • src/adapters/memory.ts (MemoryFileSystem, MemoryRemoteStorage, MemoryStateStore)
  • test/hash.test.ts (8 ํ…Œ์ŠคํŠธ โ€” ๋นˆํŒŒ์ผ, ๋‹จ์ผ๋ธ”๋ก, ๋ฉ€ํ‹ฐ๋ธ”๋ก, ๊ณต์‹๋ฒกํ„ฐ)
  • test/memory-adapters.test.ts (28 ํ…Œ์ŠคํŠธ โ€” CRUD, cursor, rev ์ถฉ๋Œ)

Phase 1.3: Planner + ๋‹จ์œ„ ํ…Œ์ŠคํŠธ โœ…

  • src/sync/planner.ts (classifyChange, createPlan ์ˆœ์ˆ˜ ํ•จ์ˆ˜)
  • test/planner.test.ts (21 ํ…Œ์ŠคํŠธ โ€” 15 classifyChange + 6 createPlan)

Phase 1.4: Executor + ๋‹จ์œ„ ํ…Œ์ŠคํŠธ โœ…

  • src/sync/executor.ts (upload/download/delete/conflict + rev ์ถฉ๋Œ โ†’ conflict ์ „ํ™˜)
  • test/executor.test.ts (14 ํ…Œ์ŠคํŠธ โ€” ๊ฐ action + partial failure + conflict path)

Phase 1.5: SyncEngine + SyncSimulator + ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ โœ… (๋งˆ์ผ์Šคํ†ค)

  • src/sync/engine.ts (base+delta ๋ณ‘ํ•ฉ, cursor ์กฐ๊ฑด๋ถ€ ๊ฐฑ์‹ )
  • test/support/sync-simulator.ts (Device, SyncSimulator)
  • test/support/failing-remote.ts (FailingRemoteStorage)
  • test/simulation/two-device.test.ts (9 ํ…Œ์ŠคํŠธ)
  • test/simulation/network-failure.test.ts (5 ํ…Œ์ŠคํŠธ)
  • test/simulation/bulk.test.ts (6 ํ…Œ์ŠคํŠธ โ€” 100ํŒŒ์ผ, 3๊ธฐ๊ธฐ)
  • 91 ํ…Œ์ŠคํŠธ ์ „๋ถ€ ํ†ต๊ณผ โ€” ๋™๊ธฐํ™” ๋กœ์ง ์ •ํ™•์„ฑ 100% ์ฆ๋ช…

๊ตฌํ˜„ ์ค‘ ๋ฐœ๊ฒฌํ•œ ์„ค๊ณ„ ๊ฒฐ์ •

  • engine์—์„œ base+delta๋ฅผ ๋ณ‘ํ•ฉํ•˜์—ฌ ์ „์ฒด ์›๊ฒฉ ์ƒํƒœ๋ฅผ ๊ตฌ์„ฑํ•œ ํ›„ planner์— ์ „๋‹ฌ
    • delta๋งŒ ์ „๋‹ฌํ•˜๋ฉด "๋ณ€๊ฒฝ ์—†๋Š” ์›๊ฒฉ ํŒŒ์ผ"์ด "์‚ญ์ œ๋จ"์œผ๋กœ ์˜คํŒ๋จ
  • cursor๋Š” ๋ชจ๋“  ์ž‘์—… ์„ฑ๊ณต ์‹œ์—๋งŒ ๊ฐฑ์‹ 
    • ์‹คํŒจ ์‹œ ๊ฐ™์€ delta๋ฅผ ๋‹ค์Œ cycle์—์„œ ์žฌ์ˆ˜์‹ ํ•˜์—ฌ ์ž๋™ ์žฌ์‹œ๋„

Phase 1.6: ์‹ค์ œ Adapter ๊ตฌํ˜„ โœ…

  • src/hash.browser.ts (crypto.subtle ๊ธฐ๋ฐ˜, async)
  • src/adapters/dropbox-types.ts (Dropbox API ์‘๋‹ต ํƒ€์ž…)
  • src/adapters/dropbox-auth.ts (OAuth PKCE)
  • src/adapters/dropbox-adapter.ts (RemoteStorage ๊ตฌํ˜„, requestUrl)
  • src/adapters/vault-adapter.ts (FileSystem ๊ตฌํ˜„, Vault API)
  • src/adapters/indexeddb-store.ts (SyncStateStore ๊ตฌํ˜„, localforage)
  • test/hash-browser.test.ts (6 ํ…Œ์ŠคํŠธ โ€” node hash์™€ ๊ฒฐ๊ณผ ์ผ์น˜ ๊ฒ€์ฆ)
  • 97 ํ…Œ์ŠคํŠธ ํ†ต๊ณผ + typecheck ํ†ต๊ณผ

Phase 1.7: Plugin ํ†ตํ•ฉ + Settings โœ…

  • src/settings.ts (PluginSettings + DEFAULT_SETTINGS)
  • src/ui/status-bar.ts (idle/syncing/success/error ์ƒํƒœ)
  • src/ui/conflict-modal.ts (์ถฉ๋Œ ์•Œ๋ฆผ)
  • src/ui/settings-tab.ts (OAuth 2๋‹จ๊ณ„ + ๋™๊ธฐํ™” ์„ค์ •)
  • src/main.ts (Plugin ์ง„์ž…์ , adapter DI)
  • styles.css
  • executor.ts: hash.ts(node:crypto) โ†’ hash.browser.ts(crypto.subtle) ์ „ํ™˜
  • npm run build โ†’ main.js (113KB) ์ƒ์„ฑ ์„ฑ๊ณต

Phase 1 ์™„๋ฃŒ ์š”์•ฝ

  • 97 ํ…Œ์ŠคํŠธ: ์ˆœ์ˆ˜ ๋กœ์ง + ์‹œ๋ฎฌ๋ ˆ์ด์…˜์œผ๋กœ ๋™๊ธฐํ™” ์ •ํ™•์„ฑ ์ฆ๋ช…
  • ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ: 113KB (obsidian ์™ธ๋ถ€ ์˜์กด์„ฑ + localforage ๋ฒˆ๋“ค)
  • ๋‹ค์Œ: Obsidian์— ๋กœ๋“œํ•˜์—ฌ ์‹ค์ œ ๋™๊ธฐํ™” ํ…Œ์ŠคํŠธ (์ˆ˜๋™)

Phase 2: ์‚ญ์ œ ๋ณดํ˜ธ + ์ถฉ๋Œ ์ „๋žต + ์ธ์ฆ ๊ฐœ์„  โœ…

Phase 2.1: ์‚ญ์ œ ์ด๋ฒคํŠธ ์ถ”์  (๋ ˆ์ด์–ด 1) โœ…

  • planner.ts: classifyChange์— localDeleteIntended ์˜ต์…˜ ์ถ”๊ฐ€
  • planner.ts: createPlan์— localDeletedPaths?: Set<string> ์˜ต์…˜ ์ถ”๊ฐ€
  • ํ•ต์‹ฌ ๋ณ€๊ฒฝ: ์‚ญ์ œ ์˜๋„ ์—†๋Š” ๋ถ€์žฌ โ†’ download(missing_local_restored) (์•ˆ์ „ ๋ฐฉํ–ฅ)
  • test/planner.test.ts: ์‚ญ์ œ ์˜๋„ ์œ /๋ฌด ๋ถ„๊ธฐ ํ…Œ์ŠคํŠธ (+4 ์ผ€์ด์Šค)

Phase 2.2: ๋Œ€๋Ÿ‰ ์‚ญ์ œ ๊ฐ€๋“œ (๋ ˆ์ด์–ด 2) โœ…

  • src/sync/guards.ts: checkDeleteGuard() ์ˆœ์ˆ˜ ํ•จ์ˆ˜ (๊ฐœ์ˆ˜ ๊ธฐ๋ฐ˜ ์ฐจ๋‹จ)
  • src/ui/delete-confirm-modal.ts: ๋Œ€๋Ÿ‰ ์‚ญ์ œ ํ™•์ธ ๋ชจ๋‹ฌ
  • test/guards.test.ts: 7 ํ…Œ์ŠคํŠธ (ํ†ต๊ณผ/์ฐจ๋‹จ/๋น„ํ™œ์„ฑํ™”/threshold ๊ฒฝ๊ณ„)

Phase 2.3: ์ถฉ๋Œ ์ „๋žต ๊ฐ•ํ™” โœ…

  • executor.ts: conflict ์ „๋žต 3๋ถ„๊ธฐ โ€” keep_both / newest(mtime) / manual(์ฝœ๋ฐฑ)
  • src/ui/conflict-modal.ts: "Keep local" / "Keep remote" ๋‘ ๋ฒ„ํŠผ
  • test/executor.test.ts: newest/manual ์ „๋žต ํ…Œ์ŠคํŠธ (+6 ์ผ€์ด์Šค)

Phase 2.4: ํ†ตํ•ฉ + ์‹œ๋ฎฌ๋ ˆ์ด์…˜ โœ…

  • engine.ts: deletedPaths ๊ด€๋ฆฌ + planner ์ „๋‹ฌ + ๊ฐ€๋“œ ์ ์šฉ + ์‚ญ์ œ ๋กœ๊ทธ API
  • main.ts: vault.on('delete/rename') ์ด๋ฒคํŠธ ์ถ”์  + store.setMeta("deleteLog") ์˜์†ํ™”
  • settings.ts: deleteProtection(true), deleteThreshold(5) ์ถ”๊ฐ€
  • test/support/sync-simulator.ts: Device.deleteFile()์— ์‚ญ์ œ ๋กœ๊ทธ ์ž๋™ ๊ธฐ๋ก
  • test/simulation/delete-protection.test.ts: 9 ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ…Œ์ŠคํŠธ

Phase 2.5: ์›ํด๋ฆญ OAuth ์ธ์ฆ โœ…

  • dropbox-auth.ts: generateState(), buildAuthUrl() ๊ฐ์ฒด ํŒŒ๋ผ๋ฏธํ„ฐ, redirectUri ์ง€์›
  • settings.ts: __DROPBOX_APP_KEY__ ๋นŒ๋“œ ํƒ€์ž„ ๋ณ€์ˆ˜, useCustomAppKey, getEffectiveAppKey()
  • main.ts: ๋ฐ์Šคํฌํ†ฑ obsidian:// ํ”„๋กœํ† ์ฝœ ํ•ธ๋“ค๋Ÿฌ + state ๊ฒ€์ฆ (CSRF ๋ฐฉ์ง€)
  • settings-tab.ts: ๋ฐ์Šคํฌํ†ฑ ์›ํด๋ฆญ / ๋ชจ๋ฐ”์ผ 2๋‹จ๊ณ„ ๋ถ„๊ธฐ, Advanced ์„น์…˜
  • test/dropbox-auth.test.ts: 10 ํ…Œ์ŠคํŠธ

Phase 2 ์™„๋ฃŒ ์š”์•ฝ

  • 133 ํ…Œ์ŠคํŠธ: ๊ธฐ์กด 97 + ์‹ ๊ทœ 36 (์‚ญ์ œ ๋ณดํ˜ธ 20 + ์ถฉ๋Œ ์ „๋žต 6 + auth 10)
  • 3๊ณ„์ธต ๋ฐฉ์–ด: ์‚ญ์ œ ์ด๋ฒคํŠธ ์ถ”์  โ†’ ๋Œ€๋Ÿ‰ ์‚ญ์ œ ๊ฐ€๋“œ โ†’ Dropbox ์ž์ฒด ํœด์ง€ํ†ต
  • ์ถฉ๋Œ ์ „๋žต: keep_both(๊ธฐ๋ณธ) / newest(mtime) / manual(๋ชจ๋‹ฌ)
  • ์ธ์ฆ: ๋ฐ์Šคํฌํ†ฑ ์›ํด๋ฆญ (obsidian:// redirect) + ๋ชจ๋ฐ”์ผ 2๋‹จ๊ณ„ ์ˆ˜๋™

Phase 3: ๋ฐฐํฌ ์ค€๋น„ + iOS ์ง€์› + ์•ˆ์ •์„ฑ โœ…

Step 1: Plugin ID ๋ณ€๊ฒฝ โœ…

  • obsidian-dropbox-sync โ†’ dropbox-sync ์ „์—ญ ๊ต์ฒด (manifest, main.ts, indexeddb-store.ts)

Step 2: Rate limit ์ž๋™ ์žฌ์‹œ๋„ โœ…

  • dropbox-adapter.ts: rpcCall/download/upload์— 429 retry (์ตœ๋Œ€ 3ํšŒ, retryAfter ๊ธฐ๋ฐ˜)
  • test/dropbox-adapter-retry.test.ts: 5 ํ…Œ์ŠคํŠธ

Step 3: Cursor ๋งŒ๋ฃŒ ๋ณต๊ตฌ โœ…

  • engine.ts: DropboxCursorResetError catch โ†’ cursor ์ดˆ๊ธฐํ™” + ์ „์ฒด ์žฌ์Šค์บ”

Step 4: iOS VaultFileStore โœ…

  • src/adapters/vault-file-store.ts: vault ๋‚ด JSON ํŒŒ์ผ ๊ธฐ๋ฐ˜ SyncStateStore
  • test/vault-file-store.test.ts: 10 ํ…Œ์ŠคํŠธ

Step 5: Store ์ž๋™ ์„ ํƒ โœ…

  • main.ts: Platform.isIosApp โ†’ VaultFileStore, ๊ทธ ์™ธ โ†’ IndexedDBStore
  • test/mocks/obsidian.ts: Platform.isIosApp ์ถ”๊ฐ€

Step 6: ๋ฐฐํฌ ํ•„์ˆ˜ ํŒŒ์ผ โœ…

  • README.md, LICENSE (MIT), versions.json

Step 7: GitHub Release workflow โœ…

  • .github/workflows/release.yml (tag push โ†’ test โ†’ build โ†’ release)

Phase 3 ์™„๋ฃŒ ์š”์•ฝ

  • 148 ํ…Œ์ŠคํŠธ: ๊ธฐ์กด 133 + ์‹ ๊ทœ 15 (retry 5 + VaultFileStore 10)
  • Plugin ID: dropbox-sync (Community Plugin ๋“ฑ๋ก ์š”๊ตฌ์‚ฌํ•ญ ์ถฉ์กฑ)
  • API ์•ˆ์ •์„ฑ: rate limit ์ž๋™ ์žฌ์‹œ๋„ + cursor ๋งŒ๋ฃŒ ์ž๋™ ๋ณต๊ตฌ
  • iOS ์ง€์›: VaultFileStore fallback (IndexedDB ๋ถˆ์•ˆ์ • ๋Œ€๋น„)
  • ๋ฐฐํฌ: README, LICENSE, versions.json, GitHub Release workflow

ํ˜„ํ™ฉ ๋ถ„์„ (2026-03-03)

์ด๋ฏธ ์ž˜ ๋Œ€์‘๋œ ์„ค๊ณ„

ํ•ญ๋ชฉ ๊ตฌํ˜„ ์ฝ”๋“œ ์œ„์น˜
Dropbox ๋ฐ์Šคํฌํ†ฑ ํด๋ผ์ด์–ธํŠธ ๊ฐ„์„ญ content_hash ๋น„๊ต๋กœ mtime ์˜คํŒ ์›์ฒœ ์ฐจ๋‹จ. ๋ฐ์Šคํฌํ†ฑ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋กœ์ปฌ์„ ๋จผ์ € ์—…๋ฐ์ดํŠธํ•ด๋„ hash ๋™์ผ โ†’ noop planner.ts:50
์‚ญ์ œ ์ „ํŒŒ ๋ฐฉํ–ฅ base state + localDeleteIntended ํ”Œ๋ž˜๊ทธ. ์‚ญ์ œ ์˜๋„ ์—†๋Š” ๋ถ€์žฌ โ†’ download(๋ณต๊ตฌ). ์‚ญ์ œ+์ˆ˜์ • ๊ต์ฐจ โ†’ ๋ณ€๊ฒฝ ์šฐ์„  planner.ts:90-105
๊ฒฝ๋กœ ๋Œ€์†Œ๋ฌธ์ž pathLower ํ‚ค ์ •๊ทœํ™”. Dropbox case-insensitive ๋™์ž‘๊ณผ ์ผ๊ด€ planner.ts:128-142
๋ฐ˜์“ฐ๊ธฐ ๋ฐฉ์ง€ download ํ›„ hash ๊ฒ€์ฆ + Vault API atomic write ์œ„์ž„ executor.ts:86-107
ํ† ํฐ ์ €์žฅ/๊ฐฑ์‹  data.json์— ์ €์žฅ, refresh ์ž๋™ ๊ฐฑ์‹ . .obsidian ๋™๊ธฐํ™”์™€ ๋ถ„๋ฆฌ main.ts:369-373
Rate limit 429 โ†’ ์ตœ๋Œ€ 3ํšŒ retry, retryAfter ์ค€์ˆ˜, exponential backoff + jitter dropbox-adapter.ts
Cursor ๋งŒ๋ฃŒ DropboxCursorResetError catch โ†’ ์ „์ฒด ์žฌ์Šค์บ” ์ž๋™ ๋ณต๊ตฌ engine.ts:77-87
ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ฏธ์‹คํ–‰ ์ค‘ ์‚ญ์ œ ์ด๋ฒคํŠธ ๋ฏธ์ˆ˜์ง‘ โ†’ ์‚ญ์ œ ์˜๋„ ์—†์Œ โ†’ download(๋ณต๊ตฌ). ์•ˆ์ „ ๋ฐฉํ–ฅ ์˜คํŒ planner.ts:100-101

๋ฏธ๋Œ€์‘ / ๊ฐœ์„  ํ•„์š”

# ํ•ญ๋ชฉ ์œ„ํ—˜๋„ ํ˜„์žฌ ์ƒํƒœ
1 ์‹ฑํฌ ์ค‘ ํ™œ์„ฑ ํŒŒ์ผ ํŽธ์ง‘ ๋†’์Œ executor๊ฐ€ write ์ „ getActiveFile() ๋ฏธ์ฒดํฌ. ํŽธ์ง‘ ์ค‘ ๋ฎ์–ด์“ฐ๊ธฐ โ†’ ๋ฐ์ดํ„ฐ ์œ ์‹ค ๊ฐ€๋Šฅ
2 ๋Œ€๋Ÿ‰ ๋ณ€๊ฒฝ ์ˆœ์ฐจ ์‹คํ–‰ ์ค‘๊ฐ„ for...of ์ง๋ ฌ ์ฒ˜๋ฆฌ. 500๊ฐœ ํŒŒ์ผ ์‹œ ๋А๋ฆผ + UI ๋ธ”๋กœํ‚น ๊ฐ€๋Šฅ
3 cursor all-or-nothing ์ค‘๊ฐ„ 200/500 ์„ฑ๊ณตํ•ด๋„ cursor ๋ฏธ๊ฐฑ์‹  โ†’ ์ „๋ถ€ ์žฌ์ฒ˜๋ฆฌ. base ๊ฐฑ์‹ ์€ ๋จ
4 ์ง„ํ–‰ ํ‘œ์‹œ ์—†์Œ ์ค‘๊ฐ„ ๋Œ€๋Ÿ‰ ์‹ฑํฌ ์‹œ ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ์—†์Œ. statusBar๋Š” syncing/success๋งŒ
5 ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ค‘๋‹จ (๋ชจ๋ฐ”์ผ) ์ค‘๊ฐ„ AbortController ์—†์Œ. ์ค‘๋‹จ ์‹œ ์ง„ํ–‰ ์ค‘ ์ž‘์—… ์ƒํƒœ ๋ถˆ๋ช…ํ™•
6 ํŠน์ˆ˜๋ฌธ์ž ๊ฒ€์ฆ ์ค‘๊ฐ„ Dropbox ๊ธˆ์ง€ ๋ฌธ์ž ํ•„ํ„ฐ๋ง ์—†์Œ
7 ๋„คํŠธ์›Œํฌ ์˜จ/์˜คํ”„ ๊ฐ์ง€ ๋‚ฎ์Œ navigator.onLine ๋ฏธ์‚ฌ์šฉ. ์˜คํ”„๋ผ์ธ ์‹œ์—๋„ ์‹ฑํฌ ์‹œ๋„
8 ํ† ํฐ revoke ์•Œ๋ฆผ ๋‚ฎ์Œ refresh_token revoke ์‹œ ์กฐ์šฉํžˆ ์‹คํŒจ. ์‚ฌ์šฉ์ž ๋ฏธํ†ต์ง€
9 ์•ฑ ์‹œ์ž‘ ๋ ˆ์ด์Šค ๋‚ฎ์Œ onLayoutReady ๊ตฌํ˜„๋จ. editLock์€ ์—†์œผ๋‚˜ content_hash๋กœ ์ตœ์ข… ๋ณดํ˜ธ

remotely-save ๋น„๊ต (Dropbox ํ•œ์ •)

๋Œ€ํ‘œ์  ์ปค๋ฎค๋‹ˆํ‹ฐ ํ”Œ๋Ÿฌ๊ทธ์ธ remotely-save์™€์˜ ๊ตฌ์กฐ์  ์ฐจ์ด. ๋ฒ”์šฉ์„ฑ(9๊ฐœ ๋ฐฑ์—”๋“œ)์„ ํƒํ•œ ๋Œ€๊ฐ€๋กœ Dropbox ๊ณ ์œ  ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜์ง€ ๋ชปํ•จ.

์šฐ๋ฆฌ๊ฐ€ ์•ž์„œ๋Š” ๋ถ€๋ถ„:

์˜์—ญ remotely-save dropbox-sync
๋ณ€๊ฒฝ ๊ฐ์ง€ mtime + size (๊ฐ™์€ ํฌ๊ธฐ๋ฉด ๋ณ€๊ฒฝ ๋ชป ๊ฐ์ง€) content_hash (๋‚ด์šฉ ๊ธฐ๋ฐ˜)
์ฆ๋ถ„ ๋™๊ธฐํ™” ๋งค๋ฒˆ ์ „์ฒด ์Šค์บ” cursor ๊ธฐ๋ฐ˜ delta
์—…๋กœ๋“œ ์•ˆ์ „ mode: "overwrite" ๋ฌด์กฐ๊ฑด ๋ฎ์–ด์“ฐ๊ธฐ rev ๋‚™๊ด€์  ์ž ๊ธˆ (์„œ๋ฒ„ ์ถฉ๋Œ ๊ฐ์ง€)
์ถฉ๋Œ ์ฒ˜๋ฆฌ keep_newer ๊ธฐ๋ณธ โ†’ ์ง„ ์ชฝ ํ†ต๋ณด ์—†์ด ์‚ญ์ œ conflict ํŒŒ์ผ ๋ณด์กด + ์‚ฌ์šฉ์ž ํ™•์ธ
์‚ญ์ œ ํŒ๋‹จ prevSync ๊ธฐ๋ฐ˜์ด๋‚˜ ๋ถ€ํ™œ ๋ฒ„๊ทธ ๋‹ค์ˆ˜ deleteIntended + ์•ˆ์ „ ๋ฐฉํ–ฅ ๊ธฐ๋ณธ๊ฐ’
์ฝ”๋“œ ๊ตฌ์กฐ 40+ ๋ธŒ๋žœ์น˜ ๊ฑฐ๋Œ€ ํ•จ์ˆ˜ planner/executor ์ˆœ์ˆ˜ ํ•จ์ˆ˜ ๋ถ„๋ฆฌ
ํ…Œ์ŠคํŠธ ~112๊ฐœ (ํ•ต์‹ฌ ๋™๊ธฐํ™” ๋กœ์ง ๋ฏธํ…Œ์ŠคํŠธ) 148๊ฐœ (์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํฌํ•จ)

remotely-save์˜ ์•Œ๋ ค์ง„ ๋ฌธ์ œ:

  • mtime ๊ธฐ๋ฐ˜ ์˜คํŒ โ€” macOS/iOS ๊ฐ„ mtime ๋ถˆ์ผ์น˜๋กœ ๋งค๋ฒˆ ์ „์ฒด ์žฌ๋™๊ธฐํ™” (#575)
  • ์‚ญ์ œ ํŒŒ์ผ ๋ถ€ํ™œ (#611, #985)
  • smart_conflict ๋ฐ์ดํ„ฐ ์œ ์‹ค (#697)
  • rate limit ํญ๋ฐœ โ€” ๋Œ€๊ทœ๋ชจ vault์—์„œ 429 ๋ฐ˜๋ณต (#1026)
  • README์— "ALWAYS backup your vault before using" ๊ฒฝ๊ณ 

remotely-save๊ฐ€ ์•ž์„œ๋Š” ๋ถ€๋ถ„:

  • ์ปค๋ฎค๋‹ˆํ‹ฐ ํ”Œ๋Ÿฌ๊ทธ์ธ ๋“ฑ๋ก ์™„๋ฃŒ, ์‹ค์‚ฌ์šฉ์ž ๋‹ค์ˆ˜
  • ์‹คํ™˜๊ฒฝ ์˜ค๋ž˜ ์šด์˜ โ†’ ์—ฃ์ง€์ผ€์ด์Šค ๋ฐœ๊ฒฌ ์ถ•์ 
  • .obsidian ๊ธฐ๊ธฐ๋ณ„ ์„ค์ • ๋ถ„๋ฆฌ ๋“ฑ ์„ธ๋ฐ€ํ•œ ์˜ต์…˜

๊ณตํ†ต ๋ฏธ๊ตฌํ˜„: ํ™œ์„ฑ ํŒŒ์ผ ๋ณดํ˜ธ, ๋ณ‘๋ ฌ ์‹คํ–‰, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” (Obsidian ํ”Œ๋žซํผ ์ œ์•ฝ)


๋‹ค์Œ: Phase 4 (์•ˆ์ •์„ฑ + UX ๊ฐ•ํ™”)

4.1 ์‹ฑํฌ ์ค‘ ํ™œ์„ฑ ํŒŒ์ผ ๋ณดํ˜ธ โ€” ์šฐ์„ ์ˆœ์œ„: ๋†’์Œ

ํ˜„์žฌ executor๊ฐ€ fs.write() ์‹œ ํ™œ์„ฑ ํŽธ์ง‘ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜์ง€ ์•Š์Œ. ์›๊ฒฉ ๋ฒ„์ „์„ ๋‹ค์šด๋กœ๋“œํ•˜์—ฌ ๋กœ์ปฌ์— ๋ฎ์–ด์“ธ ๋•Œ, ์‚ฌ์šฉ์ž๊ฐ€ ์—๋””ํ„ฐ์—์„œ ํ•ด๋‹น ํŒŒ์ผ์„ ํŽธ์ง‘ ์ค‘์ด๋ฉด ๋‚ด์šฉ์ด ์œ ์‹ค๋  ์ˆ˜ ์žˆ์Œ.

  • executor.ts download/conflict: write ์ „ app.workspace.getActiveFile() ์ฒดํฌ
  • ํ™œ์„ฑ ํŒŒ์ผ์ด๋ฉด conflict๋กœ ๋ถ„๋ฅ˜ํ•˜๊ฑฐ๋‚˜ ์‹ฑํฌ ์ง€์—ฐ
  • Obsidian์˜ vault.modifyBinary()์™€ ์—๋””ํ„ฐ in-memory ์ƒํƒœ์˜ ์ƒํ˜ธ์ž‘์šฉ ๊ฒ€์ฆ ํ•„์š”

4.2 ๋Œ€๋Ÿ‰ ๋ณ€๊ฒฝ ์„ฑ๋Šฅ โ€” ์šฐ์„ ์ˆœ์œ„: ์ค‘๊ฐ„

ํ˜„์žฌ executor๊ฐ€ plan.items๋ฅผ for...of๋กœ ์ˆœ์ฐจ ์‹คํ–‰. 500๊ฐœ ํŒŒ์ผ ๋ณ€๊ฒฝ ์‹œ ์ง๋ ฌ ์ฒ˜๋ฆฌ๋กœ ๋А๋ฆฌ๊ณ , ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ธ”๋กœํ‚น ๊ฐ€๋Šฅ.

  • executor์— p-queue ๋„์ž… (concurrency ์ œํ•œ ๋ณ‘๋ ฌ ์‹คํ–‰)
  • ์ง„ํ–‰ ๋ฐ” + ์ƒ์„ธ ์ƒํƒœ ํ‘œ์‹œ (ํ˜„์žฌ N/M ํŒŒ์ผ)
  • cursor all-or-nothing ์™„ํ™” ๊ฒ€ํ† : ์„ฑ๊ณต ํ•ญ๋ชฉ๋งŒ base ๊ฐฑ์‹ ์€ ์ด๋ฏธ ๊ตฌํ˜„๋จ. cursor ๋ถ€๋ถ„ ๊ฐฑ์‹ ์€ Dropbox API ํŠน์„ฑ์ƒ ๋ถˆ๊ฐ€ โ†’ ํ˜„์žฌ ๋ฐฉ์‹ ์œ ์ง€ํ•˜๋˜ ์„ฑ๋Šฅ์œผ๋กœ ๋ณด์ƒ

4.3 ๋ชจ๋ฐ”์ผ ์•ˆ์ •์„ฑ โ€” ์šฐ์„ ์ˆœ์œ„: ์ค‘๊ฐ„

๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ค‘๋‹จ: ๋ชจ๋ฐ”์ผ์—์„œ ์•ฑ ์ „ํ™˜ ์‹œ ์‹ฑํฌ ์ค‘๋‹จ ๊ฐ€๋Šฅ.

  • AbortController/signal ๋„์ž… โ†’ ์ค‘๋‹จ ์‹œ ์ง„ํ–‰ ์ค‘ ์ž‘์—…๋งŒ ์‹คํŒจ ์ฒ˜๋ฆฌ
  • ์ด๋ฏธ ์„ฑ๊ณตํ•œ ํ•ญ๋ชฉ์˜ base ์—…๋ฐ์ดํŠธ๋Š” ์œ ์ง€๋จ (ํ˜„์žฌ ์„ค๊ณ„๋กœ ์•ˆ์ „)
  • cursor ๋ฏธ๊ฐฑ์‹  โ†’ ๋‹ค์Œ ์‹คํ–‰ ์‹œ ์ž์—ฐ ์žฌ์‹œ๋„

๋„คํŠธ์›Œํฌ ์ƒํƒœ ๊ฐ์ง€:

  • navigator.onLine ๋˜๋Š” Obsidian ๋„คํŠธ์›Œํฌ ์ด๋ฒคํŠธ๋กœ ์˜จ/์˜คํ”„ ๊ฐ์ง€
  • ์˜คํ”„๋ผ์ธ ์‹œ ์‹ฑํฌ ์Šคํ‚ต โ†’ ์˜จ๋ผ์ธ ๋ณต๊ท€ ์‹œ ์ฆ‰์‹œ ์‹ฑํฌ

์•ฑ ์‹œ์ž‘ ๋ ˆ์ด์Šค:

  • onLayoutReady ์ดํ›„์—๋งŒ ์ด๋ฒคํŠธ ๋“ฑ๋ก (๊ตฌํ˜„๋จ)
  • ์ดˆ๊ธฐ ์‹ฑํฌ ์ค‘ ๋ฐœ์ƒํ•œ vault ์ด๋ฒคํŠธ๊ฐ€ planner์— ์ •ํ™•ํžˆ ๋ฐ˜์˜๋˜๋Š”์ง€ ๊ฒ€์ฆ

4.4 ์ž…๋ ฅ ๊ฒ€์ฆ โ€” ์šฐ์„ ์ˆœ์œ„: ์ค‘๊ฐ„

ํŠน์ˆ˜๋ฌธ์ž ํ•„ํ„ฐ๋ง: Dropbox๊ฐ€ ํ—ˆ์šฉํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ๋ช… ๋ฌธ์ž ๊ฒ€์ฆ.

  • upload ์ „ ํŒŒ์ผ๋ช… sanitize ๋˜๋Š” ์‚ฌ์šฉ์ž ์•Œ๋ฆผ
  • Dropbox ๊ธˆ์ง€ ๋ฌธ์ž: NUL, /, ์ œ์–ด๋ฌธ์ž ๋“ฑ

ํ† ํฐ revoke ๊ฐ์ง€:

  • refresh_token์ด revoke๋œ ๊ฒฝ์šฐ (์‚ฌ์šฉ์ž๊ฐ€ Dropbox ์•ฑ ๊ถŒํ•œ ํ•ด์ œ) ํ˜„์žฌ ์กฐ์šฉํžˆ ์‹คํŒจ
  • ์ธ์ฆ ์‹คํŒจ ์‹œ Notice๋กœ "์žฌ์ธ์ฆ ํ•„์š”" ์•Œ๋ฆผ + ์„ค์ • ํƒญ ์œ ๋„

4.5 longpoll ์‹ค์‹œ๊ฐ„ ๊ฐ์ง€ (๋ฐ์Šคํฌํ†ฑ) โ€” ์šฐ์„ ์ˆœ์œ„: ๋‚ฎ์Œ

  • Dropbox /2/files/list_folder/longpoll๋กœ ๋ณ€๊ฒฝ ์ฆ‰์‹œ ๊ฐ์ง€
  • ๋ฐ์Šคํฌํ†ฑ ์ „์šฉ (๋ชจ๋ฐ”์ผ์€ ํด๋ง ์œ ์ง€)

4.6 selective sync (ํด๋” ์ œ์™ธ) โ€” ์šฐ์„ ์ˆœ์œ„: ๋‚ฎ์Œ

  • excludePatterns ์„ค์ •์€ ์ด๋ฏธ ์กด์žฌ (settings.ts)
  • planner์—์„œ ์ œ์™ธ ํŒจํ„ด ์ ์šฉ ๋กœ์ง ๊ตฌํ˜„ ํ•„์š”

4.7 Community Plugin ๋“ฑ๋ก ์ œ์ถœ โ€” ์šฐ์„ ์ˆœ์œ„: ๋‚ฎ์Œ

  • ์œ„ ์•ˆ์ •์„ฑ ํ•ญ๋ชฉ ํ•ด๊ฒฐ ํ›„ ์ง„ํ–‰
  • obsidianmd/obsidian-releases PR ์ œ์ถœ