Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a53ef4cc6f |
@@ -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
4
package-lock.json
generated
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
41
src/App.vue
41
src/App.vue
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user