[Feat] 그룹 리스트 컴포넌트 UI 구현#138
Conversation
- 그룹 리스트 가로로 배치 - 아이템 개수가 늘어나면 스크롤 가능하도록 함 - 선택된 그룹을 제외하고 오버레이로 흐리게 표시
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds a Groups management feature: GroupTab and GroupList UI, a Zod-validated React Query hook for groups, Storybook stories, global scrollbar styling, a Chromatic npm script and devDependency, a Chromatic GitHub Actions workflow, and CLAUDE.md updates for pnpm and the /groups route. ChangesGroups Management UI
Sequence DiagramsequenceDiagram
participant GroupsPage
participant GroupTab
participant GroupList
participant useSuspenseGroupListQuery
participant API
GroupsPage->>useSuspenseGroupListQuery: mount / request group list (groupKeys.list)
useSuspenseGroupListQuery->>API: GET /groups (validated by Zod)
API-->>useSuspenseGroupListQuery: groups array
useSuspenseGroupListQuery->>GroupsPage: return cached/validated data
GroupsPage->>GroupTab: pass activeTab, onTabChange
GroupTab-->>GroupsPage: onTabChange(tab)
GroupsPage->>GroupList: pass filtered groups and selectedId
GroupList-->>GroupsPage: onSelect(groupId)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements the foundation for the Group Reflection feature, including a new groups page, UI components for tabs and lists, and a data fetching hook. Additionally, it updates repository documentation to use pnpm and adds Chromatic for visual testing. The review identifies a security vulnerability involving a hardcoded token in package.json, a potential runtime error in the groups page when handling empty data, and an improvement for type safety in the Zod schema.
| "storybook": "storybook dev -p 6006", | ||
| "build-storybook": "storybook build" | ||
| "build-storybook": "storybook build", | ||
| "chromatic": "npx chromatic --project-token=chpt_4fbd06fd5628810" |
There was a problem hiding this comment.
Chromatic 프로젝트 토큰이 package.json에 하드코딩되어 있습니다. 이는 민감한 정보가 소스 코드에 노출되는 심각한 보안 취약점입니다. 이 파일이 공개 저장소에 푸시되면 누구나 토큰을 사용하여 Chromatic 프로젝트에 접근할 수 있습니다.
이 토큰은 GitHub Actions Secrets과 같은 보안 저장소에 환경 변수로 저장하고, CI 워크플로우에서 secrets.CHROMATIC_PROJECT_TOKEN과 같이 참조하여 사용해야 합니다.
스크립트에서는 환경 변수를 사용하도록 수정해주세요.
| "chromatic": "npx chromatic --project-token=chpt_4fbd06fd5628810" | |
| "chromatic": "npx chromatic --project-token=$CHROMATIC_PROJECT_TOKEN" |
There was a problem hiding this comment.
이거 토큰 새로운걸로 바로 reset 했습니다!! 죄송합니다🙇♀️
|
|
||
| const GroupsPage = () => { | ||
| const [activeTab, setActiveTab] = useState<TabType>('friend'); | ||
| const [selectedId, setSelectedId] = useState(MOCK_FRIENDS_GROUPS[0].id); |
There was a problem hiding this comment.
MOCK_FRIENDS_GROUPS 배열이 비어있을 경우, MOCK_FRIENDS_GROUPS[0].id에 접근 시 런타임 에러가 발생하여 페이지가 크래시될 수 있습니다. Mock 데이터라 할지라도, 실제 API 연동 시 데이터가 없는 경우를 대비하여 안전하게 처리하는 것이 좋습니다. 옵셔널 체이닝(?.)과 nullish coalescing operator(??)를 사용하여 초기값을 안전하게 설정할 수 있습니다.
selectedId의 타입이 string으로 유지되어야 GroupList의 onSelect prop과 타입이 일치하므로, fallback 값으로 빈 문자열 등을 제공하는 것을 고려해볼 수 있습니다.
| const [selectedId, setSelectedId] = useState(MOCK_FRIENDS_GROUPS[0].id); | |
| const [selectedId, setSelectedId] = useState(MOCK_FRIENDS_GROUPS[0]?.id ?? ''); |
| id: z.string(), | ||
| code: z.string(), | ||
| name: z.string(), | ||
| type: z.string(), |
There was a problem hiding this comment.
ResponseSchema의 type 필드가 z.string()으로 관대하게 정의되어 있습니다. src/components/features/groups/GroupList/GroupList.tsx 에서는 이 type을 'friend' | 'character'로 제한하여 사용하고 있습니다. Zod의 enum을 사용하여 API 응답의 type 값이 예상된 값 중 하나인지 명확하게 검증하는 것이 좋습니다. 이렇게 하면 타입 안정성을 높이고 예기치 않은 데이터에 대한 방어적인 코드를 작성할 수 있습니다.
| type: z.string(), | |
| type: z.enum(['friend', 'character']), |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
src/components/features/groups/GroupTab/GroupTab.stories.tsx (1)
22-28: ⚡ Quick winWire
argsintorenderto keep controls/docs consistent.
Defaultdeclaresargsbutrendercurrently ignores them, so controls foractiveTabare not actually reflected in the rendered state. Initialize state from args to keep story behavior aligned with controls.Suggested diff
export const Default: Story = { - render: () => { - const [activeTab, setActiveTab] = useState<TabType>('friend'); + render: (args) => { + const [activeTab, setActiveTab] = useState<TabType>(args.activeTab); return <GroupTab activeTab={activeTab} onTabChange={setActiveTab} />; }, args: { activeTab: 'friend' as const, onTabChange: () => {} }, };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/features/groups/GroupTab/GroupTab.stories.tsx` around lines 22 - 28, The story's render ignores Storybook args so controls don't affect the rendered component; update Default.render to accept args and initialize local state from args.activeTab (e.g., const [activeTab, setActiveTab] = useState<TabType>(args.activeTab)) and pass args through to GroupTab while overriding onTabChange with setActiveTab (e.g., <GroupTab {...args} activeTab={activeTab} onTabChange={setActiveTab} />) so the story's controls and docs remain consistent with Default.args.src/styles/globals.css (1)
149-156: 💤 Low valueConsider adding thumb border/background-clip for visual consistency.
The horizontal scrollbar thumb lacks the
borderandbackground-clip: content-boxproperties present in the vertical version (lines 131-132). This creates visual inconsistency between horizontal and vertical scrollbars.♻️ Proposed adjustment for consistency
.scrollbar-x-transparent-track::-webkit-scrollbar-thumb { background-color: color-mix( in srgb, var(--color-g-0) 25%, transparent ); border-radius: 999px; + border: 1px solid transparent; + background-clip: content-box; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/styles/globals.css` around lines 149 - 156, The horizontal scrollbar thumb selector .scrollbar-x-transparent-track::-webkit-scrollbar-thumb is missing the border and background-clip: content-box properties that the vertical thumb uses; update that selector to include a matching border (same width and color used in the vertical thumb) and add background-clip: content-box so the color-mix background renders consistently with .scrollbar-y-transparent-track::-webkit-scrollbar-thumb.src/components/features/groups/GroupList/GroupList.tsx (1)
37-40: ⚡ Quick win
rounded-[10px]대신 디자인 토큰 기반 radius 유틸을 사용해 주세요.하드코딩된 arbitrary value보다 토큰 클래스(예:
rounded-*)로 맞추는 편이 일관성과 유지보수에 유리합니다.As per coding guidelines
src/**/*.{ts,tsx}: "Use only design tokens from src/styles/globals.css (@themeblock) and src/styles/typography.css for colors, typography, and spacing instead of arbitrary values".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/features/groups/GroupList/GroupList.tsx` around lines 37 - 40, The component uses a hardcoded radius class "rounded-[10px]" in the GroupList rendering (the element with className="rounded-[10px]" and the overlay div shown when !isSelected); replace both occurrences with the appropriate design-token radius utility (e.g., rounded-sm/rounded-md/rounded-lg as defined in src/styles/globals.css `@theme`) so the component uses the project's radius token instead of an arbitrary value, and confirm the chosen token exists in the theme and visually matches the previous 10px radius.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/groups/page.tsx`:
- Around line 31-38: When switching tabs the previous selectedId is retained and
can point to an invalid ID for the new list; add logic in the component that
watches activeTab (e.g., a useEffect) and calls setSelectedId to the new tab's
first item's id or undefined when activeTab changes. Use the same arrays used
for GroupList (MOCK_FRIENDS_GROUPS and MOCK_CHARACTERS_GROUPS) to determine the
first id, and ensure you handle empty arrays by setting selectedId to undefined;
update only in response to activeTab changes so GroupTab/GroupList props remain
unchanged.
In `@package.json`:
- Line 15: The package.json "chromatic" script currently exposes a hardcoded
project token; update the "chromatic" script (referencing the "chromatic" npm
script name) to avoid embedding the token by reading it from an environment
variable (e.g., use CHROMATIC_PROJECT_TOKEN) or remove the script entirely and
rely on your CI's secrets (secrets.CHROMATIC_PROJECT_TOKEN) for publishes;
ensure local developers are instructed to set CHROMATIC_PROJECT_TOKEN in their
.env.local for local runs if you keep the script.
In `@src/components/features/groups/GroupList/GroupList.tsx`:
- Around line 26-30: The clickable item is currently a div which prevents native
keyboard activation and harms accessibility; replace the outer div in
GroupList.tsx with a semantic button element (keeping key={item.id}, className,
and the onClick={() => onSelect?.(item.id)} handler), add type="button" to avoid
form submission behavior, and remove any non-button ARIA/role hacks so the
element gains native keyboard focus and activation.
In `@src/components/features/groups/GroupTab/GroupTab.tsx`:
- Around line 19-21: The button elements rendered in GroupTab (the <button
key={tab.value} onClick={() => onTabChange(tab.value)} /> instances) lack an
explicit type, so they default to type="submit" and may submit enclosing forms;
update the JSX for these buttons to include type="button" to prevent accidental
form submissions when clicking tabs (locate the button rendering in GroupTab.tsx
near the onTabChange usage and add the type="button" attribute).
In `@src/components/features/groups/queries/useGroupListQuery.ts`:
- Around line 24-28: Add and export a suspense variant of the existing hook so
it matches the project's "standard + suspense" query hook pattern: create and
export useGroupListQuerySuspense which calls useQuery with the same queryKey
(groupKeys.list()) and queryFn (fetchGroups) but passes { suspense: true } (and
any other shared options) so the suspense behavior is enabled; keep the original
useGroupListQuery unchanged and ensure the new hook name follows the
useGroupListQuerySuspense convention.
- Around line 14-16: The schema currently uses z.string() for the group "type"
which permits any string; update the Zod schema in useGroupListQuery.ts to use
z.enum(['friend','character']) (or z.union of literals) for the type field so it
only accepts 'friend' or 'character', and then export both the normal and
suspense variants of the query hook (e.g., export useGroupListQuery and export
useGroupListQuerySuspense) following the existing project hook pattern so
consumers can import either the standard or suspense-enabled hook.
---
Nitpick comments:
In `@src/components/features/groups/GroupList/GroupList.tsx`:
- Around line 37-40: The component uses a hardcoded radius class
"rounded-[10px]" in the GroupList rendering (the element with
className="rounded-[10px]" and the overlay div shown when !isSelected); replace
both occurrences with the appropriate design-token radius utility (e.g.,
rounded-sm/rounded-md/rounded-lg as defined in src/styles/globals.css `@theme`) so
the component uses the project's radius token instead of an arbitrary value, and
confirm the chosen token exists in the theme and visually matches the previous
10px radius.
In `@src/components/features/groups/GroupTab/GroupTab.stories.tsx`:
- Around line 22-28: The story's render ignores Storybook args so controls don't
affect the rendered component; update Default.render to accept args and
initialize local state from args.activeTab (e.g., const [activeTab,
setActiveTab] = useState<TabType>(args.activeTab)) and pass args through to
GroupTab while overriding onTabChange with setActiveTab (e.g., <GroupTab
{...args} activeTab={activeTab} onTabChange={setActiveTab} />) so the story's
controls and docs remain consistent with Default.args.
In `@src/styles/globals.css`:
- Around line 149-156: The horizontal scrollbar thumb selector
.scrollbar-x-transparent-track::-webkit-scrollbar-thumb is missing the border
and background-clip: content-box properties that the vertical thumb uses; update
that selector to include a matching border (same width and color used in the
vertical thumb) and add background-clip: content-box so the color-mix background
renders consistently with
.scrollbar-y-transparent-track::-webkit-scrollbar-thumb.
🪄 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: 8f60a052-7acc-474b-862c-a004319918bf
⛔ Files ignored due to path filters (2)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/images/default-group.svgis excluded by!**/*.svg
📒 Files selected for processing (12)
.github/workflows/chromatic.ymlCLAUDE.mdapp/groups/page.tsxpackage.jsonsrc/components/features/groups/GroupList/GroupList.stories.tsxsrc/components/features/groups/GroupList/GroupList.tsxsrc/components/features/groups/GroupTab/GroupTab.stories.tsxsrc/components/features/groups/GroupTab/GroupTab.tsxsrc/components/features/groups/constants/queryKey.tssrc/components/features/groups/constants/url.tssrc/components/features/groups/queries/useGroupListQuery.tssrc/styles/globals.css
| <GroupTab activeTab={activeTab} onTabChange={setActiveTab} /> | ||
| <GroupList | ||
| groups={ | ||
| activeTab === 'friend' ? MOCK_FRIENDS_GROUPS : MOCK_CHARACTERS_GROUPS | ||
| } | ||
| selectedId={selectedId} | ||
| onSelect={setSelectedId} | ||
| /> |
There was a problem hiding this comment.
탭 전환 시 selectedId를 해당 탭의 첫 항목(또는 undefined)으로 동기화해 주세요.
지금은 이전 탭의 선택값이 유지되어, 실제 데이터 ID가 달라지면 전환 직후 선택 상태가 깨질 수 있습니다.
Proposed fix
- <GroupTab activeTab={activeTab} onTabChange={setActiveTab} />
+ <GroupTab
+ activeTab={activeTab}
+ onTabChange={(nextTab) => {
+ setActiveTab(nextTab);
+ const nextGroups =
+ nextTab === 'friend' ? MOCK_FRIENDS_GROUPS : MOCK_CHARACTERS_GROUPS;
+ setSelectedId(nextGroups[0]?.id);
+ }}
+ />🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/groups/page.tsx` around lines 31 - 38, When switching tabs the previous
selectedId is retained and can point to an invalid ID for the new list; add
logic in the component that watches activeTab (e.g., a useEffect) and calls
setSelectedId to the new tab's first item's id or undefined when activeTab
changes. Use the same arrays used for GroupList (MOCK_FRIENDS_GROUPS and
MOCK_CHARACTERS_GROUPS) to determine the first id, and ensure you handle empty
arrays by setting selectedId to undefined; update only in response to activeTab
changes so GroupTab/GroupList props remain unchanged.
| type: z.string(), | ||
| image: z.string(), | ||
| }), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/components/features/groups/queries/useGroupListQuery.tsRepository: dnd-side-project/dnd-14th-5-frontend
Length of output: 925
🏁 Script executed:
# Search for other query hooks to see the suspense pattern
fd -t f "queries.*\.ts$" src/components/features --exec head -30 {} \; | head -100Repository: dnd-side-project/dnd-14th-5-frontend
Length of output: 62
🏁 Script executed:
# Look for any type field usage in the groups feature
rg "type.*friend|character" src/components/features/groups -A 2 -B 2Repository: dnd-side-project/dnd-14th-5-frontend
Length of output: 2718
🏁 Script executed:
# Check for z.enum patterns in other query schemas
rg "z\.enum" src/components/features -A 1 -B 1 | head -50Repository: dnd-side-project/dnd-14th-5-frontend
Length of output: 2178
🏁 Script executed:
# Check if suspense variants exist in other query hooks
rg "Suspense" src/components/features/*/queries/ -t tsRepository: dnd-side-project/dnd-14th-5-frontend
Length of output: 1640
🏁 Script executed:
# Look for any hook exported as both standard and suspense versions
rg "export.*Query.*Suspense|useSuspense" src/components/features --type ts | head -20Repository: dnd-side-project/dnd-14th-5-frontend
Length of output: 2357
🏁 Script executed:
# Check the structure of other query hooks to see if they export suspense variants
fd -t f "queries.*\.ts$" src/components/features -exec grep -l "export" {} \; | head -10 | xargs -I {} bash -c 'echo "=== {} ===" && tail -10 {}'Repository: dnd-side-project/dnd-14th-5-frontend
Length of output: 281
Add enum validation for type field and implement suspense hook variant
The type field should validate against specific enum values ('friend' | 'character') rather than accepting arbitrary strings. Additionally, the query hook pattern requires exporting both standard and suspense versions.
Proposed changes
+import { useSuspenseQuery } from '@tanstack/react-query';
+
-const ResponseSchema = z.array(
- z.object({
- id: z.string(),
- code: z.string(),
- name: z.string(),
- type: z.string(),
+const ResponseSchema = z.array(
+ z.object({
+ id: z.string(),
+ code: z.string(),
+ name: z.string(),
+ type: z.enum(['friend', 'character']),
image: z.string(),
}),
);
export const useGroupListQuery = () =>
useQuery({
queryKey: groupKeys.list(),
queryFn: fetchGroups,
});
+
+export const useSuspenseGroupListQuery = () =>
+ useSuspenseQuery({
+ queryKey: groupKeys.list(),
+ queryFn: fetchGroups,
+ });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/features/groups/queries/useGroupListQuery.ts` around lines 14
- 16, The schema currently uses z.string() for the group "type" which permits
any string; update the Zod schema in useGroupListQuery.ts to use
z.enum(['friend','character']) (or z.union of literals) for the type field so it
only accepts 'friend' or 'character', and then export both the normal and
suspense variants of the query hook (e.g., export useGroupListQuery and export
useGroupListQuerySuspense) following the existing project hook pattern so
consumers can import either the standard or suspense-enabled hook.
| export const useGroupListQuery = () => | ||
| useQuery({ | ||
| queryKey: groupKeys.list(), | ||
| queryFn: fetchGroups, | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
쿼리 훅 패턴에 맞춰 suspense 버전도 함께 export해 주세요.
현재 표준 훅만 있어, 팀 규약(standard + suspense 동시 제공)과 불일치합니다.
As per coding guidelines src/components/features/*/queries/*.{ts,tsx}: "Implement query hook pattern: define responseSchema with Zod, create fetch function, and export both standard and suspense versions of the hook".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/features/groups/queries/useGroupListQuery.ts` around lines 24
- 28, Add and export a suspense variant of the existing hook so it matches the
project's "standard + suspense" query hook pattern: create and export
useGroupListQuerySuspense which calls useQuery with the same queryKey
(groupKeys.list()) and queryFn (fetchGroups) but passes { suspense: true } (and
any other shared options) so the suspense behavior is enabled; keep the original
useGroupListQuery unchanged and ensure the new hook name follows the
useGroupListQuerySuspense convention.
baegyeong
left a comment
There was a problem hiding this comment.
스토리북 배포 해주셔서 감사해요! 작업하시느라 고생 많으셨습니다 👍👍
백엔드와 그룹 이름 글자수를 몇 자로 제한할 지 논의해도 좋을 것 같아요..! 그룹 이름이 차지하는 칸이 작다보니 제한하는 게 좋을 것 같습니다!
|
넵 좋아요 일단은 줄바꿈 되도록 처리 해놓긴 했는데 아예 그룹 이름 글자 수를 제한 하는 것도 좋을 것 같아요 👍 |
There was a problem hiding this comment.
♻️ Duplicate comments (2)
src/components/features/groups/queries/useGroupListQuery.ts (2)
1-1: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winExport a regular query hook in addition to the suspense hook.
This file only exports the suspense variant, which breaks the project query-hook contract for reusable consumers.
🔧 Proposed fix
-import { useSuspenseQuery } from '`@tanstack/react-query`'; +import { useQuery, useSuspenseQuery } from '`@tanstack/react-query`'; @@ +export const useGroupListQuery = () => + useQuery({ + queryKey: groupKeys.list(), + queryFn: fetchGroups, + }); + export const useSuspenseGroupListQuery = () => useSuspenseQuery({ queryKey: groupKeys.list(), queryFn: fetchGroups, });As per coding guidelines
src/components/features/*/queries/**/*.{ts,tsx}: "Implement query hooks withuseQueryanduseSuspenseQuery… then export both regular and suspense variants".Also applies to: 24-28
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/features/groups/queries/useGroupListQuery.ts` at line 1, The file only provides the suspense variant; add and export a non-suspense counterpart that uses useQuery so consumers can call the regular hook. Create a new exported hook (e.g., useGroupListQuery) that wraps the same query key and fetcher currently used by the suspense hook but calls useQuery instead of useSuspenseQuery, keep the existing exported suspense hook (e.g., useGroupListQuerySuspense or existing default) unchanged, and ensure both hooks share the same query key/fetch function and typings so they remain interchangeable for consumers.
14-14:⚠️ Potential issue | 🟠 Major | ⚡ Quick winTighten
typevalidation to allowed variants only.Line 14 currently accepts any string, so invalid server values pass validation and can break downstream group-type logic.
🔧 Proposed fix
- type: z.string(), + type: z.enum(['friend', 'character']),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/features/groups/queries/useGroupListQuery.ts` at line 14, The `type: z.string()` validator in useGroupListQuery is too permissive; replace it with a strict validator that only allows the server-known group type variants (e.g. use z.enum([...allowedVariants...]) or z.union of z.literal(...) values) so invalid values fail validation; update the schema entry for the `type` field inside the useGroupListQuery schema (where `type` is currently defined) to use z.enum or z.literal variants that exactly match the server-side group type values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@src/components/features/groups/queries/useGroupListQuery.ts`:
- Line 1: The file only provides the suspense variant; add and export a
non-suspense counterpart that uses useQuery so consumers can call the regular
hook. Create a new exported hook (e.g., useGroupListQuery) that wraps the same
query key and fetcher currently used by the suspense hook but calls useQuery
instead of useSuspenseQuery, keep the existing exported suspense hook (e.g.,
useGroupListQuerySuspense or existing default) unchanged, and ensure both hooks
share the same query key/fetch function and typings so they remain
interchangeable for consumers.
- Line 14: The `type: z.string()` validator in useGroupListQuery is too
permissive; replace it with a strict validator that only allows the server-known
group type variants (e.g. use z.enum([...allowedVariants...]) or z.union of
z.literal(...) values) so invalid values fail validation; update the schema
entry for the `type` field inside the useGroupListQuery schema (where `type` is
currently defined) to use z.enum or z.literal variants that exactly match the
server-side group type values.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1ba6a6f0-14a1-49bc-a3ee-2c4829be0201
📒 Files selected for processing (6)
app/groups/page.tsxpackage.jsonsrc/components/features/groups/GroupList/GroupList.stories.tsxsrc/components/features/groups/GroupList/GroupList.tsxsrc/components/features/groups/GroupTab/GroupTab.tsxsrc/components/features/groups/queries/useGroupListQuery.ts
✅ Files skipped from review due to trivial changes (1)
- src/components/features/groups/GroupList/GroupList.stories.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- src/components/features/groups/GroupTab/GroupTab.tsx
- app/groups/page.tsx
- package.json
What is this PR? 🔍
그룹 회고 페이지의 그룹 리스트 컴포넌트를 구현.
Changes 📝
GroupList컴포넌트 구현 (가로 스크롤, 선택 상태 오버레이)GroupTab컴포넌트 구현 (친구/캐릭터 탭 전환)useGroupListQuery)스토리북이 배포되어 있으면 UI 리뷰가 편할 것 같아서 Chromatic으로 배포했습니다.

PR을 열면 GitHub Actions가 자동으로 Storybook을 빌드해 Chromatic에 배포합니다.
변경된 UI는 PR 하단 Checks 탭의 Chromatic 항목에서 확인할 수 있습니다.
main에 머지되면 아래 링크에도 자동으로 반영됩니다.
🔗 배포된 Storybook: https://6a0747043a4480833f4697f4-nyrdwztrox.chromatic.com
추가 참고사항
Related Issues 🔗
Screenshot 📷
Check List
Summary by CodeRabbit
Release Notes
New Features
Style
Documentation
Chores