Skip to content

v0.3patch#15

Merged
rktclgh merged 4 commits into
developfrom
v0.3patch
Mar 11, 2026
Merged

v0.3patch#15
rktclgh merged 4 commits into
developfrom
v0.3patch

Conversation

@rktclgh

@rktclgh rktclgh commented Mar 11, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • 새로운 기능

    • 이용약관, 개인정보처리방침, 서비스 소개 페이지 추가
    • 인증/비회원 공통 푸터 추가로 일관된 하단 링크 제공
    • 인터뷰 재개 모달 도입(중단된 연습 세션 이어하기 또는 폐기)
    • 계정 탈퇴(회원 탈퇴) 확인 모달 및 흐름 추가
  • 개선

    • 페이지 메타(타이틀, SEO/오픈그래프, 트위터 카드) 강화
    • 세션 인증 흐름 및 상단 네비게이션 링크 개선
    • 관리자 콘솔에 접속 통계, 키워드 검색 및 새로고침 기능 추가
    • 회원가입 시 약관 동의 필수화

@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1bbf978a-520d-4caa-b1e3-1566274aaeff

📥 Commits

Reviewing files that changed from the base of the PR and between 9e177a3 and bb2a6a0.

📒 Files selected for processing (3)
  • src/lib/apiClient.js
  • src/pages/content/AdminConsolePage.jsx
  • src/pages/content/MyPage.jsx

📝 Walkthrough

Walkthrough

인덱스 메타/타이틀 업데이트와 함께 약관·개인정보·서비스 소개 페이지 및 공통 레이아웃/섹션 컴포넌트가 추가되었고, 미완료 인터뷰 세션 재개(모달·API·스냅샷) 기능이 도입되었습니다. 계정 삭제 API 연동·UI, 관리자 콘솔의 접속 요약·복구 기능 확장, 프로필 기반 브라우저 세션 가드 전환, 인증 푸터 재사용도 포함됩니다.

Changes

Cohort / File(s) Summary
HTML / 앱 라우팅
index.html, src/App.jsx
페이지 타이틀을 "VlaInter"로 변경하고 OG/Twitter/meta 태그 추가. /terms, /privacy, /about 라우트 등록.
법적 문서·레이아웃
src/pages/TermsPage.jsx, src/pages/PrivacyPolicyPage.jsx, src/pages/ServiceIntroPage.jsx, src/components/LegalDocumentLayout.jsx, src/components/LegalSection.jsx
이용약관·개인정보처리방침·서비스 소개 페이지 추가 및 공통 레이아웃/섹션 컴포넌트 도입 — 정적 문서 렌더링 중심.
공용 인증 UI / 네비게이션
src/components/AuthFooter.jsx, src/components/TopNav.jsx, src/pages/auth/Login.jsx, src/pages/auth/ForgotPassword.jsx, src/pages/auth/Join.jsx, src/pages/StartingPage.jsx
재사용 가능한 AuthFooter 추가 및 인증 페이지 적용, TopNav의 일부 앵커를 Link로 전환, 회원가입에 정책 동의 체크 도입.
세션 가드
src/components/BrowserSessionGuard.jsx
브라우저 세션 검사 로직을 세션-플래그 기반에서 프로필 조회 기반으로 변경(프로필 확인 → mark/clear 흐름 및 로그아웃 시 정리).
세션 재개(인터뷰)
src/components/ResumeSessionModal.jsx, src/lib/resumeInterviewSession.js, src/lib/interviewApi.js, src/pages/content/InterviewStartPage.jsx, src/pages/content/TechPracticePage.jsx, src/pages/content/QuestionSetsPage.jsx
최신 미완료 세션 조회·해제 API 추가, ResumeSessionModal 컴포넌트와 스냅샷 빌더 도입, 여러 인터뷰/연습 페이지에 재개/해제 플로우 통합.
관리자 콘솔 · adminApi
src/lib/adminApi.js, src/pages/content/AdminConsolePage.jsx
getAdminMembers에 키워드 필터 추가, 전역 접속 요약 조회·갱신, 멤버 세부 갱신·복구 API 추가 및 관리자 UI에 통합(검색·갱신·복구 흐름).
API 클라이언트 / 사용자 API
src/lib/apiClient.js, src/lib/userApi.js
apiRequest의 401 재시도 판단 로직을 요청 메서드 기반으로 변경(재시도 자격 판정 이동). deleteMyAccount() 추가.
MyPage 계정 삭제 UI
src/pages/content/MyPage.jsx
회원 탈퇴 모달·플로우 추가: 삭제 호출, 에러/로딩 상태, 성공 시 세션 정리 및 라우트 전환.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant UI as 인터뷰 UI
    participant API as Interview API
    participant Modal as ResumeSessionModal
    participant Snap as SnapshotBuilder

    User->>UI: 페이지 진입
    UI->>API: getLatestIncompleteTechSession()/getLatestIncompleteMockSession()
    API-->>UI: pendingResumeSession 반환
    UI->>Modal: 모달 표시 (pendingSession)

    alt 재개 선택
        User->>Modal: 이어서 하기 클릭
        Modal->>Snap: buildResumedSessionSnapshot(session)
        Snap-->>UI: 세션 스냅샷
        UI->>API: 세션 복구/진행 호출 (네비게이트)
        API-->>UI: 세션 진행 응답
    else 해제 선택
        User->>Modal: 아니요 클릭
        Modal->>API: dismissTechSession()/dismissMockSession(sessionId)
        API-->>UI: 해제 완료
        UI->>UI: 상태 초기화
    end
Loading
sequenceDiagram
    participant User as 사용자
    participant MyPage as MyPage
    participant Modal as DeleteAccountConfirmModal
    participant API as userApi
    participant Session as SessionMgr
    participant Router as 라우터

    User->>MyPage: 회원 탈퇴 버튼 클릭
    MyPage->>Modal: 모달 표시
    User->>Modal: 탈퇴 확인
    Modal->>API: deleteMyAccount()
    API-->>Modal: 삭제 성공
    Modal->>Session: clearAuthenticatedBrowserSession()
    Session-->>Router: /login으로 이동 (로그아웃/세션 정리)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • v0.3patch #15 — 거의 동일한 파일들(index.html, 페이지·컴포넌트·API 등)을 변경한 PR로 높은 코드 중첩 가능성.
  • feat/ai-fundamental #8 — 인터뷰 재개·모달·스냅샷 관련 변경과 직접적으로 중복될 가능성 있음.
  • v0.2 #10 — adminApi 확장(접속 요약·복구)과 관리자 UI 변경이 코드 수준에서 연관됨.

Poem

📝 메타가 빛나고 약관이 자리하네
🔁 멈췄던 세션 모달에서 숨을 고르고
🗑️ 계정은 고요히 정리되며 길을 떠나고
🛠️ 관리자는 접속을 들여다보며 복구하고
✨ 흐름은 다시 연결되네

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목 'v0.3patch'는 버전 번호만 표시하며 실제 변경 사항을 설명하지 않습니다. 개발자 입장에서 이 PR의 주요 변경 내용(약관/개인정보 페이지 추가, 세션 재개 기능, 관리자 API 확장 등)을 파악하기 어렵습니다. PR 제목을 구체적으로 수정하세요. 예: 'Add legal pages, session resume modals, and admin features' 또는 'Implement terms, privacy policy pages and interview session resume flow'
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v0.3patch

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 951b0a9d08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/pages/content/AdminConsolePage.jsx Outdated
setLoadingMembers(false);
}
}, [memberSize]);
}, [memberKeyword, memberSize]);

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 dashboard initialization from rerunning on member search

loadMembers is now recreated whenever memberKeyword changes, and the mount-time initialize effect depends on loadMembers, so every member search triggers a full re-initialization (profile + sets + categories reload and loadingPage spinner) instead of only refreshing the member table. This causes unnecessary network load and visible UI flicker during normal filtering; the initialization path should be decoupled from search-term-driven callback identity.

Useful? React with 👍 / 👎.

@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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/lib/apiClient.js (1)

37-49: ⚠️ Potential issue | 🟠 Major

모든 HTTP 메서드에서 401 재시도 활성화로 인한 멱등성 위험

retryOnUnauthorized 기본값이 true로 설정되어, 명시하지 않은 모든 POST/PUT/PATCH/DELETE 작업이 401 응답 시 자동으로 재시도됩니다.

src/lib/interviewApi.jsdismissTechSession, dismissMockSession 등 POST 요청들이 이를 명시하지 않아 자동 재시도 대상이 됩니다. 이 작업들이 멱등하지 않다면 중복 처리 위험이 있습니다.

비교: src/lib/paymentApi.jsconfirmPointCharge, refundPointCharge는 민감한 작업이므로 명시적으로 retryOnUnauthorized: false를 설정합니다.

비멱등 작업(dismissTechSession, dismissMockSession, 그 외 상태 변경 작업)은 명시적으로 retryOnUnauthorized: false를 설정하거나, 백엔드 엔드포인트가 멱등성을 보장하는지 확인이 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/apiClient.js` around lines 37 - 49, The current default
retryOnUnauthorized behavior in apiClient (variable retryOnUnauthorized used
around executeJsonRequest and refreshAuthSession) enables automatic 401 retry
for all methods and risks double-executing non-idempotent actions; fix by making
retryOnUnauthorized false for unsafe HTTP verbs or explicitly disabling it on
non-idempotent callers: either change the default logic so retryOnUnauthorized
defaults to false for methods other than GET/HEAD/OPTIONS (inspect
options.method before setting retryOnUnauthorized) or update callers like
dismissTechSession and dismissMockSession in interviewApi to pass
retryOnUnauthorized: false when calling the API, ensuring
executeJsonRequest/refreshAuthSession flow will not auto-retry those POST
state-changing requests.
src/components/BrowserSessionGuard.jsx (1)

19-25: ⚠️ Potential issue | 🟡 Minor

localStorage 마커를 서버 검증 없이 신뢰

hasAuthenticatedBrowserSession()은 localStorage의 userId 존재 여부만 확인하고 서버 세션을 검증하지 않습니다. BrowserSessionGuard가 이 마커를 신뢰하여 프로필 검증을 건너뛰므로, localStorage의 userId가 유효하더라도 서버 세션이 만료되었다면 인증되지 않은 상태로 진행됩니다.

또한 라인 38의 catch 블록에서 logout() API를 호출하지 않고 클라이언트 세션만 정리하므로, 프로필 조회 실패 시 서버 세션이 유지될 수 있습니다. 라인 28-30에서 프로필의 userId가 null이면 세션을 표시하지 않으면서도 라인 31에서 ready를 true로 설정하는 것도 모호한 상태를 만듭니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/BrowserSessionGuard.jsx` around lines 19 - 25, The guard
function currently trusts hasAuthenticatedBrowserSession() (which only checks
localStorage) and sets setReady(true) without server validation; change guard
to, when hasAuthenticatedBrowserSession() is true, call the profile/session
validation endpoint (e.g., fetchProfile or equivalent) to confirm the server
session before calling setReady, and if validation fails call logout() to clear
both server and client sessions; also update the catch block to invoke logout()
on profile fetch errors to avoid leaving a server session active; finally, if
the fetched profile has userId === null, clear the client marker and do not call
setReady(true) until a valid server-validated profile is returned.
src/pages/content/AdminConsolePage.jsx (1)

264-297: ⚠️ Potential issue | 🟠 Major

loadGlobalAccessSummary를 deps에서 제거하세요.

Line 297의 useEffect에서 loadGlobalAccessSummary를 의존성으로 포함했지만 effect 본문에서는 사용하지 않습니다. loadGlobalAccessSummarymemberSummaryWindowDays에 묶여 있어서 요약 기간을 7일/30일로 전환하면 이 callback이 재생성되고, 결과적으로 전체 페이지 초기화가 불필요하게 다시 실행됩니다.

제안 수정
-  }, [loadCategories, loadGlobalAccessSummary, loadMembers, loadSets, navigate]);
+  }, [loadCategories, loadMembers, loadSets, navigate]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/content/AdminConsolePage.jsx` around lines 264 - 297, The useEffect
that initializes the admin page includes loadGlobalAccessSummary in its
dependency array but never uses it in the effect body; remove
loadGlobalAccessSummary from the dependency list of that useEffect to prevent
unnecessary re-runs when loadGlobalAccessSummary (recreated by
memberSummaryWindowDays) changes, keeping only the actually used callbacks
(loadCategories, loadMembers, loadSets, navigate) and any other true
dependencies referenced inside initialize (e.g., setLoadingPage,
setPageErrorMessage if not stable via hooks) to satisfy linter expectations.
🧹 Nitpick comments (7)
index.html (2)

12-12: 브랜드명 대소문자 불일치.

og:title에서는 "VlaInter"를 사용하지만, <title> 태그에서는 "vlainter"(소문자)를 사용하고 있습니다. 일관된 브랜딩을 위해 통일하는 것이 좋습니다.

🔧 제안된 수정
-    <title>vlainter</title>
+    <title>VlaInter</title>

Also applies to: 23-23

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.html` at line 12, The og:title meta ("og:title") and the page <title>
element use different casing ("VlaInter" vs "vlainter"); update the <title>
element and any other occurrences (including the other instance mentioned) to
exactly match the brand casing used in the og:title (or vice versa), ensuring
both the <title> tag and the meta property="og:title" values are identical (use
the same string "VlaInter" across the document).

15-18: OG 이미지 크기가 권장 사양보다 작습니다.

Open Graph 이미지는 Facebook, LinkedIn 등에서 최적의 표시를 위해 최소 1200x630 픽셀이 권장됩니다. 현재 484x452 크기의 favicon.png는 소셜 공유 시 작게 표시되거나 잘릴 수 있습니다.

전용 소셜 공유 이미지 사용을 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.html` around lines 15 - 18, The Open Graph image currently uses a small
favicon (meta property="og:image" with og:image:width and og:image:height set to
484x452) which is below the recommended 1200x630; replace the favicon reference
with a dedicated social-preview image at least 1200x630, update the meta
properties (og:image, og:image:width, og:image:height, og:image:alt) to point to
the new asset and correct dimensions, and ensure the alt text (og:image:alt)
provides a concise descriptive label for the new social image.
src/pages/TermsPage.jsx (1)

114-116: key로 전체 텍스트 대신 인덱스 사용 권장

ServiceIntroPage와 동일하게, line 전체 문자열을 key로 사용하면 동일 내용 문단에서 key 충돌이 발생할 수 있습니다.

♻️ 제안된 수정
-      {body.map((line) => (
-        <p key={line}>{line}</p>
+      {body.map((line, index) => (
+        <p key={index}>{line}</p>
       ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/TermsPage.jsx` around lines 114 - 116, In TermsPage.jsx update the
body.map usage to avoid using the entire line string as the React key; change
the map callback (body.map((line) => ...)) to accept the index (body.map((line,
idx) => ...)) and use that index (or a stable composite key like
`${idx}-${line.slice(0,20)}`) for the <p> key prop so identical paragraph texts
won't collide.
src/pages/PrivacyPolicyPage.jsx (1)

111-116: Section 컴포넌트 중복

Section 컴포넌트가 TermsPage.jsx와 동일한 구조로 정의되어 있습니다. 공통 컴포넌트로 추출하면 유지보수성이 향상됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/PrivacyPolicyPage.jsx` around lines 111 - 116, The Section
component is duplicated; extract it to a shared component (e.g., export a
Section functional component) and replace the local definitions in
PrivacyPolicyPage.jsx and TermsPage.jsx with imports. Concretely: create a
single Section component file (export default Section) that renders the same
structure and classNames, update PrivacyPolicyPage.jsx and TermsPage.jsx to
import and use that Section symbol instead of defining it inline, and remove the
duplicated Section declarations from both pages.
src/pages/ServiceIntroPage.jsx (1)

42-44: key로 전체 텍스트 대신 인덱스 사용 권장

line 전체 문자열을 key로 사용하면 동일한 내용의 문단이 있을 경우 key 충돌이 발생합니다.

♻️ 제안된 수정
-      {body.map((line) => (
-        <p key={line}>{line}</p>
+      {body.map((line, index) => (
+        <p key={index}>{line}</p>
       ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/ServiceIntroPage.jsx` around lines 42 - 44, The current
body.map((line) => (<p key={line}>{line}</p>)) uses the full line text as the
React key which can collide for identical paragraphs; update the mapping in
ServiceIntroPage.jsx to use the index (or a stable unique id if available) as
the key, e.g. map((line, idx) => <p key={idx}>{line}</p>), or better yet use a
unique identifier when one exists instead of the line string to avoid key
collisions.
src/components/ResumeSessionModal.jsx (2)

14-15: 백드롭 클릭 핸들러 누락

<div className="absolute inset-0" />가 백드롭 역할을 하는 것 같지만 onClick 핸들러가 없습니다. 사용자가 모달 외부를 클릭해도 닫히지 않습니다.

♻️ 백드롭 클릭 시 모달 닫기 추가
-      <div className="absolute inset-0" />
+      <div className="absolute inset-0" onClick={onDismiss} aria-hidden="true" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ResumeSessionModal.jsx` around lines 14 - 15, The backdrop div
in ResumeSessionModal.jsx (the element rendered as <div className="absolute
inset-0" />) lacks an onClick handler so clicking outside the modal doesn't
close it; add an onClick that calls the component's close handler (use the
existing prop or state handler, e.g., onClose or closeModal) and also add
e.stopPropagation() on the modal content container (the centered wrapper with
class "fixed inset-0 z-[95] flex items-center justify-center bg-black/45 px-4"
or its immediate child) to prevent clicks inside the modal from bubbling to the
backdrop.

1-50: 포커스 트랩 고려

모달이 열렸을 때 키보드 사용자가 Tab으로 모달 외부로 이동할 수 있습니다. 접근성 향상을 위해 포커스 트랩 구현을 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ResumeSessionModal.jsx` around lines 1 - 50, When
ResumeSessionModal opens (open prop true) keyboard users can Tab out of the
dialog; add a focus trap: give the dialog container (the element with
role="dialog") a ref inside ResumeSessionModal, on open save
document.activeElement, move focus to the first focusable element (e.g., the
dismiss or continue buttons), and register a keydown handler that intercepts
Tab/Shift+Tab to cycle focus among tabbable elements in the dialog (use a
tabbable selector like 'a, button, input, textarea, select,
[tabindex]:not([tabindex="-1"])'), and on close restore the previously focused
element; implement cleanup in useEffect and ensure disabled/busy state is
respected so focus skips disabled controls and onDismiss/onContinue remain
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/BrowserSessionGuard.jsx`:
- Around line 27-36: The try block should validate that extractProfile(payload)
yields a profile.userId before marking the session or flipping ready; change the
logic so you only call markAuthenticatedBrowserSession(profile.userId) and
setReady(true) when profile?.userId != null, and when userId is missing do not
setReady(true) (leave ready false or explicitly clear the authenticated state);
adjust the code around getMyProfile, extractProfile,
markAuthenticatedBrowserSession, setReady and cancelled so children are not
rendered when no userId is present (optionally call your session-clear function
if one exists).
- Around line 37-39: The catch block in BrowserSessionGuard.jsx only calls
clearAuthenticatedBrowserSession(), leaving the server session intact; update
the catch to call the logout() function from src/lib/authApi.js (awaiting it if
the surrounding function is async), handle any errors from logout gracefully
(e.g., try/catch or .catch()) and then call clearAuthenticatedBrowserSession()
so both server and client sessions are cleared; reference the existing
clearAuthenticatedBrowserSession() and the logout() export when making this
change.

In `@src/lib/interviewApi.js`:
- Around line 129-132: The dismissTechSession POST should not be auto-retried on
401: update dismissTechSession to call apiRequest with an options object that
includes retryOnUnauthorized: false (e.g., apiRequest(..., { method: "POST",
retryOnUnauthorized: false })). Do the same change for the other dismiss POST in
this file (the other dismiss* function that sends a session-dismiss POST) so
non-idempotent session-dismiss endpoints are excluded from automatic 401
retries.

In `@src/pages/content/AdminConsolePage.jsx`:
- Around line 209-224: The render crashes because
memberGlobalSummary.dailyMetrics can be undefined; normalize the API payload
before storing it (in loadGlobalAccessSummary) or consistently guard every .map
call. Modify loadGlobalAccessSummary to ensure the payload passed to
setMemberGlobalSummary always includes dailyMetrics: default to an empty array
(e.g., ensure payload.dailyMetrics = payload.dailyMetrics || []) before calling
setMemberGlobalSummary, and/or update all usages that call
memberGlobalSummary.dailyMetrics.map(...) to use
(memberGlobalSummary.dailyMetrics || []).map(...); reference functions/vars:
loadGlobalAccessSummary, setMemberGlobalSummary, memberGlobalSummary, and
dailyMetrics.
- Around line 1140-1227: The issue: selectedMemberDetail.accessSummary,
accessSummary.dailyLoginCounts and selectedMemberDetail.recentAccessLogs are
accessed directly causing runtime errors when the API returns partial data; fix
by normalizing these fields before rendering (provide defaults) and use the
existing formatDecimal utility for number formatting. Specifically, ensure
selectedMemberDetail.accessSummary is a safe object (with numeric defaults for
recentLoginCount, activeSessionCount, totalActionCount, averageActionCount,
averageSessionMinutes, interviewCompletionRate, completedInterviewCount,
totalInterviewCount, lastLoginAt, lastLoginIpAddress, calculatedAt and an array
default for dailyLoginCounts) and that selectedMemberDetail.recentAccessLogs
defaults to an empty array; replace direct .toFixed calls with
formatDecimal(selectedMemberDetail.accessSummary.averageActionCount, 1) and
guard .map/.length usage (or use safe empty arrays) when rendering
dailyLoginCounts and recentAccessLogs so the UI never calls
.map/.toFixed/.length on undefined.

In `@src/pages/content/MyPage.jsx`:
- Around line 379-381: The delete flow can still be invoked multiple times
because setDeleteAccountSubmitting(true) is async; add a synchronous guard
(e.g., a module/local ref like isDeletingRef) that you check at the start of the
handler and set to true immediately before awaiting anything, then call
setDeleteAccountSubmitting(true) and run deleteMyAccount(), finally reset both
the ref and setDeleteAccountSubmitting(false) in finally; update the handler
that references deleteMyAccount, setDeleteAccountSubmitting,
deleteAccountErrorMessage and setShowDeleteAccountModal, and apply the same
ref-guard pattern to the other delete flow instance mentioned (lines ~659-672).
- Around line 663-667: After successfully awaiting deleteMyAccount(), call the
existing logout() flow (which already performs
clearAuthenticatedBrowserSession() and resetMyProfileCache()) to ensure
server-side session/cookie invalidation is invoked; replace or remove the direct
calls to clearAuthenticatedBrowserSession() and resetMyProfileCache() so the
code uses await logout() prior to setShowDeleteAccountModal(false) and
navigate("/login", { replace: true }) to avoid leaving server sessions active.

---

Outside diff comments:
In `@src/components/BrowserSessionGuard.jsx`:
- Around line 19-25: The guard function currently trusts
hasAuthenticatedBrowserSession() (which only checks localStorage) and sets
setReady(true) without server validation; change guard to, when
hasAuthenticatedBrowserSession() is true, call the profile/session validation
endpoint (e.g., fetchProfile or equivalent) to confirm the server session before
calling setReady, and if validation fails call logout() to clear both server and
client sessions; also update the catch block to invoke logout() on profile fetch
errors to avoid leaving a server session active; finally, if the fetched profile
has userId === null, clear the client marker and do not call setReady(true)
until a valid server-validated profile is returned.

In `@src/lib/apiClient.js`:
- Around line 37-49: The current default retryOnUnauthorized behavior in
apiClient (variable retryOnUnauthorized used around executeJsonRequest and
refreshAuthSession) enables automatic 401 retry for all methods and risks
double-executing non-idempotent actions; fix by making retryOnUnauthorized false
for unsafe HTTP verbs or explicitly disabling it on non-idempotent callers:
either change the default logic so retryOnUnauthorized defaults to false for
methods other than GET/HEAD/OPTIONS (inspect options.method before setting
retryOnUnauthorized) or update callers like dismissTechSession and
dismissMockSession in interviewApi to pass retryOnUnauthorized: false when
calling the API, ensuring executeJsonRequest/refreshAuthSession flow will not
auto-retry those POST state-changing requests.

In `@src/pages/content/AdminConsolePage.jsx`:
- Around line 264-297: The useEffect that initializes the admin page includes
loadGlobalAccessSummary in its dependency array but never uses it in the effect
body; remove loadGlobalAccessSummary from the dependency list of that useEffect
to prevent unnecessary re-runs when loadGlobalAccessSummary (recreated by
memberSummaryWindowDays) changes, keeping only the actually used callbacks
(loadCategories, loadMembers, loadSets, navigate) and any other true
dependencies referenced inside initialize (e.g., setLoadingPage,
setPageErrorMessage if not stable via hooks) to satisfy linter expectations.

---

Nitpick comments:
In `@index.html`:
- Line 12: The og:title meta ("og:title") and the page <title> element use
different casing ("VlaInter" vs "vlainter"); update the <title> element and any
other occurrences (including the other instance mentioned) to exactly match the
brand casing used in the og:title (or vice versa), ensuring both the <title> tag
and the meta property="og:title" values are identical (use the same string
"VlaInter" across the document).
- Around line 15-18: The Open Graph image currently uses a small favicon (meta
property="og:image" with og:image:width and og:image:height set to 484x452)
which is below the recommended 1200x630; replace the favicon reference with a
dedicated social-preview image at least 1200x630, update the meta properties
(og:image, og:image:width, og:image:height, og:image:alt) to point to the new
asset and correct dimensions, and ensure the alt text (og:image:alt) provides a
concise descriptive label for the new social image.

In `@src/components/ResumeSessionModal.jsx`:
- Around line 14-15: The backdrop div in ResumeSessionModal.jsx (the element
rendered as <div className="absolute inset-0" />) lacks an onClick handler so
clicking outside the modal doesn't close it; add an onClick that calls the
component's close handler (use the existing prop or state handler, e.g., onClose
or closeModal) and also add e.stopPropagation() on the modal content container
(the centered wrapper with class "fixed inset-0 z-[95] flex items-center
justify-center bg-black/45 px-4" or its immediate child) to prevent clicks
inside the modal from bubbling to the backdrop.
- Around line 1-50: When ResumeSessionModal opens (open prop true) keyboard
users can Tab out of the dialog; add a focus trap: give the dialog container
(the element with role="dialog") a ref inside ResumeSessionModal, on open save
document.activeElement, move focus to the first focusable element (e.g., the
dismiss or continue buttons), and register a keydown handler that intercepts
Tab/Shift+Tab to cycle focus among tabbable elements in the dialog (use a
tabbable selector like 'a, button, input, textarea, select,
[tabindex]:not([tabindex="-1"])'), and on close restore the previously focused
element; implement cleanup in useEffect and ensure disabled/busy state is
respected so focus skips disabled controls and onDismiss/onContinue remain
unchanged.

In `@src/pages/PrivacyPolicyPage.jsx`:
- Around line 111-116: The Section component is duplicated; extract it to a
shared component (e.g., export a Section functional component) and replace the
local definitions in PrivacyPolicyPage.jsx and TermsPage.jsx with imports.
Concretely: create a single Section component file (export default Section) that
renders the same structure and classNames, update PrivacyPolicyPage.jsx and
TermsPage.jsx to import and use that Section symbol instead of defining it
inline, and remove the duplicated Section declarations from both pages.

In `@src/pages/ServiceIntroPage.jsx`:
- Around line 42-44: The current body.map((line) => (<p key={line}>{line}</p>))
uses the full line text as the React key which can collide for identical
paragraphs; update the mapping in ServiceIntroPage.jsx to use the index (or a
stable unique id if available) as the key, e.g. map((line, idx) => <p
key={idx}>{line}</p>), or better yet use a unique identifier when one exists
instead of the line string to avoid key collisions.

In `@src/pages/TermsPage.jsx`:
- Around line 114-116: In TermsPage.jsx update the body.map usage to avoid using
the entire line string as the React key; change the map callback
(body.map((line) => ...)) to accept the index (body.map((line, idx) => ...)) and
use that index (or a stable composite key like `${idx}-${line.slice(0,20)}`) for
the <p> key prop so identical paragraph texts won't collide.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 50695418-2dd4-4839-bbc1-cad5ec3e2898

📥 Commits

Reviewing files that changed from the base of the PR and between 10bec67 and 951b0a9.

📒 Files selected for processing (24)
  • index.html
  • src/App.jsx
  • src/components/AuthFooter.jsx
  • src/components/BrowserSessionGuard.jsx
  • src/components/LegalDocumentLayout.jsx
  • src/components/ResumeSessionModal.jsx
  • src/components/TopNav.jsx
  • src/lib/adminApi.js
  • src/lib/apiClient.js
  • src/lib/interviewApi.js
  • src/lib/resumeInterviewSession.js
  • src/lib/userApi.js
  • src/pages/PrivacyPolicyPage.jsx
  • src/pages/ServiceIntroPage.jsx
  • src/pages/StartingPage.jsx
  • src/pages/TermsPage.jsx
  • src/pages/auth/ForgotPassword.jsx
  • src/pages/auth/Join.jsx
  • src/pages/auth/Login.jsx
  • src/pages/content/AdminConsolePage.jsx
  • src/pages/content/InterviewStartPage.jsx
  • src/pages/content/MyPage.jsx
  • src/pages/content/QuestionSetsPage.jsx
  • src/pages/content/TechPracticePage.jsx

Comment thread src/components/BrowserSessionGuard.jsx Outdated
Comment thread src/components/BrowserSessionGuard.jsx
Comment thread src/lib/interviewApi.js
Comment thread src/pages/content/AdminConsolePage.jsx
Comment thread src/pages/content/AdminConsolePage.jsx
Comment thread src/pages/content/MyPage.jsx
Comment thread src/pages/content/MyPage.jsx Outdated
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 풀 리퀘스트는 사용자 경험을 향상시키고 관리 기능을 확장하는 중요한 업데이트를 포함합니다. 이용약관, 개인정보처리방침, 서비스 소개 페이지를 추가하여 서비스의 투명성을 높였으며, 중단된 면접 세션을 재개할 수 있는 기능을 도입하여 사용자의 편의성을 크게 개선했습니다. 또한, 회원 탈퇴 기능을 제공하고 회원가입 시 약관 동의를 필수화하여 사용자 관리 및 규정 준수를 강화했습니다. 관리자 콘솔에는 회원 접속 통계와 검색 기능이 추가되어 운영 효율성이 증대되었습니다. 전반적으로 서비스의 안정성과 사용성을 높이는 데 중점을 두었습니다.

Highlights

  • 새로운 기능: 이용약관, 개인정보처리방침, 서비스 소개 페이지가 추가되었습니다.
  • 새로운 기능: 중단된 면접 세션을 이어서 진행할 수 있는 기능이 추가되었습니다.
  • 새로운 기능: 회원 탈퇴 기능이 추가되었습니다.
  • 새로운 기능: 회원가입 시 이용약관 및 개인정보처리방침 동의가 필수화되었습니다.
  • 새로운 기능: 관리자 콘솔에 회원 접속 통계 및 키워드 검색 기능이 추가되었습니다.
  • 개선: 세션 인증 흐름이 개선되었습니다.
  • 개선: 공통 푸터 컴포넌트 도입으로 UI 일관성이 강화되었습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • index.html
    • Open Graph 및 Twitter 메타 태그가 추가되었습니다.
  • src/App.jsx
    • 이용약관, 개인정보처리방침, 서비스 소개 페이지를 위한 새로운 라우트가 추가되었고 관련 컴포넌트가 임포트되었습니다.
  • src/components/AuthFooter.jsx
    • 인증 관련 페이지에서 사용될 공통 푸터 컴포넌트가 새로 추가되었습니다.
  • src/components/BrowserSessionGuard.jsx
    • 세션 인증 로직이 getMyProfile 및 markAuthenticatedBrowserSession을 사용하도록 변경되었으며, logout 대신 clearAuthenticatedBrowserSession이 도입되었습니다.
  • src/components/LegalDocumentLayout.jsx
    • 법률 문서를 표시하기 위한 새로운 레이아웃 컴포넌트가 추가되었습니다.
  • src/components/ResumeSessionModal.jsx
    • 미완료된 면접 세션을 재개할지 묻는 새로운 모달 컴포넌트가 추가되었습니다.
  • src/components/TopNav.jsx
    • 상단 내비게이션 링크가 Link 컴포넌트를 사용하도록 업데이트되었고, 새로운 /about 및 /join 경로를 가리키도록 변경되었습니다.
  • src/lib/adminApi.js
    • getAdminMembers에 keyword 파라미터가 추가되었고, getAdminGlobalAccessSummary 및 restoreAdminMember API 호출이 새로 추가되었습니다.
    • getAdminMemberDetail은 refreshAccess를 지원하도록 수정되었습니다.
  • src/lib/apiClient.js
    • 401 응답에 대한 apiRequest 재시도 로직이 간소화되었으며, 메서드별 검사가 제거되었습니다.
  • src/lib/interviewApi.js
    • 세션 재개/종료를 지원하기 위해 getLatestIncompleteTechSession, dismissTechSession, getLatestIncompleteMockSession, dismissMockSession API 호출이 추가되었습니다.
  • src/lib/resumeInterviewSession.js
    • 세션 재개를 위한 스냅샷을 구성하는 유틸리티 파일이 새로 추가되었습니다.
  • src/lib/userApi.js
    • deleteMyAccount 및 resetMyProfileCache API 호출이 추가되었습니다.
  • src/pages/PrivacyPolicyPage.jsx
    • 개인정보처리방침을 표시하는 새로운 페이지 컴포넌트가 추가되었습니다.
  • src/pages/ServiceIntroPage.jsx
    • 서비스 소개를 표시하는 새로운 페이지 컴포넌트가 추가되었습니다.
  • src/pages/StartingPage.jsx
    • AuthFooter 컴포넌트가 추가되었습니다.
  • src/pages/TermsPage.jsx
    • 이용약관을 표시하는 새로운 페이지 컴포넌트가 추가되었습니다.
  • src/pages/auth/ForgotPassword.jsx
    • 인라인 푸터가 AuthFooter 컴포넌트로 대체되었습니다.
  • src/pages/auth/Join.jsx
    • 인라인 푸터가 AuthFooter 컴포넌트로 대체되었고, 이용약관 동의 체크박스가 추가되었으며, 새로운 법률 페이지로 연결되는 링크가 포함되었습니다.
  • src/pages/auth/Login.jsx
    • 인라인 푸터가 AuthFooter 컴포넌트로 대체되었습니다.
  • src/pages/content/AdminConsolePage.jsx
    • 회원 검색 기능, 전체 접속 요약 통계, 회원 복구 기능이 추가되었습니다.
    • 회원 목록 및 상세 보기 화면이 업데이트되었습니다.
  • src/pages/content/InterviewStartPage.jsx
    • ResumeSessionModal이 통합되었고, 문서 요구사항이 이력서 필수로 변경되었으며, 미완료된 모의 면접 세션을 확인하고 처리하는 로직이 추가되었습니다.
  • src/pages/content/MyPage.jsx
    • 회원 탈퇴 기능과 DeleteAccountConfirmModal이 추가되었습니다.
  • src/pages/content/QuestionSetsPage.jsx
    • ResumeSessionModal이 통합되었고, 미완료된 질문 세트 연습 세션을 확인하고 처리하는 로직이 추가되었습니다.
  • src/pages/content/TechPracticePage.jsx
    • ResumeSessionModal이 통합되었고, 미완료된 기술 질문 연습 세션을 확인하고 처리하는 로직이 추가되었습니다.
Activity
  • 이 풀 리퀘스트에는 아직 인간 활동(댓글, 리뷰 등)이 없습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

v0.3패치에 대한 리뷰입니다. 이용약관, 개인정보처리방침, 서비스 소개 페이지 추가, 인터뷰 세션 재개, 회원 탈퇴 기능 등 많은 기능이 추가되었네요. 관리자 콘솔 기능 강화와 공통 푸터 컴포넌트 리팩터링 등 개선 사항도 잘 반영되었습니다. 전반적으로 코드 변경 사항은 훌륭하지만, API 요청 재시도 로직에 잠재적으로 위험한 변경점이 있어 리뷰 의견을 남겼습니다. 해당 부분은 꼭 확인하고 수정하시는 것을 권장합니다.

Comment thread src/lib/apiClient.js Outdated
canRetryUnauthorized &&
path !== "/api/auth/refresh"
) {
if (response.status === 401 && retryOnUnauthorized && path !== "/api/auth/refresh") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

401 응답 시 모든 HTTP 메서드에 대해 재시도를 하도록 변경되었는데, 이는 위험할 수 있습니다. POST, PATCH, DELETE와 같이 멱등성(idempotent)이 보장되지 않는 요청을 재시도할 경우, 리소스가 중복 생성되거나 원치 않는 부작용이 발생할 수 있습니다.

예를 들어, 사용자가 회원 탈퇴(DELETE /api/users/me)를 요청했는데 네트워크 문제로 응답을 받지 못하고 세션이 만료된 경우, 재로그인 후 탈퇴 요청이 재시도되어 이미 삭제된 계정을 다시 삭제하려는 시도가 발생할 수 있습니다.

안전한 GET, HEAD, OPTIONS 메서드에 대해서만 재시도를 허용했던 이전 로직으로 되돌리는 것을 권장합니다. 아래 제안된 if문과 함께, if문 앞에 다음 변수들을 다시 추가해주세요:

const method = String(options.method || "GET").toUpperCase();
const safeRetryMethods = new Set(["GET", "HEAD", "OPTIONS"]);
const canRetryUnauthorized = retryOnUnauthorizedOption === true || safeRetryMethods.has(method);
  if (
    response.status === 401 &&
    retryOnUnauthorized &&
    canRetryUnauthorized &&
    path !== "/api/auth/refresh"
  ) {

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e177a3abc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/pages/content/AdminConsolePage.jsx Outdated
await restoreAdminMember(selectedMemberId);
const refreshed = await getAdminMemberDetail(selectedMemberId);
setSelectedMemberDetail(normalizeMemberDetail(refreshed));
await loadMembers(memberPage);

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 Preserve member filter when refreshing after restore

After restoring a soft-deleted member, the list reload uses loadMembers(memberPage) without the active memberKeyword, so any filtered view is dropped and the admin is unexpectedly moved back to the unfiltered dataset. This is reproducible whenever restore is triggered from a keyword search result; pass the current keyword to keep pagination/selection consistent with the user’s current context.

Useful? React with 👍 / 👎.

Comment thread src/pages/content/MyPage.jsx Outdated
setDeleteAccountErrorMessage("");
try {
await deleteMyAccount();
await logout();

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 Continue logout flow even when logout API fails

Account deletion and logout are chained in one try, so if deleteMyAccount() succeeds but logout() fails (e.g., transient network/server error), the catch path reports full deletion failure and leaves the user on My Page despite the account already being removed. This creates a broken post-deletion UX; after successful deletion, the client should still close the modal and redirect to /login even if logout throws.

Useful? React with 👍 / 👎.

@rktclgh rktclgh merged commit 847105b into develop Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant