[Refactor/#127] 워크스페이스 목록/수정 페이지 UI 및 UX 개선 리팩토링 - #135
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough워크스페이스 목록·설정 페이지와 공통 컴포넌트의 Tailwind 클래스 정리 및 반응형 조정이 적용되었고, 워크스페이스 카드에 선택 상태를 나타내는 Changes
Sequence DiagramsequenceDiagram
participant User as User
participant WorkspacePage as WorkspacePage
participant Store as useWorkspaceStore
participant WorkspaceCard as WorkspaceCard
participant SettingPage as WorkspaceSetting
participant API as API
participant QueryClient as ReactQuery
rect rgba(100,150,200,0.5)
Note over User,WorkspacePage: 선택 워크스페이스 우선 정렬 및 표시
User->>WorkspacePage: 페이지 접근
WorkspacePage->>Store: selectedOrgId 조회
Store-->>WorkspacePage: selectedOrgId 반환
WorkspacePage->>WorkspacePage: workspaces 정렬(selectedOrgId 우선)
loop 렌더링
WorkspacePage->>WorkspaceCard: render with isSelected={orgId===selectedOrgId}
WorkspaceCard->>User: 시각적 선택 표시(배지/스타일)
end
end
rect rgba(200,150,100,0.5)
Note over User,SettingPage: 삭제 후 캐시 무효화 흐름
User->>SettingPage: 워크스페이스 삭제 요청
SettingPage->>API: deleteWorkspace(orgId)
API-->>SettingPage: 삭제 응답
SettingPage->>QueryClient: invalidateQueries(["my-workspaces"])
QueryClient->>WorkspacePage: 캐시 무효화(재요청 트리거)
WorkspacePage->>API: workspaces 목록 재요청
API-->>WorkspacePage: 갱신된 목록 반환
WorkspacePage-->>User: 업데이트된 목록 표시
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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.
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/WorkspaceSetting.tsx (1)
79-81:⚠️ Potential issue | 🟡 Minor
useEffect의존성 배열에fetchWorkspaceDetail이 누락되었습니다.
fetchWorkspaceDetail함수가 컴포넌트 내부에서 정의되어 있고useEffect에서 호출되지만, 의존성 배열에 포함되어 있지 않습니다. ESLint의react-hooks/exhaustive-deps규칙에서 경고가 발생할 수 있습니다.
fetchWorkspaceDetail이orgId에만 의존하고,orgId가 변경될 때만 실행되어야 하므로 현재 동작은 의도한 대로이지만, 린터 경고를 피하려면useCallback으로 감싸거나useEffect내부에서 함수를 정의하는 것이 좋습니다.♻️ 수정 제안 (useCallback 사용)
+ import { useCallback } from "react"; ... - const fetchWorkspaceDetail = async () => { + const fetchWorkspaceDetail = useCallback(async () => { if (orgId === null) { setErrorMsg("잘못된 워크스페이스ID 입니다"); return; } // ... rest of function - }; + }, [orgId]); useEffect(() => { void fetchWorkspaceDetail(); - }, [orgId]); + }, [fetchWorkspaceDetail]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/workspace/WorkspaceSetting.tsx` around lines 79 - 81, fetchWorkspaceDetail is defined inside the component but not listed in the useEffect dependency array, which triggers exhaustive-deps warnings; fix by stabilizing fetchWorkspaceDetail with useCallback (e.g., wrap the existing fetchWorkspaceDetail declaration in React.useCallback with orgId in its dependencies) or by moving its definition inside the useEffect so the effect only depends on orgId, ensuring the useEffect(() => { void fetchWorkspaceDetail(); }, [orgId]) call no longer omits a referenced function; update the function declaration named fetchWorkspaceDetail accordingly and keep orgId as the triggering dependency.src/pages/workspace/Workspace.tsx (1)
123-130:⚠️ Potential issue | 🟡 Minor
setLogoPreview(null)중복 호출이 있습니다.Line 124에서 먼저
null로 설정한 후, Line 125-128에서 다시 함수형 업데이트로 이전 URL을 revoke하고null로 설정합니다. 첫 번째 호출은 불필요하며, 두 번째 호출만 있으면 됩니다.🔧 수정 제안
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 123 - 130, The onCloseCreate handler contains a redundant setLogoPreview(null) call; remove the first direct call and keep the functional updater variant so you can revoke the previous object URL safely and then return null. Update the onCloseCreate function (which calls setLogoPreview and setCreateOpen) to only call setLogoPreview(prev => { if (prev) URL.revokeObjectURL(prev); return null; }); then call setCreateOpen(false) as before.
🧹 Nitpick comments (4)
src/components/common/textarea/TextareaField.tsx (1)
28-36:resize함수를useCallback으로 감싸는 것을 고려해 주세요.현재
resize함수는 매 렌더마다 새로 생성됩니다. 함수 자체가 간단하고ref.current접근 방식이라 성능상 큰 문제는 없지만, 코딩 가이드라인에 따라useCallback적용을 권장드립니다.♻️ useCallback 적용 제안
+ import { useLayoutEffect, useRef, useCallback } from "react"; - const resize = () => { - const el = ref.current; - if (!el) return; - el.style.height = "auto"; - el.style.height = `${el.scrollHeight}px`; - }; + const resize = useCallback(() => { + const el = ref.current; + if (!el) return; + el.style.height = "auto"; + el.style.height = `${el.scrollHeight}px`; + }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/textarea/TextareaField.tsx` around lines 28 - 36, Wrap the inline resize function with useCallback to avoid recreating it every render: convert the local resize function into const resize = useCallback(() => { ... }, []), referencing ref.current inside but keeping an empty dependency array since ref is stable; then update the useLayoutEffect to depend on resize (useLayoutEffect(() => { resize(); }, [resize])) instead of directly depending on value. This keeps behavior identical while satisfying the hook-stability guideline for the resize function referenced in useLayoutEffect.src/components/common/dropdownmenu/DropdownMenu.tsx (1)
39-55: 키보드 접근성 개선을 위해tabIndex추가를 권장합니다.
role="button"이 있는 div 요소에 명시적으로tabIndex={0}을 추가하면 키보드 포커스를 받을 수 있어 접근성이 개선됩니다. 일부 브라우저에서는 role="button"만으로도 포커스 가능하지만, 명시적으로 지정하는 것이 더 안정적입니다.♻️ 제안하는 개선 방법
<div role="button" + tabIndex={0} aria-haspopup="menu" aria-expanded={open} aria-controls={menuId}As per coding guidelines, 접근성 영역에서 시맨틱 HTML과 ARIA 속성 사용을 확인하도록 되어 있습니다.
🤖 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 acting as a button in DropdownMenu should explicitly be focusable; add tabIndex={0} to the element that currently has role="button" (the same element using aria-haspopup, aria-expanded={open}, aria-controls={menuId}, aria-label={ariaLabel}, onClick={() => setOpen(v => !v)} and onKeyDown={...}) so it reliably receives keyboard focus across browsers; keep the existing onKeyDown handler (which toggles via Enter/Space) and ensure this change is applied in the DropdownMenu component where trigger(open) is rendered.src/components/workspace/MemberList.tsx (1)
15-84: Mock 데이터가 프로덕션 컴포넌트에 포함되어 있습니다.
mockInviteItems가 컴포넌트 내부에 하드코딩되어 있고,InviteMemberModal에 직접 전달되고 있습니다 (Line 146). 이 PR의 범위 밖일 수 있지만, 실제 API 연동 전까지는 별도 mock 파일로 분리하거나 TODO 주석을 추가하는 것이 좋을 것 같습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/MemberList.tsx` around lines 15 - 84, The component currently contains a hardcoded mock array (mockInviteItems) and passes it directly into InviteMemberModal; extract mockInviteItems out of the component into a dedicated mock module (e.g., a new mocks file) or at minimum add a clear TODO comment above its declaration, then import that mock into MemberList and use the imported value when rendering InviteMemberModal; update references to mockInviteItems in MemberList and ensure the component expects real data props later (preserve the shape/TInviteMemberItem) so swapping to API data is trivial.src/pages/workspace/Workspace.tsx (1)
90-91:selectedOrgId가 삭제된 워크스페이스를 가리킬 수 있는 엣지 케이스를 고려해 주세요.
useWorkspaceStore의selectedOrgId는 워크스페이스 목록과 동기화되지 않습니다 (context snippet 참조). 사용자가 선택한 워크스페이스가 삭제되면selectedOrgId는 여전히 이전 값을 유지합니다.현재 코드에서는 해당 워크스페이스가
filtered목록에 없으면 정렬에 영향을 주지 않아 크게 문제되지 않지만, 향후selectedOrgId가 유효한 워크스페이스인지 검증하는 로직을 추가하면 더 견고해질 것 같습니다.Also applies to: 186-192
🤖 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 90 - 91, selectedOrgId from useWorkspaceStore can point to a deleted workspace; validate it against the current filtered workspace list (variable filtered) and clear or reset it when no match exists. In practice, after computing filtered, check whether filtered.some(w => w.id === selectedOrgId); if false, call the workspace store action that resets selection (e.g., setSelectedOrgId(null) or select a fallback id) so selectedOrgId never references a removed workspace; apply the same check at the other usage block referenced (lines ~186-192).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/pages/workspace/Workspace.tsx`:
- Around line 123-130: The onCloseCreate handler contains a redundant
setLogoPreview(null) call; remove the first direct call and keep the functional
updater variant so you can revoke the previous object URL safely and then return
null. Update the onCloseCreate function (which calls setLogoPreview and
setCreateOpen) to only call setLogoPreview(prev => { if (prev)
URL.revokeObjectURL(prev); return null; }); then call setCreateOpen(false) as
before.
In `@src/pages/workspace/WorkspaceSetting.tsx`:
- Around line 79-81: fetchWorkspaceDetail is defined inside the component but
not listed in the useEffect dependency array, which triggers exhaustive-deps
warnings; fix by stabilizing fetchWorkspaceDetail with useCallback (e.g., wrap
the existing fetchWorkspaceDetail declaration in React.useCallback with orgId in
its dependencies) or by moving its definition inside the useEffect so the effect
only depends on orgId, ensuring the useEffect(() => { void
fetchWorkspaceDetail(); }, [orgId]) call no longer omits a referenced function;
update the function declaration named fetchWorkspaceDetail accordingly and keep
orgId as the triggering dependency.
---
Nitpick comments:
In `@src/components/common/dropdownmenu/DropdownMenu.tsx`:
- Around line 39-55: The div acting as a button in DropdownMenu should
explicitly be focusable; add tabIndex={0} to the element that currently has
role="button" (the same element using aria-haspopup, aria-expanded={open},
aria-controls={menuId}, aria-label={ariaLabel}, onClick={() => setOpen(v => !v)}
and onKeyDown={...}) so it reliably receives keyboard focus across browsers;
keep the existing onKeyDown handler (which toggles via Enter/Space) and ensure
this change is applied in the DropdownMenu component where trigger(open) is
rendered.
In `@src/components/common/textarea/TextareaField.tsx`:
- Around line 28-36: Wrap the inline resize function with useCallback to avoid
recreating it every render: convert the local resize function into const resize
= useCallback(() => { ... }, []), referencing ref.current inside but keeping an
empty dependency array since ref is stable; then update the useLayoutEffect to
depend on resize (useLayoutEffect(() => { resize(); }, [resize])) instead of
directly depending on value. This keeps behavior identical while satisfying the
hook-stability guideline for the resize function referenced in useLayoutEffect.
In `@src/components/workspace/MemberList.tsx`:
- Around line 15-84: The component currently contains a hardcoded mock array
(mockInviteItems) and passes it directly into InviteMemberModal; extract
mockInviteItems out of the component into a dedicated mock module (e.g., a new
mocks file) or at minimum add a clear TODO comment above its declaration, then
import that mock into MemberList and use the imported value when rendering
InviteMemberModal; update references to mockInviteItems in MemberList and ensure
the component expects real data props later (preserve the
shape/TInviteMemberItem) so swapping to API data is trivial.
In `@src/pages/workspace/Workspace.tsx`:
- Around line 90-91: selectedOrgId from useWorkspaceStore can point to a deleted
workspace; validate it against the current filtered workspace list (variable
filtered) and clear or reset it when no match exists. In practice, after
computing filtered, check whether filtered.some(w => w.id === selectedOrgId); if
false, call the workspace store action that resets selection (e.g.,
setSelectedOrgId(null) or select a fallback id) so selectedOrgId never
references a removed workspace; apply the same check at the other usage block
referenced (lines ~186-192).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b300f033-0c41-4798-9686-065a5ad37473
📒 Files selected for processing (10)
src/components/common/PageHeader.tsxsrc/components/common/controlbox/ControlBox.tsxsrc/components/common/dropdownmenu/DropdownMenu.tsxsrc/components/common/textarea/TextareaField.tsxsrc/components/workspace/MemberItem.tsxsrc/components/workspace/MemberList.tsxsrc/components/workspace/PermissionTable.tsxsrc/components/workspace/WorkspaceCard.tsxsrc/pages/workspace/Workspace.tsxsrc/pages/workspace/WorkspaceSetting.tsx
There was a problem hiding this comment.
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 (2)
206-212:⚠️ Potential issue | 🟡 Minor접근성:
aria-label과 실제 기능 불일치
aria-label="조직 검색"으로 되어 있지만, placeholder는 "워크스페이스 검색하기"입니다. PR 목표에 따라 "조직"에서 "워크스페이스"로 용어를 통일하고 있으므로, aria-label도 함께 수정해야 스크린 리더 사용자에게 일관된 경험을 제공할 수 있습니다.♿ 제안하는 수정사항
<Input - aria-label="조직 검색" + aria-label="워크스페이스 검색" placeholder="워크스페이스 검색하기" value={query} onChange={(e) => setQuery(e.target.value)} rightElement={<SearchIcon className="w-6 h-6 fill-chart-3" />} />🤖 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 206 - 212, The aria-label on the Input component is inconsistent with the placeholder and project terminology; update the aria-label on the Input used in Workspace.tsx (the Input with props value={query} and onChange={(e) => setQuery(e.target.value)}) to match the placeholder ("워크스페이스 검색하기") so screen readers see the same label as sighted users; ensure you only change the aria-label value and keep the existing Input props (including rightElement={<SearchIcon ... />}) intact.
123-130:⚠️ Potential issue | 🟡 Minor중복된
setLogoPreview(null)호출 제거 필요Line 124의
setLogoPreview(null)은 불필요합니다. Lines 125-128의 함수형 업데이트가 이전 URL을 revoke하고 null로 설정하는 작업을 모두 수행하고 있어요. 첫 번째 호출을 제거하면 코드가 더 명확해집니다.🐛 제안하는 수정사항
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 123 - 130, The onCloseCreate handler calls setLogoPreview(null) twice; remove the first direct call so the single functional update (setLogoPreview((prev) => { if (prev) URL.revokeObjectURL(prev); return null; })) handles revoking the previous object URL and clearing the state. Update the onCloseCreate function (reference: onCloseCreate, setLogoPreview) to only use the functional setter and keep setCreateOpen(false) as-is.
🧹 Nitpick comments (5)
src/components/workspace/WorkspaceCard.tsx (3)
77-77: 불필요한 fallback 값
ROLE_LABEL_MAP이Record<TMemberRole, string>타입이고w.myRole이TMemberRole타입이므로, 모든 가능한 값이 커버되어 있어서?? "내 직책 및 역할"fallback은 실제로 실행되지 않습니다. 타입 시스템이 보장하므로 제거해도 무방합니다.♻️ 제안하는 수정사항
<div className="font-body1 text-text-sub mt-2"> - {ROLE_LABEL_MAP[w.myRole] ?? "내 직책 및 역할"} + {ROLE_LABEL_MAP[w.myRole]} </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/WorkspaceCard.tsx` at line 77, Remove the unnecessary nullish fallback in the WorkspaceCard rendering: since ROLE_LABEL_MAP is a Record<TMemberRole, string> and w.myRole is TMemberRole, the expression {ROLE_LABEL_MAP[w.myRole] ?? "내 직책 및 역할"} should be simplified to just use ROLE_LABEL_MAP[w.myRole]; update the JSX in WorkspaceCard (where ROLE_LABEL_MAP and w.myRole are referenced) to remove the "??" fallback so the type-guaranteed mapping is used directly.
66-70: 접근성 개선 권장: 선택 상태 배지에 역할(role) 속성 추가선택된 워크스페이스를 시각적으로 잘 표시하고 있습니다. 스크린 리더 사용자를 위해
role="status"를 추가하면 상태 정보임을 명확히 전달할 수 있습니다.♿ 접근성 개선 제안
{isSelected && ( - <span className="shrink-0 rounded-full bg-chart-3/12 px-2 py-1 font-caption text-chart-3"> + <span + role="status" + className="shrink-0 rounded-full bg-chart-3/12 px-2 py-1 font-caption text-chart-3" + > 현재 대시보드 기준 </span> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/WorkspaceCard.tsx` around lines 66 - 70, The selected-workspace badge inside the WorkspaceCard component (the span rendered when isSelected is true) should include an accessibility role to announce its state; update the span that currently renders the "현재 대시보드 기준" badge (the element using isSelected) to include role="status" so screen readers recognize it as a status notification, ensuring you modify the JSX in WorkspaceCard where that badge is returned.
22-26: 성능 최적화:React.memo적용 고려워크스페이스 목록이 많아지면 부모 컴포넌트 리렌더링 시 모든 카드가 리렌더링될 수 있습니다.
React.memo를 적용하면 props가 변경되지 않은 카드의 불필요한 리렌더링을 방지할 수 있어요.♻️ React.memo 적용 예시
-export default function WorkspaceCard({ +function WorkspaceCard({ workspace: w, menuItems, isSelected = false, }: TProps) { // ... component body } + +export default React.memo(WorkspaceCard);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/WorkspaceCard.tsx` around lines 22 - 26, Wrap the WorkspaceCard component with React.memo to avoid unnecessary re-renders when its props don't change: import React (or memo) and export the memoized component (e.g., memo(WorkspaceCard) or export default memo(function WorkspaceCard...)) and, if needed, provide a custom comparison function that shallowly compares key props like workspace (use workspace.id or relevant identifier), isSelected and menuItems to ensure stable rendering. Ensure references to the component name WorkspaceCard and props (workspace: w, menuItems, isSelected) are used in the memoization so unchanged cards skip re-rendering.src/components/workspace/MemberRoleSelect.tsx (1)
27-40: 드롭다운 아이템 레이블도ROLE_LABEL_MAP상수 활용 권장트리거에서는
ROLE_LABEL_MAP[role]을 사용하고 있지만, 드롭다운 아이템의label은 하드코딩된 문자열을 사용하고 있어요. 레이블 변경 시 유지보수를 위해 상수를 일관되게 사용하는 것이 좋겠습니다.♻️ 제안하는 수정사항
const items: TMenuItem[] = disabled ? [] : [ { - label: "관리자", + label: ROLE_LABEL_MAP.ADMIN, onClick: () => onChange("ADMIN"), active: role === "ADMIN", }, { - label: "멤버", + label: ROLE_LABEL_MAP.MEMBER, onClick: () => onChange("MEMBER"), active: role === "MEMBER", }, ];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/MemberRoleSelect.tsx` around lines 27 - 40, Dropdown item labels are hardcoded while the trigger uses ROLE_LABEL_MAP; update the items array to use ROLE_LABEL_MAP for labels to keep labels consistent. Replace the string literals in the items definition (the object entries inside the items array in MemberRoleSelect, referencing onClick and active using role and onChange) with ROLE_LABEL_MAP["ADMIN"] and ROLE_LABEL_MAP["MEMBER"] (or the appropriate enum keys) so both trigger and menu items share the same source of truth.src/pages/workspace/Workspace.tsx (1)
154-158: useEffect 클린업 로직 검토
logoPreview가 의존성 배열에 포함되어 있어logoPreview변경 시마다 이전 값의 URL이 revoke됩니다. 컴포넌트 언마운트 시에도 클린업이 실행되어 메모리 누수를 방지합니다.다만,
onPickLogo에서 이미 이전 URL을 revoke하고 있어서(lines 148-151) 이 useEffect와 역할이 중복될 수 있습니다. 현재 구조에서는 둘 다 있어도 문제없지만, 한 곳에서만 관리하면 코드가 더 명확해질 수 있어요.🤖 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 154 - 158, The cleanup in the useEffect currently revokes logoPreview on every logoPreview change and on unmount, but onPickLogo already revokes the previous preview (lines around onPickLogo), causing duplicate revokes; choose one place to manage revocation to simplify logic—either remove the URL.revokeObjectURL call from onPickLogo and keep the useEffect cleanup (keep useEffect with dependency [logoPreview] and revoke previous URL there), or remove the useEffect and let onPickLogo handle revocation of the old preview whenever a new file is picked and also revoke on unmount (add a single unmount-only cleanup if choosing onPickLogo); update references to logoPreview, onPickLogo, useEffect and URL.revokeObjectURL accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/pages/workspace/Workspace.tsx`:
- Around line 206-212: The aria-label on the Input component is inconsistent
with the placeholder and project terminology; update the aria-label on the Input
used in Workspace.tsx (the Input with props value={query} and onChange={(e) =>
setQuery(e.target.value)}) to match the placeholder ("워크스페이스 검색하기") so screen
readers see the same label as sighted users; ensure you only change the
aria-label value and keep the existing Input props (including
rightElement={<SearchIcon ... />}) intact.
- Around line 123-130: The onCloseCreate handler calls setLogoPreview(null)
twice; remove the first direct call so the single functional update
(setLogoPreview((prev) => { if (prev) URL.revokeObjectURL(prev); return null;
})) handles revoking the previous object URL and clearing the state. Update the
onCloseCreate function (reference: onCloseCreate, setLogoPreview) to only use
the functional setter and keep setCreateOpen(false) as-is.
---
Nitpick comments:
In `@src/components/workspace/MemberRoleSelect.tsx`:
- Around line 27-40: Dropdown item labels are hardcoded while the trigger uses
ROLE_LABEL_MAP; update the items array to use ROLE_LABEL_MAP for labels to keep
labels consistent. Replace the string literals in the items definition (the
object entries inside the items array in MemberRoleSelect, referencing onClick
and active using role and onChange) with ROLE_LABEL_MAP["ADMIN"] and
ROLE_LABEL_MAP["MEMBER"] (or the appropriate enum keys) so both trigger and menu
items share the same source of truth.
In `@src/components/workspace/WorkspaceCard.tsx`:
- Line 77: Remove the unnecessary nullish fallback in the WorkspaceCard
rendering: since ROLE_LABEL_MAP is a Record<TMemberRole, string> and w.myRole is
TMemberRole, the expression {ROLE_LABEL_MAP[w.myRole] ?? "내 직책 및 역할"} should be
simplified to just use ROLE_LABEL_MAP[w.myRole]; update the JSX in WorkspaceCard
(where ROLE_LABEL_MAP and w.myRole are referenced) to remove the "??" fallback
so the type-guaranteed mapping is used directly.
- Around line 66-70: The selected-workspace badge inside the WorkspaceCard
component (the span rendered when isSelected is true) should include an
accessibility role to announce its state; update the span that currently renders
the "현재 대시보드 기준" badge (the element using isSelected) to include role="status"
so screen readers recognize it as a status notification, ensuring you modify the
JSX in WorkspaceCard where that badge is returned.
- Around line 22-26: Wrap the WorkspaceCard component with React.memo to avoid
unnecessary re-renders when its props don't change: import React (or memo) and
export the memoized component (e.g., memo(WorkspaceCard) or export default
memo(function WorkspaceCard...)) and, if needed, provide a custom comparison
function that shallowly compares key props like workspace (use workspace.id or
relevant identifier), isSelected and menuItems to ensure stable rendering.
Ensure references to the component name WorkspaceCard and props (workspace: w,
menuItems, isSelected) are used in the memoization so unchanged cards skip
re-rendering.
In `@src/pages/workspace/Workspace.tsx`:
- Around line 154-158: The cleanup in the useEffect currently revokes
logoPreview on every logoPreview change and on unmount, but onPickLogo already
revokes the previous preview (lines around onPickLogo), causing duplicate
revokes; choose one place to manage revocation to simplify logic—either remove
the URL.revokeObjectURL call from onPickLogo and keep the useEffect cleanup
(keep useEffect with dependency [logoPreview] and revoke previous URL there), or
remove the useEffect and let onPickLogo handle revocation of the old preview
whenever a new file is picked and also revoke on unmount (add a single
unmount-only cleanup if choosing onPickLogo); update references to logoPreview,
onPickLogo, useEffect and URL.revokeObjectURL accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a6b2424e-f5eb-400c-acd1-569a01ce6712
📒 Files selected for processing (7)
src/components/workspace/MemberList.tsxsrc/components/workspace/MemberRoleSelect.tsxsrc/components/workspace/WorkspaceCard.tsxsrc/constants/workspaceRole.tssrc/pages/workspace/MemberManagement.tsxsrc/pages/workspace/Workspace.tsxsrc/types/workspace/workspace.ts
✅ Files skipped from review due to trivial changes (2)
- src/pages/workspace/MemberManagement.tsx
- src/constants/workspaceRole.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/workspace/MemberList.tsx
|
P4: 수고하셨습니다! UI가 훨씬 깔끔해진 것 같아요 :) |
넵 좋습니다! 우측상단이나 좌측상단으로 배치진행해보겠습니다! |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/components/workspace/MemberList.tsx (1)
116-116: Line 116 헤더는 초소형 폭 대응만 한 번 더 보강하면 더 안전합니다.현재 정렬 변경은 좋아요. 다만 좁은 화면에서 좌측 텍스트 블록과 우측 버튼이 겹치지 않도록
flex-wrap을 추가해 두면 반응형 안정성이 더 좋아집니다.제안 diff
- <header className="mb-7 flex items-start justify-between gap-4"> + <header className="mb-7 flex flex-wrap items-start justify-between gap-4 sm:flex-nowrap">As per coding guidelines,
src/**: "7. 접근성: 시맨틱 HTML, ARIA 속성 사용 확인."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/MemberList.tsx` at line 116, The header element in MemberList.tsx (the <header> with className "mb-7 flex items-start justify-between gap-4") can overlap on very narrow screens—add responsive flex wrapping to prevent the left text block and right button from colliding: update the header's className to include flex-wrap (and optionally sm:flex-nowrap or items-center adjustments as needed) so the layout wraps on extra-small widths and preserves spacing between the left content and right controls.src/pages/workspace/MemberManagement.tsx (1)
91-93: 이 헬퍼는 현재 미사용 코드라서 함께 정리하는 편이 좋겠습니다.
getInitialMembers는 호출되지 않고 있고, 이 함수가 유일한 소비자인mockMembers도 함께 미사용 상태입니다. 이 페이지는 이미 React Query로 멤버 데이터를 읽고 있어서, 이런 임시 fixture가 남아 있으면 실제 데이터 흐름을 이해하기가 더 어려워집니다.As per coding guidelines,
src/**: 구조와 책임 분리: 페이지에 비즈니스 로직이 과도하지 않은지 확인. 커스텀 훅으로의 분리 여부 검토.🤖 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 91 - 93, Remove the unused fixture and helper: delete the getInitialMembers function and the associated mockMembers data from MemberManagement.tsx (or move them to a dedicated test/fixture file if still needed), and verify there are no remaining references to getInitialMembers or mockMembers elsewhere; ensure the page uses the existing React Query hooks for member data (keep the business logic in the query/custom hook rather than leaving unused mock helpers in the page).
🤖 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/pages/workspace/MemberManagement.tsx`:
- Around line 24-93: The build fails because PAGE_SIZE is undefined and unused
mock data was added; remove the unused mockMembers array and getInitialMembers
function, and either define PAGE_SIZE (e.g. const PAGE_SIZE = 20) or remove
PAGE_SIZE usage from the queryKey/queryFn and rely on getWorkspaceMembers'
default size parameter; update references in the queryKey/queryFn and any usages
of PAGE_SIZE accordingly, ensuring getWorkspaceMembers remains the source of
truth for the page size.
---
Nitpick comments:
In `@src/components/workspace/MemberList.tsx`:
- Line 116: The header element in MemberList.tsx (the <header> with className
"mb-7 flex items-start justify-between gap-4") can overlap on very narrow
screens—add responsive flex wrapping to prevent the left text block and right
button from colliding: update the header's className to include flex-wrap (and
optionally sm:flex-nowrap or items-center adjustments as needed) so the layout
wraps on extra-small widths and preserves spacing between the left content and
right controls.
In `@src/pages/workspace/MemberManagement.tsx`:
- Around line 91-93: Remove the unused fixture and helper: delete the
getInitialMembers function and the associated mockMembers data from
MemberManagement.tsx (or move them to a dedicated test/fixture file if still
needed), and verify there are no remaining references to getInitialMembers or
mockMembers elsewhere; ensure the page uses the existing React Query hooks for
member data (keep the business logic in the query/custom hook rather than
leaving unused mock helpers in the page).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 59fb7e3a-1a33-4f98-b02e-3d46b3b4495d
📒 Files selected for processing (2)
src/components/workspace/MemberList.tsxsrc/pages/workspace/MemberManagement.tsx
|
P4: 워크스페이스 생성 모달의 '로고 이미지' 텍스트를 워크스페이스 이름/설명 텍스트와 폰트 크기를 맞추면 좋을 것 같습니다! 수고하셨습니다!! |
|
광고 부분에 따로 뒤로가기 버튼이 없어서 제목 위에 새로 넣어두었습니다! |


🚨 관련 이슈
Closed #127
✨ 변경사항
✏️ 작업 내용
공통
워크스페이스 목록 페이지 (Workspace)
워크스페이스 수정 페이지 (WorkspaceSetting)
피드백 수정사항 반영
😅 미완성 작업
N/A
📢 논의 사항 및 참고 사항
Summary by CodeRabbit
New Features
Style
Bug Fixes
Chores
Data