From ef2bc6c310fedb3cce467a6e8b122dcc0ecabbc9 Mon Sep 17 00:00:00 2001 From: Seongbin Kim Date: Sun, 15 Mar 2026 17:00:24 +0900 Subject: [PATCH 1/3] Persist selected date across refresh --- .../src/store/RawLogStorageSyncMiddleware.ts | 14 +++++- apps/web/src/store/logs.test.ts | 48 +++++++++++++++++++ apps/web/src/store/logs.ts | 7 ++- apps/web/src/utils/StorageUtil.test.ts | 31 +++++++++++- apps/web/src/utils/StorageUtil.ts | 24 +++++++++- 5 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/store/logs.test.ts diff --git a/apps/web/src/store/RawLogStorageSyncMiddleware.ts b/apps/web/src/store/RawLogStorageSyncMiddleware.ts index 64f7aa1..2c660f5 100644 --- a/apps/web/src/store/RawLogStorageSyncMiddleware.ts +++ b/apps/web/src/store/RawLogStorageSyncMiddleware.ts @@ -4,7 +4,11 @@ import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit'; import { getLogFromServer, saveLogToServer } from '../services/LogService'; import { detectConflict } from '../utils/ConflictDetector'; import { calculateHashSync } from '../utils/HashUtil'; -import { loadFromStorage, saveToStorage } from '../utils/StorageUtil'; +import { + loadFromStorage, + saveCurrentDateToStorage, + saveToStorage, +} from '../utils/StorageUtil'; import type { AppDispatch, RootState } from '.'; import { loginSuccess } from './auth'; import { @@ -36,6 +40,14 @@ const startAppListening = const shortHash = (value: string | null | undefined): string => typeof value === 'string' ? value.substring(0, 8) : 'null'; +startAppListening({ + matcher: isAnyOf(goToToday, goToPrevDate, goToNextDate), + effect: async (_, listenerApi) => { + const { currentDate } = listenerApi.getState().logs; + saveCurrentDateToStorage(currentDate); + }, +}); + // localstorage에 저장 & 배업 (Server Sync with Debounce) startAppListening({ actionCreator: updateRawLog, diff --git a/apps/web/src/store/logs.test.ts b/apps/web/src/store/logs.test.ts new file mode 100644 index 0000000..337baff --- /dev/null +++ b/apps/web/src/store/logs.test.ts @@ -0,0 +1,48 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +describe('logs initial state', () => { + beforeEach(() => { + localStorage.clear(); + vi.resetModules(); + }); + + it('저장된 현재 날짜가 없으면 오늘 날짜로 초기화해야 함', async () => { + const { getTodayString } = await import('../utils/DateUtil'); + const { LogsReducer } = await import('./logs'); + + const state = LogsReducer(undefined, { type: '@@INIT' }); + + expect(state.currentDate).toBe(getTodayString()); + }); + + it('저장된 현재 날짜가 있으면 그 날짜로 초기화해야 함', async () => { + localStorage.setItem('my-commit:current-date', '2026-03-10'); + localStorage.setItem( + '2026-03-10', + JSON.stringify({ + content: 'Persisted log', + contentHash: 'hash', + parentHash: null, + localUpdatedAt: '2026-03-10T10:00:00.000Z', + }), + ); + + const { LogsReducer } = await import('./logs'); + + const state = LogsReducer(undefined, { type: '@@INIT' }); + + expect(state.currentDate).toBe('2026-03-10'); + expect(state.rawLogs).toBe('Persisted log'); + }); + + it('저장된 현재 날짜 형식이 잘못되면 오늘 날짜로 폴백해야 함', async () => { + localStorage.setItem('my-commit:current-date', 'invalid-date'); + + const { getTodayString } = await import('../utils/DateUtil'); + const { LogsReducer } = await import('./logs'); + + const state = LogsReducer(undefined, { type: '@@INIT' }); + + expect(state.currentDate).toBe(getTodayString()); + }); +}); diff --git a/apps/web/src/store/logs.ts b/apps/web/src/store/logs.ts index 4ba4dcb..6483711 100644 --- a/apps/web/src/store/logs.ts +++ b/apps/web/src/store/logs.ts @@ -7,7 +7,10 @@ import { } from '../utils/DateUtil'; import { createLogsFromString } from '../utils/LogConverter'; import { Log } from '../utils/PaceUtil'; -import { loadFromStorage } from '../utils/StorageUtil'; +import { + loadCurrentDateFromStorage, + loadFromStorage, +} from '../utils/StorageUtil'; export type ConflictState = { localContent: string; @@ -28,7 +31,7 @@ export type LogState = { }; // reducer 바깥이어서 초기값 설정 구문이 순수하지 않아도 괜찮을 듯 -const initialDate = getTodayString(); +const initialDate = loadCurrentDateFromStorage() ?? getTodayString(); const initialLocalData = loadFromStorage(initialDate); const initialState: LogState = { diff --git a/apps/web/src/utils/StorageUtil.test.ts b/apps/web/src/utils/StorageUtil.test.ts index 0be2d8a..c1e35f9 100644 --- a/apps/web/src/utils/StorageUtil.test.ts +++ b/apps/web/src/utils/StorageUtil.test.ts @@ -1,7 +1,13 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { calculateHashSync } from './HashUtil'; -import { loadFromStorage, saveToStorage } from './StorageUtil'; +import { + CURRENT_DATE_STORAGE_KEY, + loadCurrentDateFromStorage, + loadFromStorage, + saveCurrentDateToStorage, + saveToStorage, +} from './StorageUtil'; describe('StorageUtil', () => { beforeEach(() => { @@ -9,6 +15,29 @@ describe('StorageUtil', () => { localStorage.clear(); }); + describe('current date persistence', () => { + it('현재 날짜를 저장하고 로드해야 함', () => { + saveCurrentDateToStorage('2026-03-15'); + + expect(localStorage.getItem(CURRENT_DATE_STORAGE_KEY)).toBe('2026-03-15'); + expect(loadCurrentDateFromStorage()).toBe('2026-03-15'); + }); + + it('잘못된 형식의 현재 날짜는 무시해야 함', () => { + localStorage.setItem(CURRENT_DATE_STORAGE_KEY, '2026/03/15'); + + expect(loadCurrentDateFromStorage()).toBeNull(); + }); + + it('잘못된 현재 날짜를 저장하려 하면 키를 제거해야 함', () => { + localStorage.setItem(CURRENT_DATE_STORAGE_KEY, '2026-03-15'); + + saveCurrentDateToStorage('invalid-date'); + + expect(localStorage.getItem(CURRENT_DATE_STORAGE_KEY)).toBeNull(); + }); + }); + describe('loadFromStorage', () => { it('빈 키로 로드 시 빈 데이터를 반환해야 함', () => { const data = loadFromStorage('2026-01-12'); diff --git a/apps/web/src/utils/StorageUtil.ts b/apps/web/src/utils/StorageUtil.ts index 9723182..e1e3614 100644 --- a/apps/web/src/utils/StorageUtil.ts +++ b/apps/web/src/utils/StorageUtil.ts @@ -7,6 +7,28 @@ export interface LocalLogData { localUpdatedAt: string; // 로컬에서 마지막 수정 시간 } +const LOG_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; +export const CURRENT_DATE_STORAGE_KEY = 'my-commit:current-date'; + +export const loadCurrentDateFromStorage = (): string | null => { + const storedDate = localStorage.getItem(CURRENT_DATE_STORAGE_KEY); + + if (!storedDate || !LOG_DATE_REGEX.test(storedDate)) { + return null; + } + + return storedDate; +}; + +export const saveCurrentDateToStorage = (date: string): void => { + if (!LOG_DATE_REGEX.test(date)) { + localStorage.removeItem(CURRENT_DATE_STORAGE_KEY); + return; + } + + localStorage.setItem(CURRENT_DATE_STORAGE_KEY, date); +}; + export const loadFromStorage = (key: string): LocalLogData => { const stored = localStorage.getItem(key); if (!stored) { @@ -99,8 +121,6 @@ export const saveToStorage = ( // Re-export LocalStorageManager from its new location export { LocalStorageManager } from './LocalStorageManager'; -const LOG_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; - /** * localStorage에서 모든 로그 데이터(날짜 키)를 삭제합니다. * 설정 및 기타 데이터는 유지됩니다. From 4d92ce2b758d2d1438a6c4bafde947d213b6e69b Mon Sep 17 00:00:00 2001 From: Seongbin Kim Date: Sun, 15 Mar 2026 17:18:53 +0900 Subject: [PATCH 2/3] Validate persisted current dates --- apps/web/src/utils/DateUtil.ts | 22 ++++++++++++++++++++++ apps/web/src/utils/StorageUtil.test.ts | 14 ++++++++++++++ apps/web/src/utils/StorageUtil.ts | 8 ++++---- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/apps/web/src/utils/DateUtil.ts b/apps/web/src/utils/DateUtil.ts index bec030f..f819f40 100644 --- a/apps/web/src/utils/DateUtil.ts +++ b/apps/web/src/utils/DateUtil.ts @@ -6,6 +6,28 @@ export const getDateString = (date: Date) => { )}`; }; +const DATE_STRING_REGEX = /^\d{4}-\d{2}-\d{2}$/; + +export const isValidDateString = (dateString: string) => { + if (!DATE_STRING_REGEX.test(dateString)) { + return false; + } + + const [yearString, monthString, dayString] = dateString.split('-'); + const year = Number(yearString); + const month = Number(monthString); + const day = Number(dayString); + + const parsedDate = new Date(year, month - 1, day); + + return ( + Number.isFinite(parsedDate.getTime()) && + parsedDate.getFullYear() === year && + parsedDate.getMonth() === month - 1 && + parsedDate.getDate() === day + ); +}; + // TODO: validation하기 export const justOneDayAwayAtMost = ( dateAString: string, diff --git a/apps/web/src/utils/StorageUtil.test.ts b/apps/web/src/utils/StorageUtil.test.ts index c1e35f9..fc6e551 100644 --- a/apps/web/src/utils/StorageUtil.test.ts +++ b/apps/web/src/utils/StorageUtil.test.ts @@ -29,6 +29,12 @@ describe('StorageUtil', () => { expect(loadCurrentDateFromStorage()).toBeNull(); }); + it('형식은 맞지만 실제 달력이 아닌 날짜는 무시해야 함', () => { + localStorage.setItem(CURRENT_DATE_STORAGE_KEY, '2026-13-40'); + + expect(loadCurrentDateFromStorage()).toBeNull(); + }); + it('잘못된 현재 날짜를 저장하려 하면 키를 제거해야 함', () => { localStorage.setItem(CURRENT_DATE_STORAGE_KEY, '2026-03-15'); @@ -36,6 +42,14 @@ describe('StorageUtil', () => { expect(localStorage.getItem(CURRENT_DATE_STORAGE_KEY)).toBeNull(); }); + + it('형식은 맞지만 실제 달력이 아닌 날짜를 저장하려 하면 키를 제거해야 함', () => { + localStorage.setItem(CURRENT_DATE_STORAGE_KEY, '2026-03-15'); + + saveCurrentDateToStorage('2026-02-30'); + + expect(localStorage.getItem(CURRENT_DATE_STORAGE_KEY)).toBeNull(); + }); }); describe('loadFromStorage', () => { diff --git a/apps/web/src/utils/StorageUtil.ts b/apps/web/src/utils/StorageUtil.ts index e1e3614..a17f670 100644 --- a/apps/web/src/utils/StorageUtil.ts +++ b/apps/web/src/utils/StorageUtil.ts @@ -1,4 +1,5 @@ import { calculateHashSync } from './HashUtil'; +import { isValidDateString } from './DateUtil'; export interface LocalLogData { content: string; @@ -7,13 +8,12 @@ export interface LocalLogData { localUpdatedAt: string; // 로컬에서 마지막 수정 시간 } -const LOG_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; export const CURRENT_DATE_STORAGE_KEY = 'my-commit:current-date'; export const loadCurrentDateFromStorage = (): string | null => { const storedDate = localStorage.getItem(CURRENT_DATE_STORAGE_KEY); - if (!storedDate || !LOG_DATE_REGEX.test(storedDate)) { + if (!storedDate || !isValidDateString(storedDate)) { return null; } @@ -21,7 +21,7 @@ export const loadCurrentDateFromStorage = (): string | null => { }; export const saveCurrentDateToStorage = (date: string): void => { - if (!LOG_DATE_REGEX.test(date)) { + if (!isValidDateString(date)) { localStorage.removeItem(CURRENT_DATE_STORAGE_KEY); return; } @@ -130,7 +130,7 @@ export const clearAllLogData = (): void => { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); - if (key && LOG_DATE_REGEX.test(key)) { + if (key && isValidDateString(key)) { keysToDelete.push(key); } } From ebafe11152cd98cb8d4311443b3a53b3071dea70 Mon Sep 17 00:00:00 2001 From: Seongbin Kim Date: Sun, 15 Mar 2026 17:44:33 +0900 Subject: [PATCH 3/3] Fix import order in storage util --- apps/web/src/utils/StorageUtil.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/utils/StorageUtil.ts b/apps/web/src/utils/StorageUtil.ts index a17f670..690aa57 100644 --- a/apps/web/src/utils/StorageUtil.ts +++ b/apps/web/src/utils/StorageUtil.ts @@ -1,5 +1,5 @@ -import { calculateHashSync } from './HashUtil'; import { isValidDateString } from './DateUtil'; +import { calculateHashSync } from './HashUtil'; export interface LocalLogData { content: string;