From a53ef4cc6fc1d3cb4f7449d9d329061780093c89 Mon Sep 17 00:00:00 2001 From: zenn Date: Tue, 21 Apr 2026 18:16:50 +0900 Subject: [PATCH] =?UTF-8?q?v0.1.13=20-=20=EB=A1=9C=EC=BB=AC=C2=B7=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=EB=93=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HANDOFF.md | 4 +++- package-lock.json | 4 ++-- package.json | 2 +- src/App.vue | 41 +++++++++++++++++++++++++--------- src/lib/plannerStorage.js | 46 ++++++++++++++++++++++++--------------- 5 files changed, 65 insertions(+), 32 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 4752607..84e10ac 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -4,7 +4,7 @@ - 프로젝트명: 10 Minute Planner 웹 UI - 기술 스택: Vue 3 + Vite + TailwindCSS + JavaScript -- 현재 기준 버전: `v0.1.12` +- 현재 기준 버전: `v0.1.13` - Git 원격 저장소: `https://git.sori.studio/zenn/planner.sori.studio.git` ## 기준 디자인 @@ -119,6 +119,8 @@ - 현재 프론트는 인증만 연결된 상태이고, 플래너 저장은 아직 `localStorage` 기준이다. - 로그인 상태에서는 플래너 수정 시 날짜별 서버 저장을 예약하고, 로그인 직후에는 서버 데이터를 먼저 가져오도록 연결하기 시작했다. - 현재는 로컬 저장도 계속 유지하면서 서버 저장을 병행하는 과도기 구조다. +- 로그인 시 서버 플래너 데이터로 `plannerRecords`를 교체하고, 로그아웃 시에는 로컬 저장 기반 데이터로 다시 복귀하도록 정리했다. +- 이로 인해 다른 사용자 로그인 시 이전 로컬 데이터가 서버 계정 데이터와 섞일 위험을 줄였다. - 이미지 저장 기능은 추후 `print-only` 또는 별도 export 전용 레이아웃을 기준으로 구현하면 화면/인쇄/공유 결과를 맞추기 쉽다. - Docker Compose는 프론트엔드와 백엔드를 함께 올리는 기준으로 설계하되, NAS 환경에 맞는 볼륨과 재시작 정책도 함께 고려한다. diff --git a/package-lock.json b/package-lock.json index 771014b..4c16477 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ten-minute-planner", - "version": "0.1.12", + "version": "0.1.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ten-minute-planner", - "version": "0.1.12", + "version": "0.1.13", "dependencies": { "vue": "^3.5.13" }, diff --git a/package.json b/package.json index 1a67901..aa046c5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ten-minute-planner", "private": true, - "version": "0.1.12", + "version": "0.1.13", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.vue b/src/App.vue index e94d8f4..68e6187 100644 --- a/src/App.vue +++ b/src/App.vue @@ -16,6 +16,7 @@ import { fetchPlannerEntries, savePlannerEntry } from './lib/plannerApi' import { createInitialPlannerRecords, persistPlannerState, + readPlannerStorageState, restorePlannerUiState, } from './lib/plannerStorage' @@ -557,6 +558,7 @@ watch( calendarViewDate: calendarViewDate.value, statsRangeStart: normalizedStatsRange.value.startKey, statsRangeEnd: normalizedStatsRange.value.endKey, + includeRecords: !isAuthenticated.value, }) }, { deep: true }, @@ -607,7 +609,6 @@ async function applyAuthSuccess(data) { user: data.user, }) await hydratePlannerRecordsFromApi() - queueSyncAllPlannerRecords() closeAuthDialog() } @@ -672,6 +673,31 @@ function logout() { syncStatus.value = 'local' syncMessage.value = '로컬 저장 모드' clearAuthState() + restoreLocalPlannerRecords() +} + +function replacePlannerRecords(nextRecords) { + Object.keys(plannerRecords).forEach((key) => { + delete plannerRecords[key] + }) + + Object.entries(nextRecords).forEach(([key, record]) => { + plannerRecords[key] = record + }) +} + +function restoreLocalPlannerRecords() { + replacePlannerRecords(createInitialPlannerRecords(plannerSeed, normalizeRecord)) + + const savedState = readPlannerStorageState() + + if (savedState.selectedDate) { + selectedDate.value = toDateValue(savedState.selectedDate, selectedDate.value) + } + + if (savedState.calendarViewDate) { + calendarViewDate.value = toDateValue(savedState.calendarViewDate, selectedDate.value) + } } function findRecordKey(record) { @@ -685,14 +711,6 @@ function clearPendingSyncTimers() { syncTimers.clear() } -function queueSyncAllPlannerRecords() { - Object.entries(plannerRecords).forEach(([key, record]) => { - if (hasPlannerContent(record)) { - schedulePlannerSync(key) - } - }) -} - function schedulePlannerSyncForRecord(record) { const recordKey = findRecordKey(record) @@ -760,11 +778,14 @@ async function hydratePlannerRecordsFromApi() { try { const result = await fetchPlannerEntries(authToken.value) + const remoteRecords = {} result.entries.forEach((entry) => { - plannerRecords[entry.entryDate] = normalizeRecord(entry.payload) + remoteRecords[entry.entryDate] = normalizeRecord(entry.payload) }) + replacePlannerRecords(remoteRecords) + syncStatus.value = 'cloud' syncMessage.value = '클라우드 동기화 연결됨' } catch (error) { diff --git a/src/lib/plannerStorage.js b/src/lib/plannerStorage.js index 24cdb36..50ee803 100644 --- a/src/lib/plannerStorage.js +++ b/src/lib/plannerStorage.js @@ -13,6 +13,10 @@ function readStorageState() { } } +export function readPlannerStorageState() { + return readStorageState() +} + export function createInitialPlannerRecords(seedRecords, normalizeRecord) { const baseRecords = Object.fromEntries( Object.entries(seedRecords).map(([key, record]) => [key, normalizeRecord(record)]), @@ -62,31 +66,37 @@ export function persistPlannerState({ calendarViewDate, statsRangeStart, statsRangeEnd, + includeRecords = true, }) { if (typeof window === 'undefined') { return } - const serializableRecords = Object.fromEntries( - Object.entries(plannerRecords).map(([key, record]) => [ - key, - { - ...record, - tasks: record.tasks.map((task) => ({ ...task })), - memo: record.memo.map((item) => ({ ...item })), - timetable: [...record.timetable], - }, - ]), - ) + const previousState = readStorageState() + const nextState = { + ...previousState, + selectedDate: selectedDate.toISOString(), + calendarViewDate: calendarViewDate.toISOString(), + statsRangeStart, + statsRangeEnd, + } + + if (includeRecords) { + nextState.records = Object.fromEntries( + Object.entries(plannerRecords).map(([key, record]) => [ + key, + { + ...record, + tasks: record.tasks.map((task) => ({ ...task })), + memo: record.memo.map((item) => ({ ...item })), + timetable: [...record.timetable], + }, + ]), + ) + } window.localStorage.setItem( STORAGE_KEY, - JSON.stringify({ - selectedDate: selectedDate.toISOString(), - calendarViewDate: calendarViewDate.toISOString(), - statsRangeStart, - statsRangeEnd, - records: serializableRecords, - }), + JSON.stringify(nextState), ) }