Skip to content

[Refactor/#127] 워크스페이스 목록/수정 페이지 UI 및 UX 개선 리팩토링 - #135

Merged
jjjsun merged 21 commits into
developfrom
refactor/#127
Mar 26, 2026
Merged

jjjsun merged 21 commits into
developfrom
refactor/#127

Conversation

@jjjsun

@jjjsun jjjsun commented Mar 24, 2026

Copy link
Copy Markdown
Collaborator

🚨 관련 이슈

Closed #127

✨ 변경사항

  • 🐞 BugFix Something isn't working
  • 💻 CrossBrowsing Browser compatibility
  • 🌏 Deploy Deploy
  • 🎨 Design Markup & styling
  • 📃 Docs Documentation writing and editing (README.md, etc.)
  • ✨ Feature Feature
  • 🔨 Refactor Code refactoring
  • ⚙️ Setting Development environment setup
  • ✅ Test Test related (storybook, jest, etc.)

✏️ 작업 내용

  • 워크스페이스 목록 페이지와 수정페이지 UI 전반 리팩토링 진행했습니다.

공통

  • 반응형 UI 보완 진행 (기본형, table)

워크스페이스 목록 페이지 (Workspace)

  • 검색창 input에 placeholder를 "조직을 검색하세요" -> "워크스페이스 검색하기" 로 변경
  • 드랍다운 내에 텍스트 색상을 변경된 아이콘 색상과 동일하게 수정
  • useQueryClient 이용하여 삭제된 조직이 새로고침이 없이도 바로 화면에 반영되도록 구현
  • 기존에는 현재 대시보드에 나타내고있는 조직에 대한 구분이 없었다. 그래서, border와 뱃지를 이용해서 현재 대시보드가 나타내는 조직이 어떤것인지 사용자가 인지하도록 구현
  • 추가로, 현재 선택된 대시보드가 리스트 상단에 배치 되도록 구현
  • 생성하기 모달내 이미지 크기 변경 진행과 동시에 재배치 진행
  • 생성하기 모달 태블릿 크기 반응형 적용되도록 너비 지
  • 역할 라벨 공용 상수로 분리하여 워크스페이스 카드 내에 역할명 기존 영어 -> 한국어로 설정 변경
image image image image

워크스페이스 수정 페이지 (WorkspaceSetting)

  • 로고이미지 크기를 축소 시킴에 따라, Input과 TextareaField 재배치하여 디자인 개선
image 워크스페이스수정_테블릿반응형

피드백 수정사항 반영

  • 뒤로 가기 버튼 추가
  • WorkspaceCard 선택시 WorkspaceSetting 페이지로 이동 및 멤버관리 이동가능한 버튼 추가
image image image

😅 미완성 작업

N/A

📢 논의 사항 및 참고 사항

  • 대시보드 기준을 나타내는 방식으로 border와 뱃지를 사용해서 구현하였습니다.

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

  • New Features

    • 선택된 워크스페이스를 우선 정렬하여 시각적으로 표시하고 카드에 선택 상태(isSelected) 반영
  • Style

    • 전반적인 태블릿 반응형 레이아웃·간격·정렬 개선
    • 텍스트영역 래퍼 및 비활성 상태 스타일 개선
    • 드롭다운 기본 텍스트 색상 변경
    • 멤버 목록·권한 테이블·헤더·카드 레이아웃 및 트렁케이션 개선
    • 생성 모달/폼 레이아웃·플레이스홀더 조정
  • Bug Fixes

    • 워크스페이스 삭제 후 목록 캐시 자동 갱신
  • Chores

    • 역할 라벨 상수 및 역할 타입 정리 (ROLE_LABEL_MAP, TMemberRole)
  • Data

    • 일부 목업 멤버 역할 변경 (ADMIN → MEMBER)

@jjjsun
jjjsun requested review from Seojegyeong and YermIm March 24, 2026 17:06
@jjjsun jjjsun self-assigned this Mar 24, 2026
@jjjsun jjjsun added 🎨 Html&css 마크업 & 스타일링 🔨 Refactor 코드 리팩토링 labels Mar 24, 2026
@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

워크스페이스 목록·설정 페이지와 공통 컴포넌트의 Tailwind 클래스 정리 및 반응형 조정이 적용되었고, 워크스페이스 카드에 선택 상태를 나타내는 isSelected가 도입되어 선택된 워크스페이스를 상단에 노출하도록 정렬 로직이 추가되었으며, 워크스페이스 삭제 시 React Query 캐시 무효화(invalidateQueries(["my-workspaces"]))가 호출되도록 변경되었습니다.

Changes

Cohort / File(s) Summary
공통 레이아웃
src/components/common/PageHeader.tsx, src/components/common/controlbox/ControlBox.tsx
헤더·컨트롤박스의 Tailwind 반응형 유틸리티 추가/정리(tablet:items-start, tablet:gap-3, tablet:flex-col, tablet:px-5, tablet:py-5 등).
입력 컴포넌트
src/components/common/dropdownmenu/DropdownMenu.tsx, src/components/common/textarea/TextareaField.tsx
드롭다운 비활성 텍스트 색상 변경(text-text-main) 및 Textarea 마크업 리팩토링(외부 wrapper로 상태 이동, inner textarea에 disabled 변이 적용).
멤버 UI / 테이블
src/components/workspace/MemberItem.tsx, src/components/workspace/MemberList.tsx, src/components/workspace/PermissionTable.tsx
텍스트 절단·반응형 안정성용 클래스 추가(min-w-0, flex-1, w-full), 헤더 정렬·간격 조정(items-start, gap-4), 테이블 래퍼에 w-full 추가.
워크스페이스 카드
src/components/workspace/WorkspaceCard.tsx
isSelected?: boolean prop 추가, twMerge로 조건부 스타일(선택 시 border/background 변경) 적용, 선택 배지 조건부 렌더링 및 React.memo로 내보내기 변경.
워크스페이스 목록 페이지
src/pages/workspace/Workspace.tsx
useWorkspaceStore.selectedOrgId 기준으로 정렬해 선택 항목 상단 노출, isSelected 전달, 검색/생성 UI·모달 및 생성 폼의 반응형 레이아웃·placeholder 수정.
워크스페이스 설정
src/pages/workspace/WorkspaceSetting.tsx
워크스페이스 삭제 성공 시 queryClient.invalidateQueries(['my-workspaces']) 호출 추가, 기본 정보 레이아웃 grid→flex 전환, 이미지 onError에서 console.log 제거 및 클래스/간격 조정.
역할 타입/상수
src/types/workspace/workspace.ts, src/constants/workspaceRole.ts
TMemberRole 타입 도입 및 TWorkspace.myRole에 적용, ROLE_LABEL_MAP 추가로 역할 레이블 중앙화(하드코딩 제거).
모의 데이터 변경
src/pages/workspace/MemberManagement.tsx, src/components/workspace/MemberList.tsx
모의 멤버 데이터의 일부 role 변경(예: ADMINMEMBER) 및 PAGE_SIZE 관련 선언 변경(파일 내부 상태 변경).

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • Seojegyeong
  • YermIm
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 main objective와 변경 사항을 명확하게 반영하고 있습니다. "워크스페이스 목록/수정 페이지 UI 및 UX 개선 리팩토링"은 워크스페이스 페이지들의 UI/UX 리팩토링이라는 핵심 변경을 정확히 요약합니다.
Linked Issues check ✅ Passed PR의 변경사항들이 #127의 모든 주요 목표를 충족합니다. 반응형 UI 개선, placeholder 수정, 아이콘 색상 통일, useQueryClient 적용, 선택 workspace 시각화(border/badge), 상단 배치, 역할 라벨 상수화 등 체크리스트의 모든 항목이 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #127의 scope 내에 있습니다. 워크스페이스 목록/수정 페이지의 UI/UX 개선과 코드 리팩토링만 다루고 있으며, scope를 벗어나는 기능 추가나 무관한 수정은 없습니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 충실히 따르고 있으며, 필수 섹션(관련 이슈, 변경사항, 작업 내용)이 완성되어 있고 스크린샷과 함께 상세하게 작성되었습니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/#127

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.

@github-actions

github-actions Bot commented Mar 24, 2026

Copy link
Copy Markdown

📚 Storybook 배포 완료

항목 링크
📖 Storybook https://69a147b60a56365d9e2185ef-dfooaakuwk.chromatic.com/
🔍 Chromatic https://www.chromatic.com/build?appId=69a147b60a56365d9e2185ef&number=155

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

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 규칙에서 경고가 발생할 수 있습니다.

fetchWorkspaceDetailorgId에만 의존하고, 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가 삭제된 워크스페이스를 가리킬 수 있는 엣지 케이스를 고려해 주세요.

useWorkspaceStoreselectedOrgId는 워크스페이스 목록과 동기화되지 않습니다 (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

📥 Commits

Reviewing files that changed from the base of the PR and between 2e5d82d and 32c295e.

📒 Files selected for processing (10)
  • src/components/common/PageHeader.tsx
  • src/components/common/controlbox/ControlBox.tsx
  • src/components/common/dropdownmenu/DropdownMenu.tsx
  • src/components/common/textarea/TextareaField.tsx
  • src/components/workspace/MemberItem.tsx
  • src/components/workspace/MemberList.tsx
  • src/components/workspace/PermissionTable.tsx
  • src/components/workspace/WorkspaceCard.tsx
  • src/pages/workspace/Workspace.tsx
  • src/pages/workspace/WorkspaceSetting.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.

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_MAPRecord<TMemberRole, string> 타입이고 w.myRoleTMemberRole 타입이므로, 모든 가능한 값이 커버되어 있어서 ?? "내 직책 및 역할" 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

📥 Commits

Reviewing files that changed from the base of the PR and between 95abca2 and 633138a.

📒 Files selected for processing (7)
  • src/components/workspace/MemberList.tsx
  • src/components/workspace/MemberRoleSelect.tsx
  • src/components/workspace/WorkspaceCard.tsx
  • src/constants/workspaceRole.ts
  • src/pages/workspace/MemberManagement.tsx
  • src/pages/workspace/Workspace.tsx
  • src/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

@Seojegyeong

Seojegyeong commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator

P4: 수고하셨습니다! UI가 훨씬 깔끔해진 것 같아요 :)
혹시 추가로 워크스페이스 정보 수정하기, 멤버 관리 페이지에 뒤로 가기 버튼이 있으면 어떨까요?

@jjjsun

jjjsun commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator Author

P4: 수고하셨습니다! UI가 훨씬 깔끔해진 것 같아요 :) 혹시 추가로 워크스페이스 정보 수정하기, 멤버 관리 페이지에 뒤로 가기 버튼이 있으면 어떨까요?

넵 좋습니다! 우측상단이나 좌측상단으로 배치진행해보겠습니다!

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1d20909 and 5179df0.

📒 Files selected for processing (2)
  • src/components/workspace/MemberList.tsx
  • src/pages/workspace/MemberManagement.tsx

Comment thread src/pages/workspace/MemberManagement.tsx Outdated
@Seojegyeong

Seojegyeong commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator

P4: 그리고 저번 회의 때 워크스페이스 리스트 내 카드 자체를 눌러서 이동하는 방법도 논의되었던 걸로 기억합니다!

카드 형태의 UI는 사용자가 자연스럽게 클릭을 시도하게 되는데, 반응이 없으면 고장난 UI로 인식될 수 있을 것 같습니다. 또한 ... 버튼의 존재를 인지하도록 유도하는 힌트가 없어 핵심 기능(멤버 관리, 정보 수정)을 바로 발견할 수 있을지 잘 모르겠습니다..

카드 클릭 → 워크스페이스 설정 페이지로 이동하고, 해당 페이지에서 멤버 관리와 정보 수정을 모두 처리하는 구조는 어떨까요?
image

추가로 뒤로 가기 버튼은 광고 부분과 동일하게 좌측상단으로 배치하는게 좋을 것 같습니다!

@YermIm

YermIm commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator

P4: 워크스페이스 생성 모달의 '로고 이미지' 텍스트를 워크스페이스 이름/설명 텍스트와 폰트 크기를 맞추면 좋을 것 같습니다! 수고하셨습니다!!

@jjjsun

jjjsun commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator Author

P4: 그리고 저번 회의 때 워크스페이스 리스트 내 카드 자체를 눌러서 이동하는 방법도 논의되었던 걸로 기억합니다!

카드 형태의 UI는 사용자가 자연스럽게 클릭을 시도하게 되는데, 반응이 없으면 고장난 UI로 인식될 수 있을 것 같습니다. 또한 ... 버튼의 존재를 인지하도록 유도하는 힌트가 없어 핵심 기능(멤버 관리, 정보 수정)을 바로 발견할 수 있을지 잘 모르겠습니다..

카드 클릭 → 워크스페이스 설정 페이지로 이동하고, 해당 페이지에서 멤버 관리와 정보 수정을 모두 처리하는 구조는 어떨까요? image

추가로 뒤로 가기 버튼은 광고 부분과 동일하게 좌측상단으로 배치하는게 좋을 것 같습니다!

음 WorkspaceSetting페이지내에서 +멤버관리+권한변경이 같이 있으면 정보가 조금 많을것같다는 생각도 듭니다,,!
그러면 카드 클릭하면 WorkspaceSetting페이지로 이동하고, WorkspaceSetting내에서 MemberManagement.tsx 페이지로 이동하는 버튼을 만들어서 이동하는 구조로 진행하는건어떨까요?

@Seojegyeong

Copy link
Copy Markdown
Collaborator

P4: 그리고 저번 회의 때 워크스페이스 리스트 내 카드 자체를 눌러서 이동하는 방법도 논의되었던 걸로 기억합니다!
카드 형태의 UI는 사용자가 자연스럽게 클릭을 시도하게 되는데, 반응이 없으면 고장난 UI로 인식될 수 있을 것 같습니다. 또한 ... 버튼의 존재를 인지하도록 유도하는 힌트가 없어 핵심 기능(멤버 관리, 정보 수정)을 바로 발견할 수 있을지 잘 모르겠습니다..
카드 클릭 → 워크스페이스 설정 페이지로 이동하고, 해당 페이지에서 멤버 관리와 정보 수정을 모두 처리하는 구조는 어떨까요? image
추가로 뒤로 가기 버튼은 광고 부분과 동일하게 좌측상단으로 배치하는게 좋을 것 같습니다!

음 WorkspaceSetting페이지내에서 +멤버관리+권한변경이 같이 있으면 정보가 조금 많을것같다는 생각도 듭니다,,! 그러면 카드 클릭하면 WorkspaceSetting페이지로 이동하고, WorkspaceSetting내에서 MemberManagement.tsx 페이지로 이동하는 버튼을 만들어서 이동하는 구조로 진행하는건어떨까요?

넵 좋습니다!

@jjjsun

jjjsun commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator Author

광고 부분에 따로 뒤로가기 버튼이 없어서 제목 위에 새로 넣어두었습니다!

@jjjsun
jjjsun merged commit 57871bc into develop Mar 26, 2026
3 checks passed
@jjjsun
jjjsun deleted the refactor/#127 branch March 26, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🎨 Html&css 마크업 & 스타일링 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

♻️ [Refactor] 워크스페이스 목록/수정 페이지 UX개선 및 코드 리팩토링

3 participants