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/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 0be2d8a..fc6e551 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,43 @@ 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-13-40'); + + expect(loadCurrentDateFromStorage()).toBeNull(); + }); + + it('잘못된 현재 날짜를 저장하려 하면 키를 제거해야 함', () => { + localStorage.setItem(CURRENT_DATE_STORAGE_KEY, '2026-03-15'); + + saveCurrentDateToStorage('invalid-date'); + + 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', () => { 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..690aa57 100644 --- a/apps/web/src/utils/StorageUtil.ts +++ b/apps/web/src/utils/StorageUtil.ts @@ -1,3 +1,4 @@ +import { isValidDateString } from './DateUtil'; import { calculateHashSync } from './HashUtil'; export interface LocalLogData { @@ -7,6 +8,27 @@ export interface LocalLogData { localUpdatedAt: string; // 로컬에서 마지막 수정 시간 } +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 || !isValidDateString(storedDate)) { + return null; + } + + return storedDate; +}; + +export const saveCurrentDateToStorage = (date: string): void => { + if (!isValidDateString(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에서 모든 로그 데이터(날짜 키)를 삭제합니다. * 설정 및 기타 데이터는 유지됩니다. @@ -110,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); } }