Skip to content

[공통] Storage 접근 제한 환경에서 클라이언트 예외 발생#1260

Merged
ff1451 merged 2 commits into
developfrom
fix/#1259/storage-access-error
May 15, 2026
Merged

[공통] Storage 접근 제한 환경에서 클라이언트 예외 발생#1260
ff1451 merged 2 commits into
developfrom
fix/#1259/storage-access-error

Conversation

@ff1451

@ff1451 ff1451 commented May 13, 2026

Copy link
Copy Markdown
Contributor

What is this PR? 🔍

Changes 📝

  • localStorage/sessionStorage 접근 시 SecurityError가 발생해도 앱이 크래시되지 않도록 안전 래퍼를 적용했습니다.
  • getJSONItem, setJSONItem을 추가해 storage JSON 파싱/직렬화 실패를 공통 처리하도록 정리했습니다.
  • Sentry에 보고된 auth auto-login, 시간표 local lecture store 초기화 지점을 기본값 fallback 방식으로 수정했습니다.
  • 앱 내 직접 localStorage/sessionStorage 접근을 안전 래퍼 사용으로 통일했습니다.

ScreenShot 📷

  • UI 변경 없음

Test CheckList ✅

  • yarn tsc --noEmit
  • yarn lint:eslint
  • localStorage/sessionStorage 직접 접근이 wrapper 내부에만 남는지 확인

Precaution

  • Storage 접근이 차단된 환경에서는 일부 로컬 캐시/로깅용 값이 저장되지 않을 수 있습니다.
  • 해당 경우에도 앱은 기본값으로 동작하며 클라이언트 예외를 발생시키지 않습니다.
  • yarn lint:eslint 실행 시 기존 Browserslist caniuse-lite outdated 경고가 출력됩니다.

✔️ Please check if the PR fulfills these requirements

  • It's submitted to the correct branch, not the develop branch unconditionally?
  • If on a hotfix branch, ensure it targets main?
  • There are no warning message when you run yarn lint

Summary by CodeRabbit

릴리스 노트

  • 리팩토링
    • 저장소 접근 메커니즘을 개선하여 다양한 환경에서의 안정성을 강화했습니다.
    • 애플리케이션 전반에서 일관된 데이터 저장 및 조회 방식을 적용했습니다.

Review Change Stack

@ff1451 ff1451 added the 🐞 BugFix 버그 수정 label May 13, 2026
@ff1451 ff1451 self-assigned this May 13, 2026
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

Walkthrough

전체 앱에서 직접 localStorage/sessionStorage 호출을 제거하고 에러 처리 기능이 있는 isomorphic storage 어댑터로 통합. Storage 접근이 차단된 환경(예: Edge 브라우저 보안 설정)에서도 앱이 크래시되지 않도록 변경.

Changes

Isomorphic Storage 마이그레이션

Layer / File(s) Summary
Storage 어댑터 기반 구조 및 JSON 헬퍼
src/utils/ts/env.ts
새로운 createIsomorphicStorage(variant) 어댑터는 try/catch로 모든 storage 작업을 감싸고, getJSONItem<T>(key, defaultValue), setJSONItem(key, value) 메서드 추가. getBrowserStorage, getStorageJSONValue 헬퍼 함수 추가로 JSON 직렬화/역직렬화 방어적 처리.
인증 플로우의 Storage 마이그레이션
src/utils/hooks/auth/useAuth.ts, useAutoLogin.ts, useLogout.ts, src/utils/ts/apiClient.ts, auth.ts
refresh token 조회를 isomorphicLocalStorage.getJSONItem으로 변경. useAuthuseCallback 메모이제이션 추가. redirect path, logout 시 modal 상태 제거를 isomorphic storage로 처리.
페이지 진입 시점의 Storage 초기화 및 추적
src/pages/timetable/index.tsx, store/[id].tsx, store/index.tsx, graduation/index.tsx, auth/modifyinfo/index.tsx
각 페이지의 진입 타임스탬프(enterTimetablePage, enter_storeDetail, visitedGraduationPage 등), 카테고리 진입 여부(cameFrom), 완료 상태를 isomorphic storage로 저장/조회.
컴포넌트 레벨 Analytics 및 상태 저장
src/components/Store/StoreDetailPage/components/Review/*, src/components/TimetablePage/*, src/components/layout/Header/*, src/components/ui/IntroToolTip/index.tsx, src/components/Course/hooks/useSelectedCourses.ts
Review 검토 진입/종료 시간, Timetable 진입 시간, Header의 뒤로가기 duration_time 계산, tooltip 상태 저장 모두 isomorphic storage로 마이그레이션.
상태 관리 훅 및 Zustand 스토어 리팩토링
src/utils/hooks/state/useLocalStorage.ts, useWebStorage.ts, src/utils/zustand/myLectures.ts
useLocalStorage/useWebStorageisomorphicLocalStoragegetJSONItem/setJSONItem 사용. myLectures Zustand 스토어는 새로운 getInitialLectures() 헬퍼로 초기값을 안전하게 로드.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • hyejun0228
  • JeongWon-CHO
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 Storage 접근 제한 환경에서의 클라이언트 예외 발생 문제를 다루고 있으며, 이는 raw_summary의 모든 변경사항(isomorphicLocalStorage/isomorphicSessionStorage 사용으로 전환)과 일치합니다.
Linked Issues check ✅ Passed PR은 #1259의 모든 코딩 요구사항을 충족합니다: (1) Storage 읽기 실패 시 기본값 처리 [#1259], (2) Storage 쓰기 실패 시 앱 중단 방지 [#1259], (3) JSON 파싱 실패 시 안전한 fallback [#1259]. 25개 파일에서 isomorphicLocalStorage/isomorphicSessionStorage 사용으로 통일되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 범위 내입니다: Storage 접근 안전화 작업(try/catch, 기본값 처리, JSON 헬퍼 추가)에만 집중하고 있으며, 비즈니스 로직이나 UI 변경은 없습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/#1259/storage-access-error

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ParkSungju01 ParkSungju01 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

고생하셨습니다!

@ff1451 ff1451 marked this pull request as ready for review May 14, 2026 04:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/pages/store/[id].tsx (1)

157-206: ⚡ Quick win

duration_time 계산 로직 중복 개선을 고려해보세요.

동일한 패턴의 duration_time 계산이 여러 곳에서 반복되고 있습니다:

  • Line 165, 172, 195, 204: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('...'))) / 1000

components/Store/utils/durationTime.ts에 이미 duration 관련 유틸이 있으니, 다음과 같은 헬퍼 함수를 추가하여 중복을 제거할 수 있습니다:

// components/Store/utils/durationTime.ts에 추가
export const getDurationFromStorageKey = (key: string): number => {
  const startTime = isomorphicSessionStorage.getItem(key);
  if (!startTime) return 0;
  return (new Date().getTime() - Number(startTime)) / 1000;
};

이후 사용 시:

duration_time: getDurationFromStorageKey('enterReviewPage')

현재 PR은 storage 마이그레이션이 주 목적이므로 이 개선은 선택적으로 진행하셔도 됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/store/`[id].tsx around lines 157 - 206, Extract the repeated
duration_time calculation into a helper (e.g., getDurationFromStorageKey) in
components/Store/utils/durationTime.ts and replace inline expressions in
onClickCallNumber and onClickList (and any other places using (new
Date().getTime() - Number(isomorphicSessionStorage.getItem('...'))) / 1000) with
duration_time: getDurationFromStorageKey('<storageKey>'); ensure the helper
reads the given storage key from isomorphicSessionStorage, returns 0 when
missing, and is imported where onClickCallNumber and onClickList are defined so
you only call getDurationFromStorageKey('enterReviewPage') and
getDurationFromStorageKey('enter_storeDetail') respectively.
src/components/TimetablePage/MainTimetablePage/DefaultPage/index.tsx (1)

21-44: ⚡ Quick win

duration_time 계산에 fallback 패턴 추가를 권장합니다.

Lines 30, 42에서 Number(isomorphicSessionStorage.getItem('enterTimetablePage'))를 사용하는데, null이 반환되면 Number(null) = 0이 되어 부정확한 duration 값이 계산됩니다.

♻️ 제안하는 수정안
 export default function DefaultPage({ timetableFrameId, setCurrentFrameId }: DefaultPageProps) {
   const router = useRouter();
   const logger = useLogger();
   const handlePopState = React.useCallback(() => {
+    const currentTime = new Date().getTime();
+    const enterTime = Number(isomorphicSessionStorage.getItem('enterTimetablePage')) || currentTime;
+    const durationTime = (currentTime - enterTime) / 1000;
+
     // swipe로 뒤로가기 시
     if (isomorphicSessionStorage.getItem('swipeToBack') === 'true') {
       logger.actionEventSwipe({
         team: 'USER',
         event_label: 'timetable_back',
         value: 'OS스와이프',
         previous_page: '시간표',
         current_page: '메인',
-        duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000,
+        duration_time: durationTime,
       });
       history.back();
       return;
     }
     // 브라우저의 뒤로가기 버튼 클릭 시 / 마우스 사이드 버튼 누를 시
     logger.actionEventClick({
       team: 'USER',
       event_label: 'timetable_back',
       value: '뒤로가기버튼',
       previous_page: '시간표',
       current_page: '메인',
-      duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000,
+      duration_time: durationTime,
     });
   }, [logger]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/TimetablePage/MainTimetablePage/DefaultPage/index.tsx` around
lines 21 - 44, handlePopState computes duration_time using
Number(isomorphicSessionStorage.getItem('enterTimetablePage')) which yields 0
when the key is missing; update handlePopState to defensively parse the stored
timestamp with a fallback (e.g., const enter =
Number(isomorphicSessionStorage.getItem('enterTimetablePage')) || Date.now(); or
check for null/NaN) and use (Date.now() - enter) / 1000 for duration_time in
both logger.actionEventSwipe and logger.actionEventClick so invalid or missing
session values don't produce misleading durations.
src/components/TimetablePage/components/MainTimetable/index.tsx (1)

139-151: ⚡ Quick win

duration_time 계산에 fallback 패턴 추가를 권장합니다.

Line 147에서 Number(isomorphicSessionStorage.getItem('enterTimetablePage'))를 사용하는데, null이 반환되는 경우 부정확한 duration 값이 계산됩니다. durationTime.tsgetTime 함수처럼 fallback 패턴을 적용하는 것을 권장합니다.

♻️ 제안하는 수정안
   const onClickDownloadImage = (e: React.MouseEvent<HTMLButtonElement>) => {
     e.stopPropagation();

     if (checkSemesterAndTimetable(mySemester, timeTableFrameList)) {
+      const currentTime = new Date().getTime();
+      const enterTime = Number(isomorphicSessionStorage.getItem('enterTimetablePage')) || currentTime;
       logger.actionEventClick({
         team: 'USER',
         event_label: 'timetable',
         value: '이미지저장',
-        duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000,
+        duration_time: (currentTime - enterTime) / 1000,
       });
       openModal();
     }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/TimetablePage/components/MainTimetable/index.tsx` around lines
139 - 151, The duration_time calculation in onClickDownloadImage uses
Number(isomorphicSessionStorage.getItem('enterTimetablePage')) which can be null
and produce NaN/incorrect values; update onClickDownloadImage to read the stored
timestamp into a variable (using the same fallback pattern as
durationTime.getTime), e.g., parse the value and if it's falsy use Date.now()
(or 0) as a fallback, then compute duration_time using that safe start value and
pass it into logger.actionEventClick so the computed duration is always valid.
src/components/Store/StoreReviewPage/ReviewForm/ReviewForm.tsx (1)

69-70: ⚡ Quick win

duration_time 계산에 fallback 패턴 추가를 권장합니다.

isomorphicSessionStorage.getItem('enterReview')null을 반환하는 경우(storage 차단 환경 또는 값 미설정), Number(null)0이 되어 현재 타임스탬프 전체가 duration으로 계산됩니다. 이는 부정확한 analytics 데이터를 전송할 수 있습니다.

durationTime.tsgetTime 함수처럼 fallback 패턴을 적용하는 것을 권장합니다.

♻️ 제안하는 수정안
-    const getReviewDurationTime =
-      (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterReview'))) / 1000;
+    const currentTime = new Date().getTime();
+    const enterTime = Number(isomorphicSessionStorage.getItem('enterReview')) || currentTime;
+    const getReviewDurationTime = (currentTime - enterTime) / 1000;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Store/StoreReviewPage/ReviewForm/ReviewForm.tsx` around lines
69 - 70, getReviewDurationTime currently computes duration with
Number(isomorphicSessionStorage.getItem('enterReview')) which treats null as 0
and yields incorrect durations; update the calculation in the
getReviewDurationTime definition to apply a fallback when getItem('enterReview')
is null/invalid (e.g., parse value safely and default to Date.now() or undefined
start timestamp), e.g., read const stored =
isomorphicSessionStorage.getItem('enterReview'), convert with a guarded parser
(Number(stored) only if stored != null and !isNaN(Number(stored))) or call the
existing getTime utility, and use that fallback to avoid treating null as 0 so
the computed duration_time is accurate and consistent with durationTime.ts's
getTime behavior.
src/utils/ts/env.ts (1)

15-23: ⚡ Quick win

원시 Storage export는 안전 래퍼를 우회하게 만듭니다.

getBrowserStorage를 공개 API로 두면 외부 코드가 다시 storage.getItem/setItem을 직접 호출하면서 여기의 try/catch 보장을 건너뛸 수 있습니다. 이번 PR 목표가 직접 storage 접근을 래퍼 내부로 모으는 것이라면, 이 헬퍼는 내부 구현으로만 두는 편이 안전합니다.

♻️ Proposed fix
-export const getBrowserStorage = (variant: StorageVariant): Storage | null => {
+const getBrowserStorage = (variant: StorageVariant): Storage | null => {
   if (!isBrowser) return null;

   try {
     return variant === 'local' ? window.localStorage : window.sessionStorage;
   } catch {
     return null;
   }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/ts/env.ts` around lines 15 - 23, getBrowserStorage currently
returns the raw Storage object (using StorageVariant, isBrowser and
window.localStorage/window.sessionStorage), which allows external code to bypass
the wrapper's try/catch guarantees; make getBrowserStorage internal only (remove
or unexport it) and ensure all external storage access goes through the safe
wrapper functions (e.g., the module's get/set helpers that call
getBrowserStorage internally), updating any consumers to use those safe helpers
instead of calling storage.getItem/setItem directly so the try/catch protection
is enforced.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/layout/Header/MobileHeader/Panel/index.tsx`:
- Around line 66-67: The duration_time calculation in the Panel component
unconditionally does (new Date().getTime() -
Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000 which
yields a wrong value when the storage item is missing; instead, read the raw
value from isomorphicSessionStorage.getItem('enterTimetablePage'), parse/convert
it to a Number with a guard (check for null/undefined and isFinite/!isNaN and
positive), and only compute (Date.now() - parsed) / 1000 when valid; otherwise
set duration_time to a safe fallback (e.g., 0 or null). Ensure you update the
assignment where duration_time is set so it uses the guarded parsed value rather
than Number(...) directly.

In `@src/utils/ts/env.ts`:
- Around line 57-61: The current setJSONItem function skips doing anything when
stringifyStorageValue(value) returns null (e.g., value is undefined), leaving
any previous value intact; change setJSONItem so that when serializedValue ===
null it calls storageAdapter.removeItem(key) (instead of returning) to
explicitly delete the key; reference the setJSONItem function and the
stringifyStorageValue and storageAdapter.setItem/removeItem calls when making
the change.

---

Nitpick comments:
In `@src/components/Store/StoreReviewPage/ReviewForm/ReviewForm.tsx`:
- Around line 69-70: getReviewDurationTime currently computes duration with
Number(isomorphicSessionStorage.getItem('enterReview')) which treats null as 0
and yields incorrect durations; update the calculation in the
getReviewDurationTime definition to apply a fallback when getItem('enterReview')
is null/invalid (e.g., parse value safely and default to Date.now() or undefined
start timestamp), e.g., read const stored =
isomorphicSessionStorage.getItem('enterReview'), convert with a guarded parser
(Number(stored) only if stored != null and !isNaN(Number(stored))) or call the
existing getTime utility, and use that fallback to avoid treating null as 0 so
the computed duration_time is accurate and consistent with durationTime.ts's
getTime behavior.

In `@src/components/TimetablePage/components/MainTimetable/index.tsx`:
- Around line 139-151: The duration_time calculation in onClickDownloadImage
uses Number(isomorphicSessionStorage.getItem('enterTimetablePage')) which can be
null and produce NaN/incorrect values; update onClickDownloadImage to read the
stored timestamp into a variable (using the same fallback pattern as
durationTime.getTime), e.g., parse the value and if it's falsy use Date.now()
(or 0) as a fallback, then compute duration_time using that safe start value and
pass it into logger.actionEventClick so the computed duration is always valid.

In `@src/components/TimetablePage/MainTimetablePage/DefaultPage/index.tsx`:
- Around line 21-44: handlePopState computes duration_time using
Number(isomorphicSessionStorage.getItem('enterTimetablePage')) which yields 0
when the key is missing; update handlePopState to defensively parse the stored
timestamp with a fallback (e.g., const enter =
Number(isomorphicSessionStorage.getItem('enterTimetablePage')) || Date.now(); or
check for null/NaN) and use (Date.now() - enter) / 1000 for duration_time in
both logger.actionEventSwipe and logger.actionEventClick so invalid or missing
session values don't produce misleading durations.

In `@src/pages/store/`[id].tsx:
- Around line 157-206: Extract the repeated duration_time calculation into a
helper (e.g., getDurationFromStorageKey) in
components/Store/utils/durationTime.ts and replace inline expressions in
onClickCallNumber and onClickList (and any other places using (new
Date().getTime() - Number(isomorphicSessionStorage.getItem('...'))) / 1000) with
duration_time: getDurationFromStorageKey('<storageKey>'); ensure the helper
reads the given storage key from isomorphicSessionStorage, returns 0 when
missing, and is imported where onClickCallNumber and onClickList are defined so
you only call getDurationFromStorageKey('enterReviewPage') and
getDurationFromStorageKey('enter_storeDetail') respectively.

In `@src/utils/ts/env.ts`:
- Around line 15-23: getBrowserStorage currently returns the raw Storage object
(using StorageVariant, isBrowser and window.localStorage/window.sessionStorage),
which allows external code to bypass the wrapper's try/catch guarantees; make
getBrowserStorage internal only (remove or unexport it) and ensure all external
storage access goes through the safe wrapper functions (e.g., the module's
get/set helpers that call getBrowserStorage internally), updating any consumers
to use those safe helpers instead of calling storage.getItem/setItem directly so
the try/catch protection is enforced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 19d6c6a1-c353-4406-b086-696c773cd0b7

📥 Commits

Reviewing files that changed from the base of the PR and between c060862 and a325849.

📒 Files selected for processing (29)
  • src/components/Course/hooks/useSelectedCourses.ts
  • src/components/IndexComponents/IndexCafeteria/index.tsx
  • src/components/Store/StoreDetailPage/components/Review/components/ReviewButton/index.tsx
  • src/components/Store/StoreDetailPage/components/Review/index.tsx
  • src/components/Store/StoreReviewPage/ReviewForm/ReviewForm.tsx
  • src/components/Store/utils/durationTime.ts
  • src/components/TimetablePage/MainTimetablePage/DefaultPage/index.tsx
  • src/components/TimetablePage/components/MainTimetable/index.tsx
  • src/components/TimetablePage/hooks/useTimetableMutation.ts
  • src/components/layout/Header/MobileHeader/Panel/index.tsx
  • src/components/layout/Header/MobileHeader/index.tsx
  • src/components/layout/Header/PCHeader/index.tsx
  • src/components/ui/IntroToolTip/index.tsx
  • src/pages/_app.tsx
  • src/pages/auth/modifyinfo/index.tsx
  • src/pages/graduation/index.tsx
  • src/pages/store/[id].tsx
  • src/pages/store/index.tsx
  • src/pages/timetable/index.tsx
  • src/utils/hooks/abTest/useABTestView.ts
  • src/utils/hooks/auth/useAuth.ts
  • src/utils/hooks/auth/useAutoLogin.ts
  • src/utils/hooks/auth/useLogout.ts
  • src/utils/hooks/state/useLocalStorage.ts
  • src/utils/hooks/state/useWebStorage.ts
  • src/utils/ts/apiClient.ts
  • src/utils/ts/auth.ts
  • src/utils/ts/env.ts
  • src/utils/zustand/myLectures.ts

Comment on lines +66 to 67
duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

duration_time 기본값 방어가 필요합니다.

Line 66에서 storage 값이 없거나 접근이 막히면 Number(null)이 0이 되어 비정상적으로 큰 duration_time이 기록됩니다. fallback 계산을 분리해 주세요.

제안 코드
+      const enteredAt = Number(isomorphicSessionStorage.getItem('enterTimetablePage'));
+      const durationTime = Number.isFinite(enteredAt) && enteredAt > 0 ? (Date.now() - enteredAt) / 1000 : 0;
       logger.actionEventClick({
         team: 'USER',
         event_label: 'timetable_back',
         value: '햄버거',
         previous_page: '시간표',
         current_page: title,
-        duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000,
+        duration_time: durationTime,
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/layout/Header/MobileHeader/Panel/index.tsx` around lines 66 -
67, The duration_time calculation in the Panel component unconditionally does
(new Date().getTime() -
Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000 which
yields a wrong value when the storage item is missing; instead, read the raw
value from isomorphicSessionStorage.getItem('enterTimetablePage'), parse/convert
it to a Number with a guard (check for null/undefined and isFinite/!isNaN and
positive), and only compute (Date.now() - parsed) / 1000 when valid; otherwise
set duration_time to a safe fallback (e.g., 0 or null). Ensure you update the
assignment where duration_time is set so it uses the guarded parsed value rather
than Number(...) directly.

Comment thread src/utils/ts/env.ts
Comment on lines +57 to +61
setJSONItem(key: string, value: unknown): void {
const serializedValue = stringifyStorageValue(value);
if (serializedValue === null) return;

storageAdapter.setItem(key, serializedValue);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

undefined 저장 시 이전 값이 그대로 남습니다.

Line 58에서 JSON.stringify(undefined)는 문자열을 만들지 못해서 현재 분기상 아무 작업도 하지 않습니다. 기존 키가 있던 상태에서 setJSONItem(key, undefined)를 호출하면 저장값이 지워지지 않아 다음 읽기에서 stale 값이 복원됩니다. undefined는 명시적으로 removeItem 처리하는 편이 안전합니다.

🛡️ Proposed fix
     setJSONItem(key: string, value: unknown): void {
+      if (value === undefined) {
+        storageAdapter.removeItem(key);
+        return;
+      }
+
       const serializedValue = stringifyStorageValue(value);
       if (serializedValue === null) return;

       storageAdapter.setItem(key, serializedValue);
     },

As per coding guidelines, src/utils/**: "엣지 케이스(null, undefined, 빈 배열 등) 처리가 되어 있는지 확인해주세요."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/ts/env.ts` around lines 57 - 61, The current setJSONItem function
skips doing anything when stringifyStorageValue(value) returns null (e.g., value
is undefined), leaving any previous value intact; change setJSONItem so that
when serializedValue === null it calls storageAdapter.removeItem(key) (instead
of returning) to explicitly delete the key; reference the setJSONItem function
and the stringifyStorageValue and storageAdapter.setItem/removeItem calls when
making the change.

@ff1451 ff1451 merged commit fa5b51a into develop May 15, 2026
5 checks passed
@github-actions github-actions Bot deleted the fix/#1259/storage-access-error branch May 15, 2026 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 BugFix 버그 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[공통] Storage 접근 제한 환경에서 클라이언트 예외 발생

2 participants