diff --git a/HANDOFF.md b/HANDOFF.md index a786457..a51d280 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -4,7 +4,7 @@ - 프로젝트명: 10 Minute Planner 웹 UI - 기술 스택: Vue 3 + Vite + TailwindCSS + JavaScript -- 현재 기준 버전: `v0.1.14` +- 현재 기준 버전: `v0.1.15` - Git 원격 저장소: `https://git.sori.studio/zenn/planner.sori.studio.git` ## 기준 디자인 @@ -122,6 +122,8 @@ - 로그인 시 서버 플래너 데이터로 `plannerRecords`를 교체하고, 로그아웃 시에는 로컬 저장 기반 데이터로 다시 복귀하도록 정리했다. - 이로 인해 다른 사용자 로그인 시 이전 로컬 데이터가 서버 계정 데이터와 섞일 위험을 줄였다. - 로그인 상태에서 특정 날짜의 플래너 내용을 완전히 비우면, 서버 저장 대신 해당 날짜 엔트리를 삭제하도록 정리했다. +- 현재는 로그인 전 플래너 진입을 막고, 인증 후에만 실제 플래너/통계 화면을 사용하도록 변경했다. +- 클라우드 저장 상태는 헤더가 아니라 오른쪽 하단의 작은 토스트 형태로 표시되도록 변경했다. - 이미지 저장 기능은 추후 `print-only` 또는 별도 export 전용 레이아웃을 기준으로 구현하면 화면/인쇄/공유 결과를 맞추기 쉽다. - Docker Compose는 프론트엔드와 백엔드를 함께 올리는 기준으로 설계하되, NAS 환경에 맞는 볼륨과 재시작 정책도 함께 고려한다. diff --git a/TODO.md b/TODO.md index c5cfa8c..a5be9cb 100644 --- a/TODO.md +++ b/TODO.md @@ -96,4 +96,5 @@ - 현재 백엔드는 사용자별 플래너 단건 저장/조회와 범위 조회 API까지 포함한다. - 프론트에는 로그인/회원가입 모달과 현재 사용자 상태 표시가 추가되었다. - 로그인 상태일 때는 서버 저장을 우선 사용하는 흐름으로 전환 중이다. +- 로그인 전에는 플래너 본문을 사용하지 못하도록 막고, 인증 후 사용 흐름으로 정리했다. - 구현할 때마다 완료된 항목은 체크하고, 큰 결정사항은 `HANDOFF.md`에도 함께 반영한다. diff --git a/package-lock.json b/package-lock.json index 52c331a..a1699d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ten-minute-planner", - "version": "0.1.14", + "version": "0.1.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ten-minute-planner", - "version": "0.1.14", + "version": "0.1.15", "dependencies": { "vue": "^3.5.13" }, diff --git a/package.json b/package.json index 29db8cb..981dc0d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ten-minute-planner", "private": true, - "version": "0.1.14", + "version": "0.1.15", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.vue b/src/App.vue index 9152c3f..1f006af 100644 --- a/src/App.vue +++ b/src/App.vue @@ -31,6 +31,7 @@ const authToken = ref('') const currentUser = ref(null) const syncStatus = ref('local') const syncMessage = ref('') +const syncToastVisible = ref(false) const selectedDate = ref(new Date()) const calendarViewDate = ref(new Date(selectedDate.value)) const statsRangeStart = ref(toKey(new Date(new Date().setDate(new Date().getDate() - 6)))) @@ -52,6 +53,7 @@ const timetableCellCount = hours.length * 6 let printPageStyleElement = null let isHydratingRemoteRecords = false const syncTimers = new Map() +let syncToastTimer = null function createEmptyTimetable() { return Array.from({ length: timetableCellCount }, () => false) @@ -577,6 +579,30 @@ function clearTaskLabels(record) { schedulePlannerSyncForRecord(record) } +function setSyncFeedback(status, message, options = {}) { + const { + visible = true, + duration = 1600, + sticky = false, + } = options + + syncStatus.value = status + syncMessage.value = message + + if (syncToastTimer) { + window.clearTimeout(syncToastTimer) + syncToastTimer = null + } + + syncToastVisible.value = visible + + if (visible && !sticky) { + syncToastTimer = window.setTimeout(() => { + syncToastVisible.value = false + }, duration) + } +} + function resetAuthForm() { authForm.nickname = '' authForm.email = '' @@ -602,8 +628,7 @@ function updateAuthField({ field, value }) { async function applyAuthSuccess(data) { authToken.value = data.token currentUser.value = data.user - syncStatus.value = 'cloud' - syncMessage.value = '클라우드 동기화 연결됨' + setSyncFeedback('cloud', '클라우드 동기화 연결됨') persistAuthState({ token: data.token, user: data.user, @@ -650,8 +675,7 @@ async function restoreAuthSession() { try { const result = await fetchCurrentUser(savedAuth.token) currentUser.value = result.user - syncStatus.value = 'cloud' - syncMessage.value = '클라우드 동기화 연결됨' + setSyncFeedback('cloud', '클라우드 동기화 연결됨') persistAuthState({ token: savedAuth.token, user: result.user, @@ -660,8 +684,9 @@ async function restoreAuthSession() { } catch (error) { authToken.value = '' currentUser.value = null - syncStatus.value = 'local' - syncMessage.value = '로컬 저장 모드' + setSyncFeedback('local', '로그인 후 클라우드 저장을 사용할 수 있습니다.', { + visible: false, + }) clearAuthState() } } @@ -670,8 +695,9 @@ function logout() { clearPendingSyncTimers() authToken.value = '' currentUser.value = null - syncStatus.value = 'local' - syncMessage.value = '로컬 저장 모드' + setSyncFeedback('local', '로그인 후 클라우드 저장을 사용할 수 있습니다.', { + visible: false, + }) clearAuthState() restoreLocalPlannerRecords() } @@ -730,8 +756,9 @@ function schedulePlannerSync(recordKey) { window.clearTimeout(syncTimers.get(recordKey)) } - syncStatus.value = 'syncing' - syncMessage.value = '클라우드 저장 중...' + setSyncFeedback('syncing', '클라우드 저장 중...', { + sticky: true, + }) const timerId = window.setTimeout(async () => { syncTimers.delete(recordKey) @@ -741,8 +768,7 @@ function schedulePlannerSync(recordKey) { if (!record) { if (syncTimers.size === 0) { - syncStatus.value = 'cloud' - syncMessage.value = '클라우드 동기화 연결됨' + setSyncFeedback('cloud', '클라우드 동기화 연결됨') } return } @@ -751,8 +777,7 @@ function schedulePlannerSync(recordKey) { await deletePlannerEntry(authToken.value, recordKey) if (syncTimers.size === 0) { - syncStatus.value = 'cloud' - syncMessage.value = '클라우드에서 삭제됨' + setSyncFeedback('cloud', '클라우드에서 삭제됨') } return } @@ -765,12 +790,12 @@ function schedulePlannerSync(recordKey) { }) if (syncTimers.size === 0) { - syncStatus.value = 'cloud' - syncMessage.value = '클라우드에 저장됨' + setSyncFeedback('cloud', '클라우드에 저장됨') } } catch (error) { - syncStatus.value = 'error' - syncMessage.value = error.message || '클라우드 저장에 실패했습니다.' + setSyncFeedback('error', error.message || '클라우드 저장에 실패했습니다.', { + duration: 3200, + }) } }, 500) @@ -783,8 +808,9 @@ async function hydratePlannerRecordsFromApi() { } isHydratingRemoteRecords = true - syncStatus.value = 'syncing' - syncMessage.value = '클라우드 데이터를 불러오는 중...' + setSyncFeedback('syncing', '클라우드 데이터를 불러오는 중...', { + sticky: true, + }) try { const result = await fetchPlannerEntries(authToken.value) @@ -796,11 +822,11 @@ async function hydratePlannerRecordsFromApi() { replacePlannerRecords(remoteRecords) - syncStatus.value = 'cloud' - syncMessage.value = '클라우드 동기화 연결됨' + setSyncFeedback('cloud', '클라우드 동기화 연결됨') } catch (error) { - syncStatus.value = 'error' - syncMessage.value = error.message || '클라우드 데이터를 불러오지 못했습니다.' + setSyncFeedback('error', error.message || '클라우드 데이터를 불러오지 못했습니다.', { + duration: 3200, + }) } finally { isHydratingRemoteRecords = false } @@ -834,7 +860,9 @@ async function printSelectedPlanner(layout = 'single') { } onMounted(() => { - syncMessage.value = '로컬 저장 모드' + setSyncFeedback('local', '로그인 후 클라우드 저장을 사용할 수 있습니다.', { + visible: false, + }) restoreAuthSession() }) @@ -861,12 +889,6 @@ onMounted(() => {

{{ currentUser.nickname }}

-

- {{ syncMessage }} -

-
+
-
+
-
+
+ +
+
+ + +