[Refactor/#151] 워크스페이스 UI/UX 개선 - #152
Conversation
📝 WalkthroughWalkthrough작업 공간 UI를 재구성하는 변경사항으로, 드롭다운 메뉴 기능을 제거하고 라우팅 구조를 부모-자식 관계로 개선하며, 로딩 상태 UI를 스켈레톤 리스트로 변경하고 버튼 및 드롭다운 메뉴의 스타일을 업데이트했습니다. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
주요 검토 포인트1. 라우팅 구조 변경의 영향 범위 확인라우팅을 부모-자식 계층으로 개선한 점은 좋으나, 2. WorkspaceCard props 제거의 종속성
3. 접근성 개선 검증
4. 드롭다운 메뉴 스타일 일관성DropdownMenu의 색상 변경(brand-300 → status-blue/10 및 /5)이 디자인 시스템과 일치하는지, 다른 드롭다운 사용 사례에서도 동일하게 적용되어야 하는지 검토하세요. 5. 모달 UI 변경의 반응형 동작Workspace.tsx의 생성 모달을 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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 |
📚 Storybook 배포 완료
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/pages/workspace/Workspace.tsx (1)
97-104:⚠️ Potential issue | 🟡 Minor
setLogoPreview(null)중복 호출 제거
onCloseCreate에서setLogoPreview(null)이 두 번 호출되고 있습니다. 첫 번째 호출(Line 98)은 불필요하며, 두 번째 콜백(Lines 99-102)만 있으면 됩니다.🐛 수정 제안
const onCloseCreate = () => { - setLogoPreview(null); setLogoPreview((prev) => { if (prev) URL.revokeObjectURL(prev); return null; }); setCreateOpen(false); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/workspace/Workspace.tsx` around lines 97 - 104, In onCloseCreate, remove the redundant direct call setLogoPreview(null) and keep only the functional updater that revokes the previous object URL and returns null (the existing setLogoPreview(prev => { if (prev) URL.revokeObjectURL(prev); return null; })); update the onCloseCreate function to call setCreateOpen(false) after that single setLogoPreview call so logo cleanup and closing behavior remain correct.src/components/common/dropdownmenu/DropdownMenu.tsx (1)
39-55:⚠️ Potential issue | 🟡 Minor키보드 접근성을 위해
tabIndex추가 필요
role="button"이 설정되어 있고 키보드 이벤트도 처리하고 있지만,tabIndex={0}이 없으면 키보드로 포커스할 수 없습니다.🔧 수정 제안
<div role="button" + tabIndex={0} aria-haspopup="menu" aria-expanded={open} aria-controls={menuId}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/dropdownmenu/DropdownMenu.tsx` around lines 39 - 55, The div in DropdownMenu.tsx that renders the interactive trigger (role="button", aria-haspopup, aria-expanded using open, aria-controls={menuId}, aria-label={ariaLabel}, onClick calling setOpen and onKeyDown handling Enter/Space) is not focusable by keyboard; add tabIndex={0} to that element so it can receive focus and keyboard events (keep existing handlers and props like className/twMerge and the trigger rendering intact). Ensure tabIndex is applied on the same element that calls setOpen and uses trigger(open).
🧹 Nitpick comments (7)
src/components/common/button/Button.tsx (1)
39-39: 하드코딩된 hover 색상은 토큰 기반으로 맞추는 걸 권장해요.Line 39, Line 45의
hover:bg-[#...]는 테마 확장/일괄 변경 시 추적이 어려워집니다. 현재 토큰 클래스로 통일하거나, 필요한 hover 색을@theme토큰으로 추가해서 참조하는 방식이 유지보수에 더 안전합니다.예시 diff
- primary: - "bg-chart-3 text-white hover:bg-[`#0e6add`] disabled:bg-bg-disabled disabled:text-text-disabled disabled:hover:bg-bg-disabled", + primary: + "bg-chart-3 text-white hover:bg-chart-2 disabled:bg-bg-disabled disabled:text-text-disabled disabled:hover:bg-bg-disabled", - danger: - "bg-status-red text-white hover:bg-[`#d91632`] disabled:bg-bg-disabled disabled:text-text-disabled disabled:hover:bg-bg-disabled", + danger: + "bg-status-red text-white hover:bg-status-red/90 disabled:bg-bg-disabled disabled:text-text-disabled disabled:hover:bg-bg-disabled",Also applies to: 45-45
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/button/Button.tsx` at line 39, The hover:bg hex values in the Button component's class list (the string containing "bg-chart-3 text-white hover:bg-[`#0e6add`] ...") are hardcoded—replace them with theme/token-based classes or CSS variables: add an appropriate hover token to your theme (e.g., chart-3-hover or --color-chart-3-hover) and update the Button.tsx className(s) to use that token (e.g., hover:bg-{token} or hover:bg-[var(--color-chart-3-hover)]), doing the same for the other occurrence referenced in the diff so hover colors are maintainable and theme-driven.src/components/workspace/MemberRoleSelect.tsx (2)
52-52: transition-transform 제거에 따른 UX 영향 확인 필요
transition-transform클래스를 제거하면서 chevron 아이콘의 회전이 즉각적으로 발생하게 됩니다. 일반적으로 드롭다운 UI에서는 부드러운 전환 애니메이션이 사용자 경험 향상에 도움이 됩니다.PR 설명에서 "hover 인터랙션 개선"을 언급하셨는데, 이 변경이 의도적인 것인지 확인이 필요합니다. 만약 의도하지 않은 제거였다면 다시 추가하는 것을 권장합니다.
🔄 transition 복원 제안
<ChevonIcon - className={`h-4 w-4 shrink-0 ${open ? "rotate-0" : "-rotate-180"}`} + className={`h-4 w-4 shrink-0 transition-transform ${open ? "rotate-0" : "-rotate-180"}`} />또는 duration을 조정하여 더 빠른 전환을 원하는 경우:
<ChevonIcon - className={`h-4 w-4 shrink-0 ${open ? "rotate-0" : "-rotate-180"}`} + className={`h-4 w-4 shrink-0 transition-transform duration-150 ${open ? "rotate-0" : "-rotate-180"}`} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/MemberRoleSelect.tsx` at line 52, The chevron in MemberRoleSelect now rotates instantly because the tailwind transition class was removed; restore a smooth rotation by adding the appropriate transition utility back to the element (e.g., include "transition-transform" and optionally a duration like "duration-150" or "duration-200") in the className that currently reads `className={`h-4 w-4 shrink-0 ${open ? "rotate-0" : "-rotate-180"}`}` so the rotate classes animate smoothly; adjust duration if you want a faster/slower effect.
43-43: key prop을 통한 강제 리마운트 패턴 검토
disabled상태 변경 시 컴포넌트를 강제로 리마운트하는 패턴이 적용되어 있습니다. 이는 드롭다운 내부 상태를 완전히 초기화하기 위한 의도로 보이나, 이미disabledprop과 조건부items배열로 충분히 제어되고 있어 불필요한 성능 비용이 발생할 수 있습니다.드롭다운 내부에서 특별히 초기화가 필요한 상태가 없다면, key prop을 제거하고 일반적인 prop 전달 방식으로 변경하는 것을 고려해 보세요.
♻️ 제안하는 리팩토링
<DropdownMenu - key={disabled ? "disabled" : "enabled"} items={items} className={disabled ? "pointer-events-none opacity-50" : undefined} trigger={(open) => (만약 드롭다운이 열린 상태에서 disabled로 전환될 때 닫히도록 하는 것이 목적이라면, DropdownMenu 컴포넌트 내부에서
disabledprop을 받아 처리하는 것이 더 명확한 방법입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/MemberRoleSelect.tsx` at line 43, The component MemberRoleSelect currently forces a remount by using key={disabled ? "disabled" : "enabled"}; remove this key usage and rely on the disabled prop and the conditional items array to control the DropdownMenu instead; if you need the dropdown to close when disabled becomes true, implement that inside DropdownMenu (e.g., have DropdownMenu accept a disabled prop and close on prop change) rather than remounting MemberRoleSelect.src/pages/workspace/Workspace.tsx (1)
134-137:async함수에서await미사용
onSubmitCreate가async로 선언되어 있지만mutate()를 await하지 않아 async 키워드가 불필요합니다. 현재 로직상 mutation 완료를 기다릴 필요가 없다면 async를 제거하고, 완료 후 처리가 필요하다면mutateAsync를 사용하세요.♻️ 옵션 1: async 제거
-const onSubmitCreate = async () => { +const onSubmitCreate = () => { if (!newName.trim()) return; createWorkspaceMutation.mutate(); };♻️ 옵션 2: mutateAsync 사용 (완료 후 처리 필요 시)
const onSubmitCreate = async () => { if (!newName.trim()) return; - createWorkspaceMutation.mutate(); + await createWorkspaceMutation.mutateAsync(); + // 완료 후 추가 처리 };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/workspace/Workspace.tsx` around lines 134 - 137, The onSubmitCreate function is declared async but does not await the mutation; either remove the unnecessary async from onSubmitCreate or, if you need to wait for the mutation to complete, call createWorkspaceMutation.mutateAsync() and await it (e.g., await createWorkspaceMutation.mutateAsync(...)) and handle results/errors accordingly; update any post-mutation logic to run after the await when choosing mutateAsync.src/components/workspace/WorkspaceListLoading.tsx (1)
3-19: 불필요한 Fragment 제거 가능
WorkspaceCardSkeleton의<><li>...</li></>에서 Fragment(<>...</>)는 불필요합니다.<li>단일 요소만 반환하므로 Fragment 없이 직접 반환해도 됩니다.♻️ Fragment 제거
function WorkspaceCardSkeleton() { return ( - <> - <li className="flex items-center justify-between rounded-component-md bg-white px-6 py-5 shadow-Soft border border-gray-100 tablet:px-4 tablet:py-4"> - ... - </li> - </> + <li className="flex items-center justify-between rounded-component-md bg-white px-6 py-5 shadow-Soft border border-gray-100 tablet:px-4 tablet:py-4"> + ... + </li> ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/WorkspaceListLoading.tsx` around lines 3 - 19, The WorkspaceCardSkeleton component currently wraps its single <li> element in a React Fragment; remove the unnecessary fragment so WorkspaceCardSkeleton returns the <li> directly (locate the WorkspaceCardSkeleton function and the surrounding <>...</> and delete the fragment wrappers).src/pages/workspace/MemberManagement.tsx (1)
231-245: 에러 타입 처리 개선 고려Line 234의
as unknown as IApiErrorResponse타입 단언은 타입 안전성을 우회합니다.useInfiniteQuery의 에러 타입을 제네릭으로 명시하면 이 단언을 제거할 수 있습니다.♻️ 타입 단언 제거 제안
const membersQuery = useInfiniteQuery({ + // 에러 타입을 명시적으로 지정 queryKey: ["workspaceMembers", orgId, PAGE_SIZE], ... }); // 또는 에러 메시지 접근 시 옵셔널 체이닝 활용 const errorMessage = memberCountQuery.error?.message || - (membersQuery.error as unknown as IApiErrorResponse)?.message || + membersQuery.error?.message || "팀 구성원 정보를 불러오지 못했습니다";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/workspace/MemberManagement.tsx` around lines 231 - 245, The error handling currently uses a forced cast (as unknown as IApiErrorResponse) for membersQuery.error; update the useInfiniteQuery / useQuery calls that create membersQuery and memberCountQuery to provide the correct generic error type (IApiErrorResponse) so their .error properties are typed properly, then remove the cast and use membersQuery.error?.message and memberCountQuery.error?.message directly; look for the hooks that create membersQuery and memberCountQuery and add the generic parameter(s) to align their error types with IApiErrorResponse.src/layout/workspace/WorkspaceManageLayout.tsx (1)
10-12:workspaceIdundefined 처리 검토
useParams()에서 반환되는workspaceId는string | undefined타입입니다. 현재 라우트 구성상 이 레이아웃은 항상workspace/:workspaceId경로에서만 렌더링되므로 실제로 undefined가 될 가능성은 낮지만, 타입 안전성을 위해 명시적 처리를 고려해 볼 수 있습니다.♻️ 타입 안전성 개선 (선택사항)
export default function WorkspaceManageLayout() { - const { workspaceId } = useParams(); + const { workspaceId } = useParams<{ workspaceId: string }>(); + + if (!workspaceId) { + return null; // 또는 에러 UI + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/layout/workspace/WorkspaceManageLayout.tsx` around lines 10 - 12, The workspaceId from useParams() in WorkspaceManageLayout is typed as string | undefined; make it explicit by checking and handling the undefined case instead of assuming it's present: retrieve workspaceId via useParams(), then either (a) guard early (if (!workspaceId) return a NotFound/Redirect/throw new Error with a clear message) or (b) assert non-null with a deliberate cast/comment (e.g., const workspaceId = params.workspaceId! ) if you want to guarantee presence, and adjust downstream usages to accept a string; update the WorkspaceManageLayout to perform this explicit check or assertion so type-safety is enforced.
🤖 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/utils/loadable.tsx`:
- Around line 5-8: The loadable utility currently defaults fallback to null
causing MainRoutes components to render a blank screen; update the loadable<T
extends ComponentType<any>>(Component: T, fallback: ReactNode = null) function
to use a sensible default fallback (e.g., a shared GenericSkeleton or
LoadingPlaceholder) instead of null, and then for the specific MainRoutes
components (OverviewDashboard, PlatformDashboard, Timeline, AdsListPage,
CampaignDetail, CampaignGroup, WorkspaceManageLayout, WorkspaceSetting,
MemberManagement, Billing) pass their corresponding more specific skeleton
components as the fallback when calling loadable so they show an appropriate
skeleton on slow networks while AuthRoutes can keep their own skeleton
fallbacks. Ensure you reference the loadable function signature and the listed
component names when making the changes.
---
Outside diff comments:
In `@src/components/common/dropdownmenu/DropdownMenu.tsx`:
- Around line 39-55: The div in DropdownMenu.tsx that renders the interactive
trigger (role="button", aria-haspopup, aria-expanded using open,
aria-controls={menuId}, aria-label={ariaLabel}, onClick calling setOpen and
onKeyDown handling Enter/Space) is not focusable by keyboard; add tabIndex={0}
to that element so it can receive focus and keyboard events (keep existing
handlers and props like className/twMerge and the trigger rendering intact).
Ensure tabIndex is applied on the same element that calls setOpen and uses
trigger(open).
In `@src/pages/workspace/Workspace.tsx`:
- Around line 97-104: In onCloseCreate, remove the redundant direct call
setLogoPreview(null) and keep only the functional updater that revokes the
previous object URL and returns null (the existing setLogoPreview(prev => { if
(prev) URL.revokeObjectURL(prev); return null; })); update the onCloseCreate
function to call setCreateOpen(false) after that single setLogoPreview call so
logo cleanup and closing behavior remain correct.
---
Nitpick comments:
In `@src/components/common/button/Button.tsx`:
- Line 39: The hover:bg hex values in the Button component's class list (the
string containing "bg-chart-3 text-white hover:bg-[`#0e6add`] ...") are
hardcoded—replace them with theme/token-based classes or CSS variables: add an
appropriate hover token to your theme (e.g., chart-3-hover or
--color-chart-3-hover) and update the Button.tsx className(s) to use that token
(e.g., hover:bg-{token} or hover:bg-[var(--color-chart-3-hover)]), doing the
same for the other occurrence referenced in the diff so hover colors are
maintainable and theme-driven.
In `@src/components/workspace/MemberRoleSelect.tsx`:
- Line 52: The chevron in MemberRoleSelect now rotates instantly because the
tailwind transition class was removed; restore a smooth rotation by adding the
appropriate transition utility back to the element (e.g., include
"transition-transform" and optionally a duration like "duration-150" or
"duration-200") in the className that currently reads `className={`h-4 w-4
shrink-0 ${open ? "rotate-0" : "-rotate-180"}`}` so the rotate classes animate
smoothly; adjust duration if you want a faster/slower effect.
- Line 43: The component MemberRoleSelect currently forces a remount by using
key={disabled ? "disabled" : "enabled"}; remove this key usage and rely on the
disabled prop and the conditional items array to control the DropdownMenu
instead; if you need the dropdown to close when disabled becomes true, implement
that inside DropdownMenu (e.g., have DropdownMenu accept a disabled prop and
close on prop change) rather than remounting MemberRoleSelect.
In `@src/components/workspace/WorkspaceListLoading.tsx`:
- Around line 3-19: The WorkspaceCardSkeleton component currently wraps its
single <li> element in a React Fragment; remove the unnecessary fragment so
WorkspaceCardSkeleton returns the <li> directly (locate the
WorkspaceCardSkeleton function and the surrounding <>...</> and delete the
fragment wrappers).
In `@src/layout/workspace/WorkspaceManageLayout.tsx`:
- Around line 10-12: The workspaceId from useParams() in WorkspaceManageLayout
is typed as string | undefined; make it explicit by checking and handling the
undefined case instead of assuming it's present: retrieve workspaceId via
useParams(), then either (a) guard early (if (!workspaceId) return a
NotFound/Redirect/throw new Error with a clear message) or (b) assert non-null
with a deliberate cast/comment (e.g., const workspaceId = params.workspaceId! )
if you want to guarantee presence, and adjust downstream usages to accept a
string; update the WorkspaceManageLayout to perform this explicit check or
assertion so type-safety is enforced.
In `@src/pages/workspace/MemberManagement.tsx`:
- Around line 231-245: The error handling currently uses a forced cast (as
unknown as IApiErrorResponse) for membersQuery.error; update the
useInfiniteQuery / useQuery calls that create membersQuery and memberCountQuery
to provide the correct generic error type (IApiErrorResponse) so their .error
properties are typed properly, then remove the cast and use
membersQuery.error?.message and memberCountQuery.error?.message directly; look
for the hooks that create membersQuery and memberCountQuery and add the generic
parameter(s) to align their error types with IApiErrorResponse.
In `@src/pages/workspace/Workspace.tsx`:
- Around line 134-137: The onSubmitCreate function is declared async but does
not await the mutation; either remove the unnecessary async from onSubmitCreate
or, if you need to wait for the mutation to complete, call
createWorkspaceMutation.mutateAsync() and await it (e.g., await
createWorkspaceMutation.mutateAsync(...)) and handle results/errors accordingly;
update any post-mutation logic to run after the await when choosing mutateAsync.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e07e9575-cdf5-4d7e-b5ef-3c8f996e34a5
⛔ Files ignored due to path filters (1)
src/assets/icon/common/uploadImg.svgis excluded by!**/*.svgand included bysrc/**
📒 Files selected for processing (11)
src/components/common/button/Button.tsxsrc/components/common/dropdownmenu/DropdownMenu.tsxsrc/components/workspace/MemberRoleSelect.tsxsrc/components/workspace/WorkspaceCard.tsxsrc/components/workspace/WorkspaceListLoading.tsxsrc/layout/workspace/WorkspaceManageLayout.tsxsrc/pages/workspace/MemberManagement.tsxsrc/pages/workspace/Workspace.tsxsrc/pages/workspace/WorkspaceSetting.tsxsrc/routes/MainRoutes.tsxsrc/utils/loadable.tsx
💤 Files with no reviewable changes (1)
- src/pages/workspace/WorkspaceSetting.tsx
|
P4: 상단 탭 좋은 것 같습니다! 수고하셨습니다 :) |
🚨 관련 이슈
#151
✨ 변경사항
✏️ 작업 내용
스크린샷
😅 미완성 작업
스켈레톤 UI 임시 적용했으니 로딩 중 상태 최적화 부탁드립니다!
📢 논의 사항 및 참고 사항
Summary by CodeRabbit
릴리스 노트
새로운 기능
UI 개선
기타