Compare commits

...

1 Commits

Author SHA1 Message Date
a53ef4cc6f v0.1.13 - 로컬·클라우드 데이터 분리 2026-04-21 18:16:50 +09:00
5 changed files with 65 additions and 32 deletions

View File

@@ -4,7 +4,7 @@
- 프로젝트명: 10 Minute Planner 웹 UI - 프로젝트명: 10 Minute Planner 웹 UI
- 기술 스택: Vue 3 + Vite + TailwindCSS + JavaScript - 기술 스택: Vue 3 + Vite + TailwindCSS + JavaScript
- 현재 기준 버전: `v0.1.12` - 현재 기준 버전: `v0.1.13`
- Git 원격 저장소: `https://git.sori.studio/zenn/planner.sori.studio.git` - Git 원격 저장소: `https://git.sori.studio/zenn/planner.sori.studio.git`
## 기준 디자인 ## 기준 디자인
@@ -119,6 +119,8 @@
- 현재 프론트는 인증만 연결된 상태이고, 플래너 저장은 아직 `localStorage` 기준이다. - 현재 프론트는 인증만 연결된 상태이고, 플래너 저장은 아직 `localStorage` 기준이다.
- 로그인 상태에서는 플래너 수정 시 날짜별 서버 저장을 예약하고, 로그인 직후에는 서버 데이터를 먼저 가져오도록 연결하기 시작했다. - 로그인 상태에서는 플래너 수정 시 날짜별 서버 저장을 예약하고, 로그인 직후에는 서버 데이터를 먼저 가져오도록 연결하기 시작했다.
- 현재는 로컬 저장도 계속 유지하면서 서버 저장을 병행하는 과도기 구조다. - 현재는 로컬 저장도 계속 유지하면서 서버 저장을 병행하는 과도기 구조다.
- 로그인 시 서버 플래너 데이터로 `plannerRecords`를 교체하고, 로그아웃 시에는 로컬 저장 기반 데이터로 다시 복귀하도록 정리했다.
- 이로 인해 다른 사용자 로그인 시 이전 로컬 데이터가 서버 계정 데이터와 섞일 위험을 줄였다.
- 이미지 저장 기능은 추후 `print-only` 또는 별도 export 전용 레이아웃을 기준으로 구현하면 화면/인쇄/공유 결과를 맞추기 쉽다. - 이미지 저장 기능은 추후 `print-only` 또는 별도 export 전용 레이아웃을 기준으로 구현하면 화면/인쇄/공유 결과를 맞추기 쉽다.
- Docker Compose는 프론트엔드와 백엔드를 함께 올리는 기준으로 설계하되, NAS 환경에 맞는 볼륨과 재시작 정책도 함께 고려한다. - Docker Compose는 프론트엔드와 백엔드를 함께 올리는 기준으로 설계하되, NAS 환경에 맞는 볼륨과 재시작 정책도 함께 고려한다.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "ten-minute-planner", "name": "ten-minute-planner",
"version": "0.1.12", "version": "0.1.13",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ten-minute-planner", "name": "ten-minute-planner",
"version": "0.1.12", "version": "0.1.13",
"dependencies": { "dependencies": {
"vue": "^3.5.13" "vue": "^3.5.13"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "ten-minute-planner", "name": "ten-minute-planner",
"private": true, "private": true,
"version": "0.1.12", "version": "0.1.13",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -16,6 +16,7 @@ import { fetchPlannerEntries, savePlannerEntry } from './lib/plannerApi'
import { import {
createInitialPlannerRecords, createInitialPlannerRecords,
persistPlannerState, persistPlannerState,
readPlannerStorageState,
restorePlannerUiState, restorePlannerUiState,
} from './lib/plannerStorage' } from './lib/plannerStorage'
@@ -557,6 +558,7 @@ watch(
calendarViewDate: calendarViewDate.value, calendarViewDate: calendarViewDate.value,
statsRangeStart: normalizedStatsRange.value.startKey, statsRangeStart: normalizedStatsRange.value.startKey,
statsRangeEnd: normalizedStatsRange.value.endKey, statsRangeEnd: normalizedStatsRange.value.endKey,
includeRecords: !isAuthenticated.value,
}) })
}, },
{ deep: true }, { deep: true },
@@ -607,7 +609,6 @@ async function applyAuthSuccess(data) {
user: data.user, user: data.user,
}) })
await hydratePlannerRecordsFromApi() await hydratePlannerRecordsFromApi()
queueSyncAllPlannerRecords()
closeAuthDialog() closeAuthDialog()
} }
@@ -672,6 +673,31 @@ function logout() {
syncStatus.value = 'local' syncStatus.value = 'local'
syncMessage.value = '로컬 저장 모드' syncMessage.value = '로컬 저장 모드'
clearAuthState() 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) { function findRecordKey(record) {
@@ -685,14 +711,6 @@ function clearPendingSyncTimers() {
syncTimers.clear() syncTimers.clear()
} }
function queueSyncAllPlannerRecords() {
Object.entries(plannerRecords).forEach(([key, record]) => {
if (hasPlannerContent(record)) {
schedulePlannerSync(key)
}
})
}
function schedulePlannerSyncForRecord(record) { function schedulePlannerSyncForRecord(record) {
const recordKey = findRecordKey(record) const recordKey = findRecordKey(record)
@@ -760,11 +778,14 @@ async function hydratePlannerRecordsFromApi() {
try { try {
const result = await fetchPlannerEntries(authToken.value) const result = await fetchPlannerEntries(authToken.value)
const remoteRecords = {}
result.entries.forEach((entry) => { result.entries.forEach((entry) => {
plannerRecords[entry.entryDate] = normalizeRecord(entry.payload) remoteRecords[entry.entryDate] = normalizeRecord(entry.payload)
}) })
replacePlannerRecords(remoteRecords)
syncStatus.value = 'cloud' syncStatus.value = 'cloud'
syncMessage.value = '클라우드 동기화 연결됨' syncMessage.value = '클라우드 동기화 연결됨'
} catch (error) { } catch (error) {

View File

@@ -13,6 +13,10 @@ function readStorageState() {
} }
} }
export function readPlannerStorageState() {
return readStorageState()
}
export function createInitialPlannerRecords(seedRecords, normalizeRecord) { export function createInitialPlannerRecords(seedRecords, normalizeRecord) {
const baseRecords = Object.fromEntries( const baseRecords = Object.fromEntries(
Object.entries(seedRecords).map(([key, record]) => [key, normalizeRecord(record)]), Object.entries(seedRecords).map(([key, record]) => [key, normalizeRecord(record)]),
@@ -62,31 +66,37 @@ export function persistPlannerState({
calendarViewDate, calendarViewDate,
statsRangeStart, statsRangeStart,
statsRangeEnd, statsRangeEnd,
includeRecords = true,
}) { }) {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return return
} }
const serializableRecords = Object.fromEntries( const previousState = readStorageState()
Object.entries(plannerRecords).map(([key, record]) => [ const nextState = {
key, ...previousState,
{ selectedDate: selectedDate.toISOString(),
...record, calendarViewDate: calendarViewDate.toISOString(),
tasks: record.tasks.map((task) => ({ ...task })), statsRangeStart,
memo: record.memo.map((item) => ({ ...item })), statsRangeEnd,
timetable: [...record.timetable], }
},
]), 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( window.localStorage.setItem(
STORAGE_KEY, STORAGE_KEY,
JSON.stringify({ JSON.stringify(nextState),
selectedDate: selectedDate.toISOString(),
calendarViewDate: calendarViewDate.toISOString(),
statsRangeStart,
statsRangeEnd,
records: serializableRecords,
}),
) )
} }