Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion apps/web/src/store/RawLogStorageSyncMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions apps/web/src/store/logs.test.ts
Original file line number Diff line number Diff line change
@@ -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());
});
});
7 changes: 5 additions & 2 deletions apps/web/src/store/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,7 +31,7 @@ export type LogState = {
};

// reducer 바깥이어서 초기값 설정 구문이 순수하지 않아도 괜찮을 듯
const initialDate = getTodayString();
const initialDate = loadCurrentDateFromStorage() ?? getTodayString();
const initialLocalData = loadFromStorage(initialDate);

const initialState: LogState = {
Expand Down
22 changes: 22 additions & 0 deletions apps/web/src/utils/DateUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
45 changes: 44 additions & 1 deletion apps/web/src/utils/StorageUtil.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,57 @@
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(() => {
// localStorage 초기화
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');
Expand Down
26 changes: 23 additions & 3 deletions apps/web/src/utils/StorageUtil.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isValidDateString } from './DateUtil';
import { calculateHashSync } from './HashUtil';

export interface LocalLogData {
Expand All @@ -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) {
Expand Down Expand Up @@ -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에서 모든 로그 데이터(날짜 키)를 삭제합니다.
* 설정 및 기타 데이터는 유지됩니다.
Expand All @@ -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)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep clear-all aligned with date-key matching

clearAllLogData now deletes only keys that pass isValidDateString, so malformed-but-date-shaped keys (for example 2026-13-40) are no longer removed. This creates inconsistent behavior because data-management paths still treat regex-matching keys as logs (apps/web/src/features/dataManagement/backupService.ts uses LOG_DATE_REGEX when exporting/importing), so a user can end up with stale malformed log entries that survive “clear all” and get re-exported. Use the same key predicate across these flows or malformed keys will persist unexpectedly.

Useful? React with 👍 / 👎.

keysToDelete.push(key);
}
}
Expand Down
Loading