Skip to content

refactor: public 폴더 refactor#7

Merged
goodaseul merged 18 commits into
mainfrom
dev
Mar 27, 2026
Merged

refactor: public 폴더 refactor#7
goodaseul merged 18 commits into
mainfrom
dev

Conversation

@goodaseul

@goodaseul goodaseul commented Mar 24, 2026

Copy link
Copy Markdown
Owner

[style]

  • 랜딩페이지 UI 변경
  • 포인트 색상 변경
  • 글꼴 반응형에 맞게 수정
  • 다크모드에 맞게 home 수정
  • 다크모드 bg 색상 지우기

[refactor]

  • AuthProvider 중복 제거
  • 차트 중복 로직 분리 및 타입 중복 이슈 수정
  • chartPeriod 를 constants 폴더로 이동

[feature]

  • public 폴더 다크모드 적용
  • 회원가입 후 대시보드 시 유저 싱크
  • header 구조 변경 및 layout 변경]

[chore]

  • 로그인/회원가입 import 순서 변경
  • 사용하지 않는 import 제거
  • 대시보드 최근등록 단어 개수 수정
  • readme 수정

@goodaseul goodaseul self-assigned this Mar 24, 2026
@vercel

vercel Bot commented Mar 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-plus Ready Ready Preview, Comment Mar 27, 2026 1:25pm

@coderabbitai

coderabbitai Bot commented Mar 24, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds theme support with next-themes (ThemeProvider + ThemeToggle), centralizes Supabase session handling via onAuthStateChange/getMyProfile, introduces chart period constants and defaultChart utility, and applies widespread dark-mode, UI, and layout refinements across public and protected pages.

Changes

Cohort / File(s) Summary
Theme infra & provider
package.json, src/app/globals.css, src/app/layout.tsx, src/providers/ThemeProvider.tsx, src/components/button/ThemeButton.tsx
Added next-themes; new ThemeProvider wrapper and ThemeToggle component; migrated CSS tokens to --background/--foreground and added base-layer body rules. Review hydration/SSR considerations.
Auth & session flow
src/api/auth.ts, src/api/profile.ts, src/providers/AuthProvider.tsx, src/store/useUserStore.ts
signUp returns full auth response; getMyProfile() derives user from supabase.auth.getUser() and uses .maybeSingle(); AuthProvider now relies solely on onAuthStateChange with handleSession to fetch/clear profile and set initialization; user store adds setInitialized. Pay attention to initialization timing and error logging.
Words API & queries
src/api/types/words.ts, src/api/words.ts, src/hooks/queries/words/querykey.ts, src/hooks/queries/words/useWordsQuery.ts
Introduced WordsQueryRequest; getWords signature changed to accept that type; query key factory now requires page; hooks updated (default wordIdnull) and call sites adjusted. Verify query key usage and pagination.
Charting & data util
src/constants/chartPeriod.ts, src/utils/defaultChart.ts, src/app/(protected)/dashboard/_components/record/...
Added Period type, PERIOD_LABELS/PERIOD_OPTIONS, and defaultChart with DATE_SLICE; chart components refactored to use shared period typing and centralized aggregation. Check chart data correctness and label mapping.
Header & nav refactor
src/app/(protected)/_components/layout/Header.tsx, src/app/(protected)/_components/layout/Header/Logo.tsx, src/app/(protected)/_components/layout/Header/MyMenu.tsx, .../MyTooltip.tsx, .../NavBtn.tsx, .../MenuLink.tsx
Extracted Logo, MyMenu, MyTooltip, NavBtn; removed inline user/menu logic from Header and adjusted classes and click-outside behavior. Review accessibility and focus handling for menus.
Landing / public pages
src/app/(public)/landing-page/...
Removed Info component; simplified SectionWrapper props/layout; converted Banner to server-compatible (removed client hooks/CTA logic); adjusted Feature layout and added CTA. Confirm SSR/CSR component boundaries.
Auth pages & Title
src/app/(public)/join/page.tsx, src/app/(public)/login/page.tsx, src/app/(public)/_components/title/Title.tsx
Updated sign-up/login flows to use returned auth data and conditional redirects; localized copy and styling changes; Title.desc now accepts React.ReactNode. Check navigation flows after auth calls.
Record/list & pagination
src/app/(protected)/_components/list/*, src/app/(protected)/record/_components/*
ListContent/List updated with isRecordPage prop; top controls extracted to RecordTopContent; pagination uses useMobileSize (removed resize listeners). Verify conditional rendering and query param handling.
Dashboard & client gating
src/app/(protected)/dashboard/page.tsx, src/app/(protected)/dashboard/_components/*
Dashboard marked client and waits for isInitialized before render; charts use shared Period types; banner/title variants added. Ensure client gating doesn't block expected SSR behavior.
UI styling & small components
src/components/*, src/app/.../modal/*, src/app/.../banner/*
Widespread Tailwind class updates for dark-mode and visual tweaks (colors, rounding, spacing, buttons). Watch for visual regressions and omitted class tokens.
Types & utilities
src/types/word.ts, src/api/types/errors.ts, src/hooks/useMobileSize.ts, src/utils/defaultChart.ts
Refactored WordFormProps to reuse BaseFormProps; minor formatting tweaks; added defaultChart and DATE_SLICE. Review type changes and ensure callers align.
Miscellaneous small edits
src/app/(public)/_components/PublicHeader.tsx, src/app/(public)/join/page.tsx, src/app/(public)/login/page.tsx, src/components/layout/Footer.tsx, others
Copy/text color updates, removed/added imports, small prop/type adjustments across multiple components. Check for unused imports or subtle behavior changes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant RootLayout as RootLayout
    participant ThemeProv as ThemeProvider
    participant Local as LocalStorage
    participant ThemeUI as ThemeToggle

    User->>RootLayout: Request page
    RootLayout->>ThemeProv: Mount ThemeProvider (attribute="class")
    ThemeProv->>Local: Read persisted theme (if present)
    ThemeProv->>ThemeProv: Resolve system/default theme
    ThemeProv->>ThemeUI: Provide theme context (useTheme)
    User->>ThemeUI: Click toggle
    ThemeUI->>ThemeProv: setTheme("dark"/"light")
    ThemeProv->>Local: Persist chosen theme
    ThemeProv->>RootLayout: Update class on <html>/<body>
    ThemeProv->>ThemeUI: Broadcast theme change
    ThemeUI->>User: UI re-renders with updated theme
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰✨ I nibbled CSS and wrapped the tree,
Dark and light now hop with me.
Sessions settle, charts align,
Toggles flip — a subtle sign.
StackPlus gleams — a tiny rhyme.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
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.
Title check ❓ Inconclusive The PR title 'refactor: public 폴더 refactor' partially describes the changeset but is redundant and overly broad. While public folder refactoring is indeed a component, the change encompasses much more: dark mode implementation, API signature updates, theme system integration, landing page UI changes, and provider/store restructuring. Revise the title to highlight the primary change. Consider: 'feat: implement dark mode and theme system' or 'refactor: modernize public pages with dark mode and theme support' to better convey the scope and intent.
✅ 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
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

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

Caution

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

⚠️ Outside diff range comments (2)
package.json (1)

24-25: ⚠️ Potential issue | 🟡 Minor

Remove unused sooner dependency from package.json.

The sooner package is not used anywhere in the codebase and appears to be a typo of sonner. Remove it from package.json to reduce bundle size and avoid confusion.

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

In `@package.json` around lines 24 - 25, Remove the unused "sooner" dependency
entry from package.json (the line with "sooner": "^1.1.4") since it's not
referenced in the codebase and appears to be a typo of "sonner"; update
package.json by deleting that dependency entry and then run dependency install
(e.g., npm install or yarn) to refresh lockfile and node_modules so the change
is reflected.
src/app/(public)/login/page.tsx (1)

38-43: ⚠️ Potential issue | 🟠 Major

Double toast error on login failure.

Same issue as in the join page: when error instanceof Error is true, both toasts will fire. Line 42 executes unconditionally after the if block.

🐛 Proposed fix
     } catch (error) {
       if (error instanceof Error) {
         toast.error(error.message);
+      } else {
+        toast.error("로그인 중 오류가 발생했습니다.");
       }
-      toast.error("로그인 중 오류가 발생했습니다.");
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/login/page.tsx around lines 38 - 43, In the catch block in
src/app/(public)/login/page.tsx where two toast.error calls exist, ensure only
one toast runs by returning or using an else: when (error instanceof Error) call
toast.error(error.message) and then return (or use else) so the generic
toast.error("로그인 중 오류가 발생했습니다.") does not also execute; update the catch block
around the login submit/handler to either add a return after the specific toast
or wrap the generic toast in an else branch.
🧹 Nitpick comments (7)
src/components/input/Input.tsx (1)

50-50: Remove extra space in className string.

There's an extra space between text-sm and transition in the className string, which should be removed for consistent formatting.

✨ Proposed fix
-              "w-full rounded-full px-5 py-3.5 text-sm  transition focus:outline-none",
+              "w-full rounded-full px-5 py-3.5 text-sm transition focus:outline-none",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/input/Input.tsx` at line 50, In the Input component
(Input.tsx) fix the className string that currently has two spaces between
"text-sm" and "transition" by removing the extra space so the value reads "...
text-sm transition focus:outline-none"; update the className assignment in the
Input component/JSX where that string is used.
src/providers/ThemeProvider.tsx (1)

1-1: Remove redundant file path comment.

The comment // providers/ThemeProvider.tsx duplicates information already available from the file path itself and adds no value.

♻️ Suggested fix
-// providers/ThemeProvider.tsx
 "use client";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/providers/ThemeProvider.tsx` at line 1, Remove the redundant leading file
path comment "// providers/ThemeProvider.tsx" from the top of the ThemeProvider
module; simply delete that comment line so the file begins with the actual code
(e.g., the ThemeProvider component or its imports) and leave all existing
symbols like ThemeProvider unchanged.
src/app/(protected)/_components/list/list-actions/ListActionsView.tsx (1)

91-92: Dropdown has hardcoded light-mode colors.

The dropdown uses bg-white and border-gray-200, which won't adapt to dark mode. Consider using theme-aware tokens like bg-background and border-border to ensure proper styling in both themes.

♻️ Suggested fix
           <ul
-            className="absolute z-10 right-0 -bottom-23 w-max rounded-xl
-         bg-white px-5 py-3 border border-gray-200 shadow-sm"
+            className="absolute z-10 right-0 -bottom-23 w-max rounded-xl
+         bg-background px-5 py-3 border border-border shadow-sm"
           >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/_components/list/list-actions/ListActionsView.tsx around
lines 91 - 92, The dropdown in ListActionsView.tsx uses hardcoded light-theme
Tailwind classes (bg-white, border-gray-200); update the container's className
in the ListActionsView component to theme-aware tokens (e.g., replace bg-white →
bg-background and border-gray-200 → border-border) so the dropdown adapts to
dark mode; ensure any related text or shadow classes inside the same element
also use theme-aware tokens (e.g., text-foreground if needed) to maintain
contrast across themes.
src/components/empty-state/EmptyState.tsx (1)

14-14: Hardcoded color may not adapt to dark mode.

With the PR introducing dark mode via ThemeProvider, this hardcoded text-gray-600 won't automatically adjust for dark theme, potentially resulting in poor contrast. Consider using a theme-aware token (e.g., text-muted-foreground or a custom CSS variable defined in globals.css).

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

In `@src/components/empty-state/EmptyState.tsx` at line 14, In EmptyState.tsx
replace the hardcoded Tailwind color on the paragraph (<p className="...
text-gray-600 ...">) with a theme-aware token so it adapts to dark mode; for
example swap `text-gray-600` to your design token such as
`text-muted-foreground` or a CSS variable like `text-[var(--muted-foreground)]`
(defined in globals.css) so the EmptyState component uses the theme-aware color
instead of a fixed gray.
src/app/(protected)/dashboard/_components/record/RecordLineChart/RecordChartToggle.tsx (1)

15-17: Hardcoded text-black won't adapt to dark mode.

The active and hover states use text-black, which will be invisible or poorly visible on a dark background. Consider using theme-aware tokens like text-foreground for active states to ensure proper contrast in both light and dark modes.

♻️ Suggested theme-aware styling
         className={
           period === "daily"
-            ? "font-medium text-black after:w-full"
-            : "text-gray-600 hover:text-black"
+            ? "font-medium text-foreground after:w-full"
+            : "text-muted-foreground hover:text-foreground"
         }

Apply the same pattern to the monthly and yearly buttons.

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

In
`@src/app/`(protected)/dashboard/_components/record/RecordLineChart/RecordChartToggle.tsx
around lines 15 - 17, The active/hover styles in RecordChartToggle (the ternary
that checks period === "daily", and the similar logic for monthly/yearly
buttons) use hardcoded "text-black" which breaks in dark mode; update those
classnames to theme-aware tokens like "text-foreground" (or the project's
equivalent) for active/selected states and replace hover "text-black" with a
neutral hover token (e.g., "hover:text-foreground") so the daily/monthly/yearly
buttons adapt to light/dark themes while preserving the existing conditional
structure in the RecordChartToggle component.
src/app/(public)/login/page.tsx (1)

97-97: Inconsistent text color with join page.

This uses text-gray-600 while the join page (line 140) uses text-foreground. Consider using the theme token for consistency.

♻️ Proposed fix
-        <div className="mt-6 text-center text-sm text-gray-600">
+        <div className="mt-6 text-center text-sm text-foreground">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/login/page.tsx at line 97, The small help text in the login
page (the div with className "mt-6 text-center text-sm text-gray-600" in
page.tsx) uses the hardcoded tailwind color token text-gray-600 whereas the join
page uses the theme token text-foreground; update that div to use
text-foreground instead of text-gray-600 so both pages use the same theme token
for consistent text color.
src/providers/AuthProvider.tsx (1)

45-52: Consider adding handleSession to the dependency array or wrapping it with useCallback.

The useEffect has an empty dependency array but uses handleSession, which references setUser, clearUser, and setInitialized from the store. While Zustand store actions are typically stable, wrapping handleSession in useCallback with the store functions as dependencies would be more explicit and satisfy the exhaustive-deps rule.

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

In `@src/providers/AuthProvider.tsx` around lines 45 - 52, The effect uses
handleSession but has an empty dependency array; wrap handleSession in
useCallback (or add it to the effect deps) so it is stable and satisfies
exhaustive-deps: create a const handleSession = useCallback((session) => { ...
}, [setUser, clearUser, setInitialized]) referencing the zustand actions
(setUser, clearUser, setInitialized) used inside, then keep the useEffect that
subscribes via supabase.auth.onAuthStateChange and include handleSession in the
effect deps.
🤖 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/api/profile.ts`:
- Around line 13-16: Update the API function that calls maybeSingle() so its
signature returns Promise<Profile | null> instead of Promise<Profile>, remove
the unsafe cast "as Profile" and let TypeScript infer that data may be null from
maybeSingle(); keep the existing null-checks at the call site (AuthProvider.tsx)
which already handle null. Specifically, modify the function declaration (the
one that calls maybeSingle()) to change its return type to Promise<Profile |
null>, delete the "as Profile" cast on the return, and ensure any exports/types
align with the new nullable return.

In `@src/app/`(public)/join/page.tsx:
- Line 44: The success toast in src/app/(public)/join/page.tsx currently calls
toast.success("로그인이 되었습니다."); update the message passed to toast.success to
reflect successful signup/registration (e.g., "회원가입이 완료되었습니다." or "가입이
완료되었습니다.") so the toast matches the join page action; locate the toast.success
call in the join/page component and replace the string accordingly.
- Around line 50-55: The catch block in the signup handler currently calls
toast.error twice because the generic message runs unconditionally; update the
catch in the function containing the try/catch (the signup handler in page.tsx)
so that when error instanceof Error you only call toast.error(error.message) and
skip the generic toast (either by putting the generic toast in an else branch or
returning after the specific toast), ensuring only one toast is shown on signup
failure.

In `@src/app/`(public)/landing-page/_components/feature/Feature.tsx:
- Around line 77-79: The Button's href currently uses a relative path
("./dashboard") which will resolve incorrectly from nested routes; update the
Button component's href prop (the instance with href="./dashboard" in
Feature.tsx) to an absolute path "/dashboard" so it always navigates to the
top-level dashboard route.
- Line 57: The className on the Feature component contains duplicate border
tokens where "border-gray-300" overrides the theme token "border-border"; remove
"border-gray-300" from the className string in the JSX (the element in
Feature.tsx that currently has className="rounded-2xl border border-border
border-gray-300 p-5 ...") and keep only the theme-aware "border-border" so
dark-mode and theme tokens behave correctly.

In `@src/components/button/Button.tsx`:
- Around line 28-34: The outline variant's style string (property name "outline"
in Button.tsx) sets both text and background to "point" on hover which loses
contrast; update the hover rules so the hover background remains "bg-point" but
the hover text class switches to a high-contrast color (for example "text-white"
or your design token for text-on-point) and do the same for the dark:hover
variant so the label remains readable. Ensure only the hover text classes are
changed (not the base text-point) and run a quick contrast check visually or
with your design tokens.

In `@src/components/button/ThemeButton.tsx`:
- Around line 6-18: The button currently uses useTheme().theme which can be
"system" and yields wrong label/styles; switch to using resolvedTheme for
display and styling and use resolvedTheme (falling back to theme if needed) when
computing the toggle value passed to setTheme; update the JSX in ThemeButton
(references: useTheme, theme, resolvedTheme, setTheme, mounted, setMounted) so
className and button text use resolvedTheme and the onClick toggles to the
opposite of the resolvedTheme ("light" <-> "dark") while preserving the existing
mounted logic.

In `@src/providers/AuthProvider.tsx`:
- Around line 46-47: The onAuthStateChange callback incorrectly names the second
parameter "sesseion" causing a typo when passing it to handleSession; update the
callback signature in supabase.auth.onAuthStateChange to use "session" (and
update any uses inside the callback, e.g., the argument passed to handleSession)
so it calls handleSession(session) consistently.

---

Outside diff comments:
In `@package.json`:
- Around line 24-25: Remove the unused "sooner" dependency entry from
package.json (the line with "sooner": "^1.1.4") since it's not referenced in the
codebase and appears to be a typo of "sonner"; update package.json by deleting
that dependency entry and then run dependency install (e.g., npm install or
yarn) to refresh lockfile and node_modules so the change is reflected.

In `@src/app/`(public)/login/page.tsx:
- Around line 38-43: In the catch block in src/app/(public)/login/page.tsx where
two toast.error calls exist, ensure only one toast runs by returning or using an
else: when (error instanceof Error) call toast.error(error.message) and then
return (or use else) so the generic toast.error("로그인 중 오류가 발생했습니다.") does not
also execute; update the catch block around the login submit/handler to either
add a return after the specific toast or wrap the generic toast in an else
branch.

---

Nitpick comments:
In `@src/app/`(protected)/_components/list/list-actions/ListActionsView.tsx:
- Around line 91-92: The dropdown in ListActionsView.tsx uses hardcoded
light-theme Tailwind classes (bg-white, border-gray-200); update the container's
className in the ListActionsView component to theme-aware tokens (e.g., replace
bg-white → bg-background and border-gray-200 → border-border) so the dropdown
adapts to dark mode; ensure any related text or shadow classes inside the same
element also use theme-aware tokens (e.g., text-foreground if needed) to
maintain contrast across themes.

In
`@src/app/`(protected)/dashboard/_components/record/RecordLineChart/RecordChartToggle.tsx:
- Around line 15-17: The active/hover styles in RecordChartToggle (the ternary
that checks period === "daily", and the similar logic for monthly/yearly
buttons) use hardcoded "text-black" which breaks in dark mode; update those
classnames to theme-aware tokens like "text-foreground" (or the project's
equivalent) for active/selected states and replace hover "text-black" with a
neutral hover token (e.g., "hover:text-foreground") so the daily/monthly/yearly
buttons adapt to light/dark themes while preserving the existing conditional
structure in the RecordChartToggle component.

In `@src/app/`(public)/login/page.tsx:
- Line 97: The small help text in the login page (the div with className "mt-6
text-center text-sm text-gray-600" in page.tsx) uses the hardcoded tailwind
color token text-gray-600 whereas the join page uses the theme token
text-foreground; update that div to use text-foreground instead of text-gray-600
so both pages use the same theme token for consistent text color.

In `@src/components/empty-state/EmptyState.tsx`:
- Line 14: In EmptyState.tsx replace the hardcoded Tailwind color on the
paragraph (<p className="... text-gray-600 ...">) with a theme-aware token so it
adapts to dark mode; for example swap `text-gray-600` to your design token such
as `text-muted-foreground` or a CSS variable like
`text-[var(--muted-foreground)]` (defined in globals.css) so the EmptyState
component uses the theme-aware color instead of a fixed gray.

In `@src/components/input/Input.tsx`:
- Line 50: In the Input component (Input.tsx) fix the className string that
currently has two spaces between "text-sm" and "transition" by removing the
extra space so the value reads "... text-sm transition focus:outline-none";
update the className assignment in the Input component/JSX where that string is
used.

In `@src/providers/AuthProvider.tsx`:
- Around line 45-52: The effect uses handleSession but has an empty dependency
array; wrap handleSession in useCallback (or add it to the effect deps) so it is
stable and satisfies exhaustive-deps: create a const handleSession =
useCallback((session) => { ... }, [setUser, clearUser, setInitialized])
referencing the zustand actions (setUser, clearUser, setInitialized) used
inside, then keep the useEffect that subscribes via
supabase.auth.onAuthStateChange and include handleSession in the effect deps.

In `@src/providers/ThemeProvider.tsx`:
- Line 1: Remove the redundant leading file path comment "//
providers/ThemeProvider.tsx" from the top of the ThemeProvider module; simply
delete that comment line so the file begins with the actual code (e.g., the
ThemeProvider component or its imports) and leave all existing symbols like
ThemeProvider unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71dd6090-a3a7-49ac-943f-2f8e74dbe2c1

📥 Commits

Reviewing files that changed from the base of the PR and between 15557c5 and 6819126.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • package.json
  • src/api/auth.ts
  • src/api/profile.ts
  • src/app/(protected)/_components/layout/Header.tsx
  • src/app/(protected)/_components/list/list-actions/ListActionsView.tsx
  • src/app/(protected)/_components/modal/ModalTop.tsx
  • src/app/(protected)/dashboard/_components/common/banner/Banner.tsx
  • src/app/(protected)/dashboard/_components/record/RecordLineChart/RecordChartToggle.tsx
  • src/app/(protected)/record/_components/FilterCount.tsx
  • src/app/(public)/_components/PublicHeader.tsx
  • src/app/(public)/_components/title/Title.tsx
  • src/app/(public)/join/page.tsx
  • src/app/(public)/landing-page/LadingPage.tsx
  • src/app/(public)/landing-page/_components/SectionWrapper.tsx
  • src/app/(public)/landing-page/_components/banner/Banner.tsx
  • src/app/(public)/landing-page/_components/feature/Feature.tsx
  • src/app/(public)/landing-page/_components/info/Info.tsx
  • src/app/(public)/login/page.tsx
  • src/app/globals.css
  • src/app/layout.tsx
  • src/components/button/Button.tsx
  • src/components/button/ThemeButton.tsx
  • src/components/empty-state/EmptyState.tsx
  • src/components/input/Input.tsx
  • src/components/layout/Footer.tsx
  • src/providers/AuthProvider.tsx
  • src/providers/ThemeProvider.tsx
  • src/store/useUserStore.ts
💤 Files with no reviewable changes (2)
  • src/app/(public)/landing-page/LadingPage.tsx
  • src/app/(public)/landing-page/_components/info/Info.tsx

Comment thread src/api/profile.ts
Comment on lines +13 to 16
.maybeSingle();

if (error) throw error;
return data as Profile;

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Definition + call sites for getMyProfile:"
rg -nP --type=ts -C3 '\bgetMyProfile\s*\('

echo
echo "Potential null-handling patterns near profile usage:"
rg -nP --type=ts -C3 '(==\s*null|===\s*null|\?\.)'

Repository: goodaseul/stack-plus

Length of output: 8416


Remove unsafe cast and update return type to reflect nullable value.

The cast as Profile on Line 16 hides that maybeSingle() returns nullable data. While the call site in AuthProvider.tsx already checks for null, relying on runtime checks instead of type safety is error-prone. Update the return type to Promise<Profile | null> and remove the cast.

Suggested fix
-export async function getMyProfile(userId: string) {
+export async function getMyProfile(userId: string): Promise<Profile | null> {
   const { data, error } = await supabase
     .from("profiles")
     .select("id, nickname")
     .eq("id", userId)
     .maybeSingle();

   if (error) throw error;
-  return data as Profile;
+  return data;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.maybeSingle();
if (error) throw error;
return data as Profile;
export async function getMyProfile(userId: string): Promise<Profile | null> {
const { data, error } = await supabase
.from("profiles")
.select("id, nickname")
.eq("id", userId)
.maybeSingle();
if (error) throw error;
return data;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/profile.ts` around lines 13 - 16, Update the API function that calls
maybeSingle() so its signature returns Promise<Profile | null> instead of
Promise<Profile>, remove the unsafe cast "as Profile" and let TypeScript infer
that data may be null from maybeSingle(); keep the existing null-checks at the
call site (AuthProvider.tsx) which already handle null. Specifically, modify the
function declaration (the one that calls maybeSingle()) to change its return
type to Promise<Profile | null>, delete the "as Profile" cast on the return, and
ensure any exports/types align with the new nullable return.

});
router.push("/login");

toast.success("로그인이 되었습니다.");

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

Incorrect success toast message.

The toast says "로그인이 되었습니다" (You have logged in) but this is the signup page. It should indicate successful registration.

📝 Proposed fix
-      toast.success("로그인이 되었습니다.");
+      toast.success("회원가입이 완료되었습니다.");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
toast.success("로그인이 되었습니다.");
toast.success("회원가입이 완료되었습니다.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/join/page.tsx at line 44, The success toast in
src/app/(public)/join/page.tsx currently calls toast.success("로그인이 되었습니다.");
update the message passed to toast.success to reflect successful
signup/registration (e.g., "회원가입이 완료되었습니다." or "가입이 완료되었습니다.") so the toast
matches the join page action; locate the toast.success call in the join/page
component and replace the string accordingly.

Comment on lines 50 to 55
} catch (error) {
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error("회원가입 중 알 수 없는 오류가 발생했습니다.");
}
toast.error("회원가입 중 오류가 발생했습니다.");
}

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 | 🟠 Major

Double toast error on signup failure.

When error instanceof Error is true, both toast.error(error.message) (line 52) and toast.error("회원가입 중 오류가 발생했습니다.") (line 54) will execute because line 54 is outside the if block. This shows two error toasts to the user.

🐛 Proposed fix
     } catch (error) {
       if (error instanceof Error) {
         toast.error(error.message);
+      } else {
+        toast.error("회원가입 중 오류가 발생했습니다.");
       }
-      toast.error("회원가입 중 오류가 발생했습니다.");
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error("회원가입 중 알 수 없는 오류가 발생했습니다.");
}
toast.error("회원가입 중 오류가 발생했습니다.");
}
} catch (error) {
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error("회원가입 중 오류가 발생했습니다.");
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/join/page.tsx around lines 50 - 55, The catch block in the
signup handler currently calls toast.error twice because the generic message
runs unconditionally; update the catch in the function containing the try/catch
(the signup handler in page.tsx) so that when error instanceof Error you only
call toast.error(error.message) and skip the generic toast (either by putting
the generic toast in an else branch or returning after the specific toast),
ensuring only one toast is shown on signup failure.

<div
key={feature.title}
className="group rounded-2xl border border-border p-6 border-gray-200"
className="rounded-2xl border border-border border-gray-300 p-5 md:px-2 md:py-7 md:text-center text-left"

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

Remove duplicate border color class.

border-border (theme token) is overridden by border-gray-300. Use only the theme-aware token to maintain dark mode compatibility.

♻️ Proposed fix
-            className="rounded-2xl border border-border border-gray-300 p-5 md:px-2 md:py-7 md:text-center text-left"
+            className="rounded-2xl border border-border p-5 md:px-2 md:py-7 md:text-center text-left"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
className="rounded-2xl border border-border border-gray-300 p-5 md:px-2 md:py-7 md:text-center text-left"
className="rounded-2xl border border-border p-5 md:px-2 md:py-7 md:text-center text-left"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/landing-page/_components/feature/Feature.tsx at line 57,
The className on the Feature component contains duplicate border tokens where
"border-gray-300" overrides the theme token "border-border"; remove
"border-gray-300" from the className string in the JSX (the element in
Feature.tsx that currently has className="rounded-2xl border border-border
border-gray-300 p-5 ...") and keep only the theme-aware "border-border" so
dark-mode and theme tokens behave correctly.

Comment on lines +77 to +79
<Button href="./dashboard" type="button" className="mt-10 w-40 mx-auto">
시작하기
</Button>

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 | 🟠 Major

Use absolute path for dashboard link.

The relative path ./dashboard will resolve relative to the current route (/landing-page/dashboard), which is likely incorrect. Use an absolute path /dashboard instead.

🐛 Proposed fix
-      <Button href="./dashboard" type="button" className="mt-10 w-40 mx-auto">
+      <Button href="/dashboard" type="button" className="mt-10 w-40 mx-auto">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button href="./dashboard" type="button" className="mt-10 w-40 mx-auto">
시작하기
</Button>
<Button href="/dashboard" type="button" className="mt-10 w-40 mx-auto">
시작하기
</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/landing-page/_components/feature/Feature.tsx around lines
77 - 79, The Button's href currently uses a relative path ("./dashboard") which
will resolve incorrectly from nested routes; update the Button component's href
prop (the instance with href="./dashboard" in Feature.tsx) to an absolute path
"/dashboard" so it always navigates to the top-level dashboard route.

Comment thread src/components/button/Button.tsx
Comment thread src/components/button/ThemeButton.tsx Outdated
Comment on lines +6 to +18
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);

// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => setMounted(true), []);
if (!mounted) return null;

return (
<button
className={`${theme === "dark" ? "bg-white text-point" : "bg-point text-white"} rounded-sm px-2 py-1 `}
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
{theme === "dark" ? "light" : "dark"}

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

Use resolvedTheme instead of theme for consistent behavior.

When the theme is set to "system", theme will return "system" rather than the actual resolved theme ("dark" or "light"). This causes incorrect styling and label when users have system theme preference. The login and join pages correctly use resolvedTheme for this purpose.

🐛 Proposed fix
 export function ThemeToggle() {
-  const { theme, setTheme } = useTheme();
+  const { resolvedTheme, setTheme } = useTheme();
   const [mounted, setMounted] = useState(false);
 
   // eslint-disable-next-line react-hooks/set-state-in-effect
   useEffect(() => setMounted(true), []);
   if (!mounted) return null;
 
   return (
     <button
-      className={`${theme === "dark" ? "bg-white text-point" : "bg-point text-white"} rounded-sm px-2 py-1  `}
-      onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
+      className={`${resolvedTheme === "dark" ? "bg-white text-point" : "bg-point text-white"} rounded-sm px-2 py-1  `}
+      onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
     >
-      {theme === "dark" ? "light" : "dark"}
+      {resolvedTheme === "dark" ? "light" : "dark"}
     </button>
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return (
<button
className={`${theme === "dark" ? "bg-white text-point" : "bg-point text-white"} rounded-sm px-2 py-1 `}
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
{theme === "dark" ? "light" : "dark"}
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return (
<button
className={`${resolvedTheme === "dark" ? "bg-white text-point" : "bg-point text-white"} rounded-sm px-2 py-1 `}
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
>
{resolvedTheme === "dark" ? "light" : "dark"}
</button>
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/button/ThemeButton.tsx` around lines 6 - 18, The button
currently uses useTheme().theme which can be "system" and yields wrong
label/styles; switch to using resolvedTheme for display and styling and use
resolvedTheme (falling back to theme if needed) when computing the toggle value
passed to setTheme; update the JSX in ThemeButton (references: useTheme, theme,
resolvedTheme, setTheme, mounted, setMounted) so className and button text use
resolvedTheme and the onClick toggles to the opposite of the resolvedTheme
("light" <-> "dark") while preserving the existing mounted logic.

Comment on lines +46 to +47
const { data } = supabase.auth.onAuthStateChange((event, sesseion) => {
handleSession(sesseion);

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

Fix typo in variable name.

sesseion should be session.

📝 Proposed fix
-    const { data } = supabase.auth.onAuthStateChange((event, sesseion) => {
-      handleSession(sesseion);
+    const { data } = supabase.auth.onAuthStateChange((_event, session) => {
+      handleSession(session);
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data } = supabase.auth.onAuthStateChange((event, sesseion) => {
handleSession(sesseion);
const { data } = supabase.auth.onAuthStateChange((_event, session) => {
handleSession(session);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/providers/AuthProvider.tsx` around lines 46 - 47, The onAuthStateChange
callback incorrectly names the second parameter "sesseion" causing a typo when
passing it to handleSession; update the callback signature in
supabase.auth.onAuthStateChange to use "session" (and update any uses inside the
callback, e.g., the argument passed to handleSession) so it calls
handleSession(session) consistently.

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

♻️ Duplicate comments (4)
src/app/(public)/landing-page/_components/feature/Feature.tsx (1)

79-81: ⚠️ Potential issue | 🟠 Major

Use an absolute route for the dashboard CTA.

Line 79 uses a relative path ("./dashboard"), which can resolve to a nested route from /landing-page. Use "/dashboard" for deterministic navigation.

🐛 Proposed fix
-      <Button href="./dashboard" type="button" className="mt-10 w-40 mx-auto">
+      <Button href="/dashboard" type="button" className="mt-10 w-40 mx-auto">
         시작하기
       </Button>
#!/bin/bash
# Verify remaining relative dashboard links in TSX files
rg -nP --type=tsx 'href="\./dashboard"|href="../dashboard"|href="dashboard"' src
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/landing-page/_components/feature/Feature.tsx around lines
79 - 81, In Feature.tsx update the Button component's href prop from the
relative "./dashboard" to the absolute "/dashboard" so the CTA always navigates
to the root dashboard; locate the Button usage in the Feature component (the JSX
element named Button with href="./dashboard") and change its href value to
"/dashboard", and then run the suggested ripgrep check to verify no other TSX
files use relative dashboard links.
src/app/(public)/join/page.tsx (3)

49-54: ⚠️ Potential issue | 🟠 Major

Double toast error on signup failure.

When error instanceof Error is true, both toast.error(error.message) (line 51) and toast.error("회원가입 중 오류가 발생했습니다.") (line 53) will execute because line 53 is outside the if block.

🐛 Proposed fix
     } catch (error) {
       if (error instanceof Error) {
         toast.error(error.message);
+      } else {
+        toast.error("회원가입 중 오류가 발생했습니다.");
       }
-      toast.error("회원가입 중 오류가 발생했습니다.");
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/join/page.tsx around lines 49 - 54, The catch block in the
signup handler in page.tsx currently calls toast.error twice when error is an
instance of Error; modify the catch so only one toast is shown by moving the
generic toast into an else branch or returning after toast.error(error.message).
Specifically update the catch(error) logic around the existing instanceof Error
check to either use `else toast.error("회원가입 중 오류가 발생했습니다.");` or add an early
return after `toast.error(error.message)` to prevent the duplicate notification.

42-42: ⚠️ Potential issue | 🟡 Minor

Incorrect success toast message.

The toast says "로그인이 되었습니다" (You have logged in) but this is the signup page. It should indicate successful registration.

📝 Proposed fix
-      toast.success("로그인이 되었습니다.");
+      toast.success("회원가입이 완료되었습니다.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/join/page.tsx at line 42, Replace the incorrect login
success message used in the signup flow by updating the toast.success call
(toast.success("로그인이 되었습니다.");) in src/app/(public)/join/page.tsx to a
registration-specific message (e.g., "회원가입이 완료되었습니다." or similar) so the user
sees a signup success notification; locate the toast.success invocation in the
signup/onSubmit handler and change the string accordingly.

44-48: ⚠️ Potential issue | 🟠 Major

Improve signup flow messaging for email verification cases.

The current signup flow has a misleading toast message. Line 42 shows toast.success("로그인이 되었습니다.") (says "logged in") during signup, which is confusing. Additionally, when Supabase email confirmation is enabled, result.session will be null and users are redirected to /login without understanding they need to verify their email.

The code at lines 44-48 handles the null session case, but provides no user-facing indication of why they're being sent to the login page.

Consider updating the messages to clarify the email verification requirement:

🛠️ Suggested improvement
-      toast.success("로그인이 되었습니다.");
-
-      if (result.session) {
-        router.push("/dashboard");
-      } else {
-        router.push("/login");
-      }
+      if (result.session) {
+        toast.success("회원가입이 완료되었습니다.");
+        router.push("/dashboard");
+      } else {
+        toast.success("이메일 인증 링크를 확인해주세요.");
+        router.push("/login");
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/join/page.tsx around lines 44 - 48, Change the signup
success messaging and add a clear notification when Supabase returns no session:
update the toast.success call currently saying "로그인이 되었습니다." to a more generic
signup-success message (e.g., "회원가입이 완료되었습니다. 이메일을 확인해주세요.") and, in the branch
that checks result.session (the result.session conditional that calls
router.push("/dashboard") or router.push("/login")), add a
toast.info/toast.success before redirecting when result.session is null to
explain that email verification is required (e.g., "이메일 인증 후 로그인이 가능합니다. 이메일의 확인
링크를 클릭하세요.") so users redirected by router.push("/login") understand why; keep
the logic in the same block that references result.session and router.push and
do not change the redirect targets.
🧹 Nitpick comments (10)
src/app/(protected)/_components/list/ListContent.tsx (2)

62-62: Simplify: template literal is unnecessary.

The memo prop is already a string, so the template literal wrapping is redundant.

✨ Suggested simplification
-            value={`${memo}`}
+            value={memo}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/_components/list/ListContent.tsx at line 62, Replace the
unnecessary template literal around the memo prop in the JSX (value={`${memo}`})
with the direct string value (value={memo}) to avoid redundant interpolation;
locate the usage in ListContent.tsx where the memo prop is passed into the
input/field component and update the JSX to use value={memo} instead.

66-66: Remove commented-out code.

Leaving tabIndex={-1} commented out adds noise. If the change in focus behavior is intentional, remove the comment entirely; if reverting is likely, track it in a TODO with context or version control instead.

🧹 Proposed cleanup
-            // tabIndex={-1}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/_components/list/ListContent.tsx at line 66, Remove the
commented-out prop "tabIndex={-1}" in ListContent.tsx to eliminate noise; if the
change to focus behavior is intentional and may need re-evaluation, replace the
commented line with a short TODO comment explaining why tabIndex was removed and
when it can be revisited (reference the "tabIndex={-1}" snippet in the
ListContent component).
src/api/words.ts (1)

55-57: Guard pagination bounds before building range.

If page <= 0 or pageSize <= 0, Line 55/Line 57 can produce invalid negative ranges. Add a quick guard/clamp at function entry.

Proposed fix
 export async function getWords({
   filter,
   keyword,
   wordId,
   page = 1,
   pageSize = 20,
 }: WordsQueryRequest): Promise<{ words: WordsRequest[]; totalCount: number }> {
+  if (page < 1 || pageSize < 1) {
+    throw new Error("Invalid pagination: page and pageSize must be >= 1");
+  }
+
   const user = await getUserOrThrow();
@@
   const from = (page - 1) * pageSize;
   query = query.range(from, from + pageSize - 1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/words.ts` around lines 55 - 57, Guard against non-positive pagination
values before computing from and calling query.range: clamp or default page and
pageSize to minimum 1 (e.g., if page <= 0 set page = 1; if pageSize <= 0 set
pageSize = 1) so that const from = (page - 1) * pageSize never becomes negative,
then call query.range(from, from + pageSize - 1) as before; reference the page,
pageSize, from variables and the query.range(...) invocation when applying the
change.
src/app/(protected)/_components/layout/Header/NavBtn.tsx (1)

10-12: Parameter name shadows type keyword.

Using boolean as the parameter name is confusing since it's also a type keyword. Consider a more descriptive name.

✏️ Suggested fix
 }: {
   isMenuOpen: boolean;
-  setIsMenuOpen: (boolean: boolean) => void;
+  setIsMenuOpen: (isOpen: boolean) => void;
 }) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/_components/layout/Header/NavBtn.tsx around lines 10 -
12, The prop type for setIsMenuOpen currently uses the parameter name "boolean"
which shadows the type keyword; update the function signature for setIsMenuOpen
in the NavBtn props so the parameter has a descriptive name (e.g., "value" or
"open") and keep its type as boolean (for example setIsMenuOpen: (open: boolean)
=> void), and update any call sites inside NavBtn that reference the parameter
name accordingly; this change affects the props object containing isMenuOpen and
setIsMenuOpen in NavBtn.
src/app/(protected)/dashboard/page.tsx (3)

10-10: Make the loading gate screen-reader friendly.

At Line 10, consider adding role="status" and aria-live="polite" so the loading state is announced accessibly.

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

In `@src/app/`(protected)/dashboard/page.tsx at line 10, The loading gate
currently returns a plain div when isInitialized is false; update that JSX
return (the conditional using isInitialized in page.tsx) so the loading
container includes accessibility attributes role="status" and aria-live="polite"
(and keep the visible text "로딩 중..") to ensure screen readers announce the
loading state.

30-30: Consider restoring a semantic section wrapper for RecordStudy.

At Line 30, rendering RecordStudy directly is fine functionally, but if this is a distinct page region, a <section> wrapper would improve document structure and navigation landmarks.

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

In `@src/app/`(protected)/dashboard/page.tsx at line 30, Restore a semantic
section wrapper around the RecordStudy component in the dashboard page by
replacing the direct <RecordStudy /> render with a <section> element that
contains <RecordStudy /> (optionally add an id or aria-labelledby if there is a
heading inside RecordStudy to create a proper landmark). Update the JSX in the
page component that renders RecordStudy so the section wraps the component (keep
existing props and imports unchanged).

6-9: Use a Zustand selector to prevent unnecessary re-renders when accessing a single state value.

At line 9, destructuring from useUserStore() subscribes the component to all store changes. Since only isInitialized is needed, use a selector to subscribe to only that slice.

♻️ Proposed refactor
-  const { isInitialized } = useUserStore();
+  const isInitialized = useUserStore((state) => state.isInitialized);

Note: Other files in the codebase (e.g., src/providers/ProtectedProvider.tsx) already follow this pattern.

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

In `@src/app/`(protected)/dashboard/page.tsx around lines 6 - 9, Replace the
current full-store destructuring in DashboardPage (const { isInitialized } =
useUserStore()) with a selector-based subscription so the component only
subscribes to the isInitialized slice; specifically, call useUserStore with a
selector that returns state.isInitialized (and assign that single boolean to
isInitialized) to avoid re-renders on unrelated store changes.
src/utils/defaultChart.ts (1)

3-7: DATE_SLICE values assume ISO 8601 date format.

The slice positions (4, 7, 10) work correctly only when created_at is in ISO 8601 format (YYYY-MM-DDTHH:mm:ss...). Per the WordsRequest interface in src/api/types/words.ts, created_at is typed as string without explicit format documentation.

Consider adding a brief comment documenting this assumption, or validating the format if the data source could vary.

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

In `@src/utils/defaultChart.ts` around lines 3 - 7, DATE_SLICE assumes ISO 8601
date strings (positions 4,7,10 for year/month/day) which isn't documented in
WordsRequest; add a brief inline comment next to the DATE_SLICE constant stating
the ISO 8601 assumption and the expected created_at format (e.g., "expects
YYYY-MM-DD..."), and/or add a small validation helper (used where created_at is
sliced) that checks the created_at string matches /^\d{4}-\d{2}-\d{2}/ and falls
back or normalizes input before slicing; reference DATE_SLICE and the
WordsRequest created_at field when making the change.
src/app/(protected)/_components/layout/Header/MyMenu.tsx (2)

18-24: Consider adding aria-expanded for accessibility.

The button toggles a dropdown menu but doesn't communicate its state to assistive technologies. Adding aria-expanded improves accessibility for screen reader users.

Suggested improvement
       <Button
         className="w-10 sm:w-auto"
         onClick={() => setIsMyOpen((prev) => !prev)}
         variant="text_underline"
+        aria-expanded={isMyOpen}
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/_components/layout/Header/MyMenu.tsx around lines 18 -
24, The Button in MyMenu.tsx that toggles the dropdown should include
aria-expanded to expose its open/closed state to assistive tech; update the
Button (the one using onClick={() => setIsMyOpen((prev) => !prev)}) to add
aria-expanded={isMyOpen} (or String(isMyOpen) if a string is required) so the
button reflects the value managed by isMyOpen when rendering the span showing
{id && nickname}.

1-6: Add "use client" directive for clarity and maintainability.

This component uses useState and useRef, which are client-side React hooks. While the component will work without the directive since it's only imported by the already-marked client component Header.tsx, adding "use client" at the top of the file makes the component self-contained, explicit about its client nature, and prevents potential issues if imported elsewhere in the future.

Suggested fix
+"use client";
+
 import Button from "@/components/button/Button";
 import { useWordStats } from "@/hooks/queries/words";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/_components/layout/Header/MyMenu.tsx around lines 1 - 6,
This file uses client-side hooks (useState, useRef, useClickOutside,
useUserStore) so add the React "use client" directive as the very first line of
src/app/(protected)/_components/layout/Header/MyMenu.tsx to make the component
explicitly client-side; ensure the directive appears above all imports so MyMenu
and its related hooks (useWordStats, useClickOutside, useUserStore) remain
self-contained and safe to import elsewhere.
🤖 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/app/`(protected)/_components/layout/Header/NavBtn.tsx:
- Around line 14-17: The click-outside ref (menuRef) is only wrapping the toggle
button in NavBtn, so clicks on the absolutely positioned MenuLink sibling are
treated as outside and immediately close the menu; fix this by ensuring the same
ref/container encloses both the toggle and the menu content (either move
MenuLink into NavBtn under the same <div ref={menuRef}> or lift menuRef to the
parent that renders both NavBtn and MenuLink and pass that ref into
useClickOutside), keeping the existing useClickOutside(menuRef, () =>
setIsMenuOpen(false), isMenuOpen) usage intact.

In `@src/app/`(protected)/_components/modal/word/ModalBody.tsx:
- Line 14: Remove the ineffective "focus:ring-1" from the shared InputStyles
string and from any manual className usages in ModalBody so the Input
component's injected "focus:ring-2" (from Input.tsx / the Input component) is
the only focus ring applied; locate the InputStyles symbol and the three Input
usages plus the textarea in ModalBody and delete the "focus:ring-1" token(s),
ensuring you do not reintroduce a conflicting focus ring elsewhere.
- Line 14: Update the Tailwind classes in the class string in ModalBody.tsx:
replace the invalid "placeholder place-gray-100" with the correct Tailwind v4
syntax "placeholder:text-gray-100", and remove the conflicting "focus:ring-1"
since the Input component applies "focus:ring-2" (so leave focus ring to the
Input component or change to match "focus:ring-2" if you want the same effect);
adjust the class string accordingly where it appears in ModalBody.tsx so there
are no invalid or conflicting focus/placeholder utilities.

In `@src/app/`(protected)/dashboard/_components/common/banner/BannerTitle.tsx:
- Around line 18-27: The h3 in BannerTitle.tsx is missing the light-mode class
when isWhite is true; update the clsx usage in the <h3> element (where title is
rendered and isWhite prop is used) to include "text-black" for the light theme
as well as "dark:text-black" when isWhite is true so its behavior matches the
<p> below and avoids relying on inherited colors.

In `@src/app/`(protected)/dashboard/_components/record/RecordChart.tsx:
- Around line 36-40: Typo: change the Korean text inside EmptyState in
RecordChart.tsx from "아직 저장된 표현가 없습니다." to the correct "아직 저장된 표현이 없습니다."—update
the string in the JSX where EmptyState is rendered (the conditional that checks
words?.length) so the displayed message uses "표현이".

---

Duplicate comments:
In `@src/app/`(public)/join/page.tsx:
- Around line 49-54: The catch block in the signup handler in page.tsx currently
calls toast.error twice when error is an instance of Error; modify the catch so
only one toast is shown by moving the generic toast into an else branch or
returning after toast.error(error.message). Specifically update the catch(error)
logic around the existing instanceof Error check to either use `else
toast.error("회원가입 중 오류가 발생했습니다.");` or add an early return after
`toast.error(error.message)` to prevent the duplicate notification.
- Line 42: Replace the incorrect login success message used in the signup flow
by updating the toast.success call (toast.success("로그인이 되었습니다.");) in
src/app/(public)/join/page.tsx to a registration-specific message (e.g., "회원가입이
완료되었습니다." or similar) so the user sees a signup success notification; locate the
toast.success invocation in the signup/onSubmit handler and change the string
accordingly.
- Around line 44-48: Change the signup success messaging and add a clear
notification when Supabase returns no session: update the toast.success call
currently saying "로그인이 되었습니다." to a more generic signup-success message (e.g.,
"회원가입이 완료되었습니다. 이메일을 확인해주세요.") and, in the branch that checks result.session
(the result.session conditional that calls router.push("/dashboard") or
router.push("/login")), add a toast.info/toast.success before redirecting when
result.session is null to explain that email verification is required (e.g.,
"이메일 인증 후 로그인이 가능합니다. 이메일의 확인 링크를 클릭하세요.") so users redirected by
router.push("/login") understand why; keep the logic in the same block that
references result.session and router.push and do not change the redirect
targets.

In `@src/app/`(public)/landing-page/_components/feature/Feature.tsx:
- Around line 79-81: In Feature.tsx update the Button component's href prop from
the relative "./dashboard" to the absolute "/dashboard" so the CTA always
navigates to the root dashboard; locate the Button usage in the Feature
component (the JSX element named Button with href="./dashboard") and change its
href value to "/dashboard", and then run the suggested ripgrep check to verify
no other TSX files use relative dashboard links.

---

Nitpick comments:
In `@src/api/words.ts`:
- Around line 55-57: Guard against non-positive pagination values before
computing from and calling query.range: clamp or default page and pageSize to
minimum 1 (e.g., if page <= 0 set page = 1; if pageSize <= 0 set pageSize = 1)
so that const from = (page - 1) * pageSize never becomes negative, then call
query.range(from, from + pageSize - 1) as before; reference the page, pageSize,
from variables and the query.range(...) invocation when applying the change.

In `@src/app/`(protected)/_components/layout/Header/MyMenu.tsx:
- Around line 18-24: The Button in MyMenu.tsx that toggles the dropdown should
include aria-expanded to expose its open/closed state to assistive tech; update
the Button (the one using onClick={() => setIsMyOpen((prev) => !prev)}) to add
aria-expanded={isMyOpen} (or String(isMyOpen) if a string is required) so the
button reflects the value managed by isMyOpen when rendering the span showing
{id && nickname}.
- Around line 1-6: This file uses client-side hooks (useState, useRef,
useClickOutside, useUserStore) so add the React "use client" directive as the
very first line of src/app/(protected)/_components/layout/Header/MyMenu.tsx to
make the component explicitly client-side; ensure the directive appears above
all imports so MyMenu and its related hooks (useWordStats, useClickOutside,
useUserStore) remain self-contained and safe to import elsewhere.

In `@src/app/`(protected)/_components/layout/Header/NavBtn.tsx:
- Around line 10-12: The prop type for setIsMenuOpen currently uses the
parameter name "boolean" which shadows the type keyword; update the function
signature for setIsMenuOpen in the NavBtn props so the parameter has a
descriptive name (e.g., "value" or "open") and keep its type as boolean (for
example setIsMenuOpen: (open: boolean) => void), and update any call sites
inside NavBtn that reference the parameter name accordingly; this change affects
the props object containing isMenuOpen and setIsMenuOpen in NavBtn.

In `@src/app/`(protected)/_components/list/ListContent.tsx:
- Line 62: Replace the unnecessary template literal around the memo prop in the
JSX (value={`${memo}`}) with the direct string value (value={memo}) to avoid
redundant interpolation; locate the usage in ListContent.tsx where the memo prop
is passed into the input/field component and update the JSX to use value={memo}
instead.
- Line 66: Remove the commented-out prop "tabIndex={-1}" in ListContent.tsx to
eliminate noise; if the change to focus behavior is intentional and may need
re-evaluation, replace the commented line with a short TODO comment explaining
why tabIndex was removed and when it can be revisited (reference the
"tabIndex={-1}" snippet in the ListContent component).

In `@src/app/`(protected)/dashboard/page.tsx:
- Line 10: The loading gate currently returns a plain div when isInitialized is
false; update that JSX return (the conditional using isInitialized in page.tsx)
so the loading container includes accessibility attributes role="status" and
aria-live="polite" (and keep the visible text "로딩 중..") to ensure screen readers
announce the loading state.
- Line 30: Restore a semantic section wrapper around the RecordStudy component
in the dashboard page by replacing the direct <RecordStudy /> render with a
<section> element that contains <RecordStudy /> (optionally add an id or
aria-labelledby if there is a heading inside RecordStudy to create a proper
landmark). Update the JSX in the page component that renders RecordStudy so the
section wraps the component (keep existing props and imports unchanged).
- Around line 6-9: Replace the current full-store destructuring in DashboardPage
(const { isInitialized } = useUserStore()) with a selector-based subscription so
the component only subscribes to the isInitialized slice; specifically, call
useUserStore with a selector that returns state.isInitialized (and assign that
single boolean to isInitialized) to avoid re-renders on unrelated store changes.

In `@src/utils/defaultChart.ts`:
- Around line 3-7: DATE_SLICE assumes ISO 8601 date strings (positions 4,7,10
for year/month/day) which isn't documented in WordsRequest; add a brief inline
comment next to the DATE_SLICE constant stating the ISO 8601 assumption and the
expected created_at format (e.g., "expects YYYY-MM-DD..."), and/or add a small
validation helper (used where created_at is sliced) that checks the created_at
string matches /^\d{4}-\d{2}-\d{2}/ and falls back or normalizes input before
slicing; reference DATE_SLICE and the WordsRequest created_at field when making
the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b3ddfb7-4fab-4533-8f10-3620db3d07ce

📥 Commits

Reviewing files that changed from the base of the PR and between 6819126 and 7d0387c.

⛔ Files ignored due to path filters (1)
  • public/next.svg is excluded by !**/*.svg
📒 Files selected for processing (40)
  • src/api/profile.ts
  • src/api/types/words.ts
  • src/api/words.ts
  • src/app/(protected)/_components/add-word-button/AddWordButton.tsx
  • src/app/(protected)/_components/layout/Header.tsx
  • src/app/(protected)/_components/layout/Header/Logo.tsx
  • src/app/(protected)/_components/layout/Header/MenuLink.tsx
  • src/app/(protected)/_components/layout/Header/MyMenu.tsx
  • src/app/(protected)/_components/layout/Header/MyTooltip.tsx
  • src/app/(protected)/_components/layout/Header/NavBtn.tsx
  • src/app/(protected)/_components/list/List.tsx
  • src/app/(protected)/_components/list/ListContent.tsx
  • src/app/(protected)/_components/list/list-actions/ListActionsView.tsx
  • src/app/(protected)/_components/modal/word/ModalBody.tsx
  • src/app/(protected)/dashboard/_components/RecentWords.tsx
  • src/app/(protected)/dashboard/_components/common/Title.tsx
  • src/app/(protected)/dashboard/_components/common/banner/Banner.tsx
  • src/app/(protected)/dashboard/_components/common/banner/BannerTitle.tsx
  • src/app/(protected)/dashboard/_components/record/RecordChart.tsx
  • src/app/(protected)/dashboard/_components/record/RecordLineChart/RecordChartToggle.tsx
  • src/app/(protected)/dashboard/_components/record/RecordLineChart/RecordChartView.tsx
  • src/app/(protected)/dashboard/_components/record/RecordLineChart/useRecordChartData.ts
  • src/app/(protected)/dashboard/page.tsx
  • src/app/(public)/join/page.tsx
  • src/app/(public)/landing-page/_components/banner/Banner.tsx
  • src/app/(public)/landing-page/_components/feature/Feature.tsx
  • src/app/(public)/login/page.tsx
  • src/app/globals.css
  • src/components/button/Button.tsx
  • src/components/button/ThemeButton.tsx
  • src/components/input/Input.tsx
  • src/components/layout/Footer.tsx
  • src/constants/chartPeriod.ts
  • src/hooks/queries/words/querykey.ts
  • src/hooks/queries/words/useWordsQuery.ts
  • src/hooks/useMobileSize.ts
  • src/providers/AuthProvider.tsx
  • src/providers/ThemeProvider.tsx
  • src/types/word.ts
  • src/utils/defaultChart.ts
💤 Files with no reviewable changes (1)
  • src/app/(protected)/_components/layout/Header/MenuLink.tsx
✅ Files skipped from review due to trivial changes (15)
  • src/hooks/useMobileSize.ts
  • src/app/(protected)/_components/add-word-button/AddWordButton.tsx
  • src/app/(protected)/_components/layout/Header/MyTooltip.tsx
  • src/app/(protected)/dashboard/_components/record/RecordLineChart/RecordChartView.tsx
  • src/app/(protected)/dashboard/_components/RecentWords.tsx
  • src/app/(protected)/dashboard/_components/common/Title.tsx
  • src/app/(protected)/_components/list/List.tsx
  • src/app/(protected)/_components/layout/Header/Logo.tsx
  • src/app/(protected)/dashboard/_components/common/banner/Banner.tsx
  • src/app/(public)/landing-page/_components/banner/Banner.tsx
  • src/constants/chartPeriod.ts
  • src/providers/ThemeProvider.tsx
  • src/components/input/Input.tsx
  • src/components/button/Button.tsx
  • src/api/types/words.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/components/layout/Footer.tsx
  • src/api/profile.ts
  • src/app/(protected)/_components/list/list-actions/ListActionsView.tsx
  • src/components/button/ThemeButton.tsx
  • src/providers/AuthProvider.tsx
  • src/app/(protected)/_components/layout/Header.tsx
  • src/app/(public)/login/page.tsx
  • src/app/(protected)/dashboard/_components/record/RecordLineChart/RecordChartToggle.tsx

Comment on lines +14 to +17
const menuRef = useRef<HTMLDivElement | null>(null);
useClickOutside(menuRef, () => setIsMenuOpen(false), isMenuOpen);
return (
<div ref={menuRef}>

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how Header.tsx uses NavBtn and where the menu content is rendered
ast-grep --pattern 'NavBtn'
echo "---"
# Look at the Header component structure to see menu content placement
fd -t f "Header.tsx" --exec cat {}

Repository: goodaseul/stack-plus

Length of output: 2955


🏁 Script executed:

#!/bin/bash
# Get full NavBtn.tsx implementation
echo "=== NavBtn.tsx ===" 
cat "src/app/(protected)/_components/layout/Header/NavBtn.tsx"
echo ""
echo "=== MenuLink.tsx ===" 
cat "src/app/(protected)/_components/layout/Header/MenuLink.tsx"
echo ""
echo "=== useClickOutside hook ===" 
fd -t f "useClickOutside" --exec cat {}

Repository: goodaseul/stack-plus

Length of output: 2919


The ref scope is too narrow and will cause the menu to close when clicking menu items.

menuRef only wraps the button element in NavBtn. The actual menu content (MenuLink) is rendered as a separate sibling component in Header.tsx with position: absolute. When a user clicks on menu items in MenuLink, those clicks fall outside the menuRef boundary and trigger the useClickOutside handler, immediately closing the menu.

The ref should encompass both the toggle button and the menu content, or the menu content should be repositioned inside the same ref container.

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

In `@src/app/`(protected)/_components/layout/Header/NavBtn.tsx around lines 14 -
17, The click-outside ref (menuRef) is only wrapping the toggle button in
NavBtn, so clicks on the absolutely positioned MenuLink sibling are treated as
outside and immediately close the menu; fix this by ensuring the same
ref/container encloses both the toggle and the menu content (either move
MenuLink into NavBtn under the same <div ref={menuRef}> or lift menuRef to the
parent that renders both NavBtn and MenuLink and pass that ref into
useClickOutside), keeping the existing useClickOutside(menuRef, () =>
setIsMenuOpen(false), isMenuOpen) usage intact.

export default function ModalBody({ register, errors }: ModalBodyProps) {
const InputStyles =
"w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-black placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-gray-900 transition";
"w-full border border-gray-300 px-3 py-2 rounded-md text-sm text-black placeholder place-gray-100 focus:outline-none focus:ring-1 transition";

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the shared Input component class composition and where ModalBody is passing focus:ring-1.

set -e

echo "== Shared Input focus ring classes =="
rg -n -C3 'focus:ring-[0-9]|className=\{clsx\(' --type=tsx --type=ts 'src/components/input/Input.tsx'

echo
echo "== ModalBody usage of focus:ring-1 =="
rg -n -C2 'InputStyles|focus:ring-1|className=\{InputStyles\}' --type=tsx 'src/app/(protected)/_components/modal/word/ModalBody.tsx'

Repository: goodaseul/stack-plus

Length of output: 146


🏁 Script executed:

# First, find and read the Input component
echo "== Input.tsx file =="
find . -name "Input.tsx" -path "*/components/input/*" | head -1 | xargs cat -n

echo -e "\n== ModalBody.tsx file =="
find . -name "ModalBody.tsx" -path "*/(protected)/*" | head -1 | xargs cat -n

Repository: goodaseul/stack-plus

Length of output: 5471


Remove ineffective focus:ring-1 from InputStyles; Input component injects focus:ring-2.

InputStyles includes focus:ring-1 (line 14), but the Input component always injects focus:ring-2 (lines 52–53 in Input.tsx). Since both classes have equal CSS specificity, the focus:ring-2 from the shared Input component takes precedence in the Tailwind cascade, making the focus:ring-1 declaration ineffective.

Applies to all three Input usages at lines 22, 33, and 43, plus the textarea at line 70.

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

In `@src/app/`(protected)/_components/modal/word/ModalBody.tsx at line 14, Remove
the ineffective "focus:ring-1" from the shared InputStyles string and from any
manual className usages in ModalBody so the Input component's injected
"focus:ring-2" (from Input.tsx / the Input component) is the only focus ring
applied; locate the InputStyles symbol and the three Input usages plus the
textarea in ModalBody and delete the "focus:ring-1" token(s), ensuring you do
not reintroduce a conflicting focus ring elsewhere.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Tailwind version and check whether the current utility pattern is used elsewhere.

set -e

echo "== Tailwind dependency versions =="
fd '^package\.json$' -t f | xargs -I{} sh -c 'echo "--- {}"; cat "{}" | jq -r ".dependencies.tailwindcss // .devDependencies.tailwindcss // \"(not declared)\""'

echo
echo "== Occurrences of suspicious placeholder tokens =="
rg -n -C2 'placeholder place-gray|place-gray-[0-9]+' --type=tsx --type=ts --type=css

Repository: goodaseul/stack-plus

Length of output: 215


🏁 Script executed:

cat -n src/app/\(protected\)/_components/modal/word/ModalBody.tsx | head -50

Repository: goodaseul/stack-plus

Length of output: 2103


🏁 Script executed:

cat -n src/components/input/Input.tsx | head -80

Repository: goodaseul/stack-plus

Length of output: 2544


🌐 Web query:

Tailwind CSS v4 placeholder color utility syntax

💡 Result:

In Tailwind CSS v4, the syntax for placeholder color utilities remains the same as in previous versions: placeholder:text-{color} (e.g., placeholder:text-gray-400). This generates CSS targeting &::placeholder { color: var(--color-gray-400); }. Non-color utilities like placeholder:italic work reliably. However, there are reported issues in Chrome where placeholder:text-{color} may not apply due to browser specificity, requiring placeholder:text-{color}! (with ! for !important) as a workaround. Arbitrary values or custom selectors can also be used: [&::placeholder]:text-gray-400 or placeholder:text-[rgb(255,0,0)]. Official v4 docs do not indicate a syntax change for this utility, and Play examples confirm generation.

Citations:


Fix invalid Tailwind placeholder utility syntax and conflicting focus ring class.

Line 14 uses placeholder place-gray-100, which is invalid in Tailwind v4. The correct syntax is placeholder:text-gray-100.

Additionally, the focus:ring-1 utility in this constant will be overridden by focus:ring-2 applied by the Input component (lines 52–53 in src/components/input/Input.tsx), making the override ineffective.

Suggested fix
-    "w-full border border-gray-300 px-3 py-2 rounded-md text-sm text-black placeholder place-gray-100 focus:outline-none focus:ring-1 transition";
+    "w-full border border-gray-300 px-3 py-2 rounded-md text-sm text-black placeholder:text-gray-100 focus:outline-none focus:ring-2 transition";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"w-full border border-gray-300 px-3 py-2 rounded-md text-sm text-black placeholder place-gray-100 focus:outline-none focus:ring-1 transition";
"w-full border border-gray-300 px-3 py-2 rounded-md text-sm text-black placeholder:text-gray-100 focus:outline-none focus:ring-2 transition";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/_components/modal/word/ModalBody.tsx at line 14, Update
the Tailwind classes in the class string in ModalBody.tsx: replace the invalid
"placeholder place-gray-100" with the correct Tailwind v4 syntax
"placeholder:text-gray-100", and remove the conflicting "focus:ring-1" since the
Input component applies "focus:ring-2" (so leave focus ring to the Input
component or change to match "focus:ring-2" if you want the same effect); adjust
the class string accordingly where it appears in ModalBody.tsx so there are no
invalid or conflicting focus/placeholder utilities.

Comment on lines +18 to +27
<h3
className={clsx(
"text-base font-semibold",
isWhite
? "dark:text-black"
: "text-black dark:group-hover:text-white",
)}
>
{title}
</h3>

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

Inconsistent light-mode text color for white variant.

When isWhite is true, the <h3> only receives dark:text-black (Line 22), while the <p> below (Line 31) correctly receives text-black for light mode. This asymmetry could cause display issues if the inherited text color differs from black.

🛠️ Proposed fix
         isWhite
-          ? "dark:text-black"
+          ? "text-black dark:text-black"
           : "text-black dark:group-hover:text-white",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<h3
className={clsx(
"text-base font-semibold",
isWhite
? "dark:text-black"
: "text-black dark:group-hover:text-white",
)}
>
{title}
</h3>
<h3
className={clsx(
"text-base font-semibold",
isWhite
? "text-black dark:text-black"
: "text-black dark:group-hover:text-white",
)}
>
{title}
</h3>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/dashboard/_components/common/banner/BannerTitle.tsx
around lines 18 - 27, The h3 in BannerTitle.tsx is missing the light-mode class
when isWhite is true; update the clsx usage in the <h3> element (where title is
rendered and isWhite prop is used) to include "text-black" for the light theme
as well as "dark:text-black" when isWhite is true so its behavior matches the
<p> below and avoids relying on inherited colors.

Comment on lines 36 to 40
{words?.length === 0 ? (
<EmptyState>아직 저장된 표현가 없습니다.</EmptyState>
) : (
<RecordChartView data={chartData} />
<RecordChartView data={chartDataMap[period] ?? []} />
)}

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

Possible typo in Korean text.

Line 37: "표현가" appears to be a typo. The subject marker "이" should be used after words ending in a consonant (표현 ends in ㄴ), so it should likely be "표현이".

📝 Proposed fix
-        <EmptyState>아직 저장된 표현가 없습니다.</EmptyState>
+        <EmptyState>아직 저장된 표현이 없습니다.</EmptyState>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{words?.length === 0 ? (
<EmptyState>아직 저장된 표현가 없습니다.</EmptyState>
) : (
<RecordChartView data={chartData} />
<RecordChartView data={chartDataMap[period] ?? []} />
)}
{words?.length === 0 ? (
<EmptyState>아직 저장된 표현이 없습니다.</EmptyState>
) : (
<RecordChartView data={chartDataMap[period] ?? []} />
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/dashboard/_components/record/RecordChart.tsx around
lines 36 - 40, Typo: change the Korean text inside EmptyState in RecordChart.tsx
from "아직 저장된 표현가 없습니다." to the correct "아직 저장된 표현이 없습니다."—update the string in
the JSX where EmptyState is rendered (the conditional that checks words?.length)
so the displayed message uses "표현이".

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

🧹 Nitpick comments (3)
src/app/(protected)/_components/list/ListContent.tsx (2)

64-64: Remove unnecessary template literal.

Since memo is already a string, the template literal is redundant.

♻️ Suggested simplification
-            value={`${memo}`}
+            value={memo}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/_components/list/ListContent.tsx at line 64, The value
prop in ListContent.tsx is using an unnecessary template literal: replace the
expression value={`${memo}`} with the direct string variable value={memo} to
remove redundancy (look for the JSX element where value={`${memo}`} is used and
update it accordingly).

68-68: Remove commented-out code.

Dead code should be removed rather than commented out. If restoration is needed, git history provides that capability. Additionally, removing tabIndex={-1} improves accessibility by allowing keyboard focus on the read-only textarea.

♻️ Suggested fix
-            // tabIndex={-1}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/_components/list/ListContent.tsx at line 68, Remove the
commented-out dead code "// tabIndex={-1}" from the ListContent component so the
read-only textarea can receive keyboard focus; locate the comment in
ListContent.tsx (inside the JSX for the textarea element in the ListContent
component) and delete that line (and any similar leftover commented tabIndex
usages) rather than leaving it commented out.
src/app/(protected)/record/_components/RecordTopContent.tsx (1)

7-19: Consider extracting props type to a named interface.

The inline type definition works, but extracting it improves readability and enables reuse.

♻️ Optional: Extract props interface
+interface RecordTopContentProps {
+  searchParams: ReadonlyURLSearchParams;
+  pathname: string;
+  keyword: string;
+  activeFilterKey: FilterKey;
+  totalCount: number;
+}
+
-export default function RecordTopContent({
-  searchParams,
-  pathname,
-  keyword,
-  activeFilterKey,
-  totalCount,
-}: {
-  searchParams: ReadonlyURLSearchParams;
-  pathname: string;
-  keyword: string;
-  activeFilterKey: FilterKey;
-  totalCount: number;
-}) {
+export default function RecordTopContent({
+  searchParams,
+  pathname,
+  keyword,
+  activeFilterKey,
+  totalCount,
+}: RecordTopContentProps) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/record/_components/RecordTopContent.tsx around lines 7 -
19, Extract the inline props type for the RecordTopContent function into a named
interface (e.g., RecordTopContentProps or Props) and update the RecordTopContent
signature to use that interface; specifically move the type block containing
searchParams, pathname, keyword, activeFilterKey, and totalCount into a
top-level interface declaration and replace the inline type annotation on the
RecordTopContent parameter with the new interface name to improve readability
and enable reuse.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/`(protected)/_components/list/ListContent.tsx:
- Line 64: The value prop in ListContent.tsx is using an unnecessary template
literal: replace the expression value={`${memo}`} with the direct string
variable value={memo} to remove redundancy (look for the JSX element where
value={`${memo}`} is used and update it accordingly).
- Line 68: Remove the commented-out dead code "// tabIndex={-1}" from the
ListContent component so the read-only textarea can receive keyboard focus;
locate the comment in ListContent.tsx (inside the JSX for the textarea element
in the ListContent component) and delete that line (and any similar leftover
commented tabIndex usages) rather than leaving it commented out.

In `@src/app/`(protected)/record/_components/RecordTopContent.tsx:
- Around line 7-19: Extract the inline props type for the RecordTopContent
function into a named interface (e.g., RecordTopContentProps or Props) and
update the RecordTopContent signature to use that interface; specifically move
the type block containing searchParams, pathname, keyword, activeFilterKey, and
totalCount into a top-level interface declaration and replace the inline type
annotation on the RecordTopContent parameter with the new interface name to
improve readability and enable reuse.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb5ebd1e-f801-4cd4-b568-6ca482aebdb8

📥 Commits

Reviewing files that changed from the base of the PR and between 7d0387c and ecb8578.

📒 Files selected for processing (14)
  • src/api/types/errors.ts
  • src/app/(protected)/_components/add-word-button/AddWordButton.tsx
  • src/app/(protected)/_components/layout/Header.tsx
  • src/app/(protected)/_components/layout/Header/NavBtn.tsx
  • src/app/(protected)/_components/list/List.tsx
  • src/app/(protected)/_components/list/ListContent.tsx
  • src/app/(protected)/_components/modal/word/WordModal.tsx
  • src/app/(protected)/dashboard/_components/RecentWords.tsx
  • src/app/(protected)/dashboard/_components/common/Title.tsx
  • src/app/(protected)/record/_components/FilterCount.tsx
  • src/app/(protected)/record/_components/Pagination.tsx
  • src/app/(protected)/record/_components/Record.tsx
  • src/app/(protected)/record/_components/RecordHeader.tsx
  • src/app/(protected)/record/_components/RecordTopContent.tsx
✅ Files skipped from review due to trivial changes (5)
  • src/app/(protected)/_components/add-word-button/AddWordButton.tsx
  • src/app/(protected)/record/_components/FilterCount.tsx
  • src/api/types/errors.ts
  • src/app/(protected)/_components/list/List.tsx
  • src/app/(protected)/dashboard/_components/common/Title.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/(protected)/_components/layout/Header/NavBtn.tsx
  • src/app/(protected)/dashboard/_components/RecentWords.tsx
  • src/app/(protected)/_components/layout/Header.tsx

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

🧹 Nitpick comments (1)
src/app/(protected)/_components/list/ListContent.tsx (1)

65-70: Clean up redundant code and commented-out line.

  • Line 65: The template literal is unnecessary; value={memo} is cleaner.
  • Line 69: Remove commented-out tabIndex rather than leaving dead code.
♻️ Suggested cleanup
          <textarea
            ref={textareaRef}
-            value={`${memo}`}
+            value={memo}
            name="memo"
            id={`memo-${expression}`}
            readOnly
-            // tabIndex={-1}
-            className="font-medium text-sm md:text-md lg:text-lg  mt-5 resize-none w-full overflow-hidden focus-visible:outline-none"
+            className="font-medium text-sm md:text-md lg:text-lg mt-5 resize-none w-full overflow-hidden focus-visible:outline-none dark:text-white"
          />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(protected)/_components/list/ListContent.tsx around lines 65 - 70,
Replace the redundant template literal and remove the dead commented code in the
ListContent component: change the input/textarea prop from value={`${memo}`} to
value={memo} and delete the commented-out tabIndex line (the element with id
`memo-${expression}` and name "memo" in ListContent.tsx). Ensure no other
formatting or behavior changes are introduced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/`(protected)/_components/list/ListContent.tsx:
- Around line 65-70: Replace the redundant template literal and remove the dead
commented code in the ListContent component: change the input/textarea prop from
value={`${memo}`} to value={memo} and delete the commented-out tabIndex line
(the element with id `memo-${expression}` and name "memo" in ListContent.tsx).
Ensure no other formatting or behavior changes are introduced.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7608ba76-98f2-4442-889b-3d632e97c625

📥 Commits

Reviewing files that changed from the base of the PR and between ecb8578 and 83d772f.

📒 Files selected for processing (1)
  • src/app/(protected)/_components/list/ListContent.tsx

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 18-26: The README's section structure is wrong: the landing page
image and its description (./public/readme/landing.png and the Next.js
description) are placed under the "### 로그인" heading instead of under "###
랜딩페이지"; move the landing image and its descriptive bullet so they are directly
beneath the "### 랜딩페이지" heading and ensure the "### 로그인" section only contains
login-related content (e.g., ./public/readme/login.png), preserving headings and
formatting.
- Line 24: The README sentence "Next.js로  초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을
명확히 구분" contains a double space between "Next.js로" and "초기"; update that string
(the sentence in README.md) to use a single space so it reads "Next.js로 초기 로딩
성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분".
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d05f6f3-e512-4a94-a0fa-19c60aaae09d

📥 Commits

Reviewing files that changed from the base of the PR and between 83d772f and cffa357.

⛔ Files ignored due to path filters (12)
  • public/readme/add-word.jpeg is excluded by !**/*.jpeg
  • public/readme/add-word.png is excluded by !**/*.png
  • public/readme/dashboard-2.jpeg is excluded by !**/*.jpeg
  • public/readme/dashboard-2.png is excluded by !**/*.png
  • public/readme/dashboard.jpeg is excluded by !**/*.jpeg
  • public/readme/dashboard.png is excluded by !**/*.png
  • public/readme/landing.png is excluded by !**/*.png
  • public/readme/login.jpeg is excluded by !**/*.jpeg
  • public/readme/login.png is excluded by !**/*.png
  • public/readme/words.jpeg is excluded by !**/*.jpeg
  • public/readme/words.png is excluded by !**/*.png
  • src/app/favicon.ico is excluded by !**/*.ico
📒 Files selected for processing (1)
  • README.md

Comment thread README.md
Comment on lines +18 to +26
### 랜딩페이지

### 로그인

![로그인](./public/readme/login.jpeg)
![랜딩페이지](./public/readme/landing.png)

- Next.js로 초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분

![로그인](./public/readme/login.png)

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

Fix documentation structure: landing page content is misplaced.

The landing page image (line 22) and description (lines 24-25) appear under the "로그인" heading instead of the "랜딩페이지" heading. This creates confusing documentation where the landing page section is empty and its content incorrectly appears in the login section.

📝 Proposed fix to correct the section structure
 ### 랜딩페이지
 
+![랜딩페이지](./public/readme/landing.png)
+
+    - Next.js로  초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분
+
 ### 로그인
 
-![랜딩페이지](./public/readme/landing.png)
-
-    - Next.js로  초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분
-
 ![로그인](./public/readme/login.png)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### 랜딩페이지
### 로그인
![로그인](./public/readme/login.jpeg)
![랜딩페이지](./public/readme/landing.png)
- Next.js로 초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분
![로그인](./public/readme/login.png)
### 랜딩페이지
![랜딩페이지](./public/readme/landing.png)
- Next.js로 초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분
### 로그인
![로그인](./public/readme/login.png)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 18 - 26, The README's section structure is wrong: the
landing page image and its description (./public/readme/landing.png and the
Next.js description) are placed under the "### 로그인" heading instead of under
"### 랜딩페이지"; move the landing image and its descriptive bullet so they are
directly beneath the "### 랜딩페이지" heading and ensure the "### 로그인" section only
contains login-related content (e.g., ./public/readme/login.png), preserving
headings and formatting.

Comment thread README.md
![로그인](./public/readme/login.jpeg)
![랜딩페이지](./public/readme/landing.png)

- Next.js로 초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분

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

Fix extra whitespace.

"Next.js로 초기" contains a double space.

✏️ Proposed fix
-    - Next.js로  초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분
+    - Next.js로 초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Next.js로 초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분
- Next.js로 초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 24, The README sentence "Next.js로  초기 로딩 성능과 SEO를 개선하고,
서비스 소개와 앱 영역의 역할을 명확히 구분" contains a double space between "Next.js로" and "초기";
update that string (the sentence in README.md) to use a single space so it reads
"Next.js로 초기 로딩 성능과 SEO를 개선하고, 서비스 소개와 앱 영역의 역할을 명확히 구분".

@goodaseul goodaseul merged commit ab45a8f into main Mar 27, 2026
4 checks passed
@goodaseul goodaseul deleted the dev branch March 27, 2026 12:44
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