Skip to content

[Feature/#78] 워크스페이스 로고 이미지 업로드 API 연동 - #90

Merged
jjjsun merged 9 commits into
developfrom
feature/#78
Mar 11, 2026
Merged

[Feature/#78] 워크스페이스 로고 이미지 업로드 API 연동#90
jjjsun merged 9 commits into
developfrom
feature/#78

Conversation

@jjjsun

@jjjsun jjjsun commented Mar 10, 2026

Copy link
Copy Markdown
Collaborator

🚨 관련 이슈

Closed #78

✨ 변경사항

  • 🐞 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.)

✏️ 작업 내용

  • 워크스페이스 로고 이미지 업로드 API 연동
  • 로고이미지 업로드후에 반환되는 logoUrl을 상태에 반영
  • 워크스페이스 생성시 logoUrl같이 전달하도록 수정
  • 워크스페이스 수정시 logoUrl 변경 가능하도록 수정
  • 로고 이미지 미리보기 기능 추가
  • 워크스페이스 관리페이지에서 로고 미지정시에 기본 아이콘 표시하도록 UI 수정
  • axios 공통 설정에서 multipart/form-data 업로드가 정상 동작가능하도록 Content-Type: application/json 제거

😅 미완성 작업

N/A

📢 논의 사항 및 참고 사항

  • axios 공통 설정에서 Content-Type을 제거해서 multipart 업로드가 정상 동작하도록 수정했습니다.

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

Summary by CodeRabbit

  • New Features

    • 워크스페이스 생성 및 설정에서 로고 이미지 업로드 기능 추가 및 업로드 전 실시간 미리보기 제공 (JPG/JPEG/PNG/WEBP 지원)
  • Improvements

    • 워크스페이스 카드 및 설정에서 로고가 없거나 로드 실패할 때 대체 표시 개선
    • 외부/상대 이미지 URL 처리 안정성 향상 및 이미지 로드 오류 처리 추가
    • 업로드 중 관련 버튼 비활성화 및 진행 상태 반영
  • Style

    • 입력 라벨 간격 소폭 증가로 레이아웃 정돈

@jjjsun
jjjsun requested review from Seojegyeong and YermIm March 10, 2026 17:19
@jjjsun jjjsun self-assigned this Mar 10, 2026
@jjjsun jjjsun added the ✨ Feature 기능 개발 label Mar 10, 2026
@coderabbitai

coderabbitai Bot commented Mar 10, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@jjjsun has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 59 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8493cca0-5722-4e2b-8aa5-08a2f7416c37

📥 Commits

Reviewing files that changed from the base of the PR and between 9736711 and e6ce5bf.

📒 Files selected for processing (1)
  • src/pages/workspace/WorkspaceSetting.tsx
📝 Walkthrough

Walkthrough

워크스페이스 로고 파일 업로드 기능을 추가했습니다. 클라이언트 미리보기와 FormData 기반 uploadImage API 호출을 통합하고, 생성/설정 화면에서 업로드된 URL을 로고로 사용하도록 변경했습니다.

Changes

Cohort / File(s) Summary
API 및 타입 정의
src/api/workspace/org.ts, src/types/workspace/workspace.ts
uploadImage(file: File): Promise<string> 추가 — FormDataPOST /api/images/upload 전송하고 응답의 data.data.url 반환. 응답 타입 TUploadImageResponse 추가.
이미지 URL 처리 유틸리티
src/lib/getImageUrl.ts
상대 경로를 환경변수 VITE_API_BASE_URL 기반 절대 URL로 변환하는 `getImageUrl(url?: string
Axios 설정
src/lib/axiosInstance.ts
기본 Content-Type: application/json 헤더 제거 — 브라우저가 FormData 전송 시 multipart 경계(boundary)를 자동 설정하도록 변경.
컴포넌트: WorkspaceCard
src/components/workspace/WorkspaceCard.tsx
getImageUrl 사용 및 이미지 로드 실패 처리(imageError 상태, onError) 추가하여 플레이스홀더 노출 로직 적용.
페이지: Workspace 생성
src/pages/workspace/Workspace.tsx
logoFile, logoPreview 상태와 파일 선택/미리보기/정리 로직 추가. 생성 시 uploadImage로 파일 업로드 후 반환 URL을 createWorkspace에 전달. 파일 타입 제한 및 객체 URL 해제 처리 추가.
페이지: Workspace 설정
src/pages/workspace/WorkspaceSetting.tsx
serverLogoUrl, logoFile, logoPreview, uploading, imageError 등 상태 추가. 저장 시 파일이 있으면 uploadImage로 업로드하고 updateWorkspace에 URL 포함. 업로드 중 UI 비활성화 및 객체 URL 정리 로직 추가.
공통 입력 컴포넌트 스타일
src/components/common/input/Input.tsx, src/components/common/textarea/TextareaField.tsx
레이블 하단 여백을 mb-2로 변경하여 간격 조정(스타일 변경만).

Sequence Diagram

sequenceDiagram
    participant User as 사용자
    participant UI as 클라이언트(UI)
    participant UploadAPI as 업로드 헬퍼
    participant WorkspaceAPI as 워크스페이스 API
    participant Server as 백엔드

    rect rgba(0,128,0,0.5)
    User->>UI: 이미지 파일 선택
    UI->>UI: Object URL 생성(미리보기)
    end

    rect rgba(0,0,255,0.5)
    User->>UI: 생성/저장 요청
    UI->>UploadAPI: uploadImage(file) (FormData POST)
    UploadAPI->>Server: POST /api/images/upload (FormData)
    Server-->>UploadAPI: { data: { url: logoUrl } }
    UploadAPI-->>UI: logoUrl 반환
    end

    rect rgba(128,0,128,0.5)
    UI->>WorkspaceAPI: createWorkspace/updateWorkspace { logoUrl, ... }
    WorkspaceAPI->>Server: 워크스페이스 생성/수정 요청
    Server-->>WorkspaceAPI: 성공 응답
    WorkspaceAPI-->>UI: 성공
    UI->>UI: Object URL 해제 및 상태 정리
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

📬 API

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 제목은 워크스페이스 로고 업로드 API 연동이라는 변경사항의 핵심을 명확하게 전달하며, 모든 파일 변경사항과 직접 관련성이 높습니다.
Description check ✅ Passed PR 설명은 템플릿 구조를 따르고, 관련 이슈 #78을 명시하며, 변경사항 체크박스, 작업 내용, 그리고 검토 가이드를 포함하고 있습니다.
Linked Issues check ✅ Passed PR의 모든 코드 변경사항이 이슈 #78의 체크리스트(파일 업로드 API 연동, logoUrl 상태 저장, updateWorkspace/생성 시 반영)를 완벽히 충족합니다.
Out of Scope Changes check ✅ Passed Input.tsx와 TextareaField.tsx의 margin 조정은 UI 일관성 개선을 위한 마이너 스타일 변경이며, 로고 업로드 기능과 직접적으로 연관되지만 범위 내에서 수용 가능합니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#78

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

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

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)

56-59: ⚠️ Potential issue | 🟡 Minor

성공 시에도 모달 정리 로직을 공통으로 타게 해주세요.

여기서는 setCreateOpen(false)만 호출해서 logoPreview revoke와 파일 상태 초기화가 빠집니다. 성공 경로도 onCloseCreate()나 별도 resetCreateForm()을 재사용해야 object URL과 숨겨진 폼 상태가 남지 않습니다.

💡 제안
    onSuccess: () => {
      void queryClient.invalidateQueries({ queryKey: ["my-workspaces"] });
-      setCreateOpen(false);
+      onCloseCreate();
    },
🤖 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 56 - 59, The success handler
currently only calls setCreateOpen(false) which skips cleanup of object URLs and
hidden form state; update the onSuccess path to call the shared cleanup routine
(e.g., onCloseCreate() or resetCreateForm()) instead of or in addition to
setCreateOpen(false) so logoPreview is revoked and file state is reset—locate
the onSuccess block in Workspace.tsx and replace the direct setCreateOpen(false)
with the existing cleanup function (onCloseCreate or resetCreateForm) to ensure
consistent teardown.

226-262: ⚠️ Potential issue | 🟡 Minor

생성 중에는 파일 선택 UI도 같이 잠가두는 게 안전합니다.

mutationFn이 시작된 뒤에도 업로드 버튼과 미리보기 버튼이 살아 있어서, 사용자가 파일을 다시 고르면 화면에 보이는 preview와 실제로 업로드 중인 파일이 달라질 수 있습니다. creating 동안 hidden input과 업로드 트리거도 함께 비활성화하는 편이 맞아요.

As per coding guidelines, src/**: 1. 상태 관리: 서버 상태(React Query)와 전역 상태(Zustand)의 분리 여부 확인. useMutation, useQuery의 올바른 사용 확인.

🤖 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 226 - 262, Disable the file
input and upload triggers while the mutation is in progress: when the mutation
state variable creating is true, set the hidden file input (ref fileRef) to
disabled, prevent/ignore clicks on the Upload Button (onClick openFile) and the
preview button (the <button> that currently calls openFile), and add an
appropriate aria-busy/aria-disabled state for accessibility; ensure onPickLogo
cannot run when creating is true (guard at start of onPickLogo) so the visible
logoPreview cannot diverge from the in-flight upload. Reference: fileRef,
onPickLogo, openFile, logoPreview and the creating mutation state.
🧹 Nitpick comments (1)
src/lib/getImageUrl.ts (1)

6-8: new URL() 조합 규칙을 한 번만 검증해주세요.

url/uploads/... 같은 루트 상대경로이고 VITE_API_BASE_URLhttps://api.example.com/v1/처럼 path prefix를 포함하면, new URL(url, BASE_URL)/v1를 버린 URL을 만듭니다. 업로드 API가 절대경로/루트 상대경로/상대경로 중 어떤 형태를 돌려주는지 계약을 고정하거나, origin 기준 조합과 base-path 기준 조합을 분리하는 편이 더 안전해 보여요.

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

In `@src/lib/getImageUrl.ts` around lines 6 - 8, The current getImageUrl logic
uses new URL(url, BASE_URL) unconditionally which mis-resolves root-relative
paths when BASE_URL includes a path prefix; update getImageUrl to first classify
url: if it is an absolute URL (starts with http:// or https://) return it as-is;
if it is a root-relative path (starts with "/" but not "//") resolve it against
the origin of BASE_URL (use new URL(url, new URL(BASE_URL).origin)) so the
BASE_URL path prefix is not dropped; otherwise (relative paths like "./img.png"
or "img.png") resolve them against the full BASE_URL as before (new URL(url,
BASE_URL)); keep using the BASE_URL constant and new URL but branch based on url
form to ensure correct composition.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/workspace/org.ts`:
- Around line 52-60: Currently uploadImage(file) returns only the uploaded URL
and leaves no way to clean up if later createWorkspace() or updateWorkspace()
fails; modify the flow so uploadImage returns an identifier (e.g., imageId or
uploadToken) and implement/call an image delete/abort API when downstream save
fails, or change the server to provide an atomic upload+save endpoint; update
uploadImage to return the id/token and update callers
(createWorkspace/updateWorkspace) to call the new delete/abort API on failure so
uploaded files are cleaned up and no orphaned storage remains.

In `@src/components/workspace/WorkspaceCard.tsx`:
- Around line 22-29: WorkspaceCard currently only logs image load errors in the
img onError handler so a broken image icon is shown; add a local React state
(e.g., imageLoadFailed) in the WorkspaceCard component, set it to true inside
the img onError handler (reference the img element using getImageUrl and the
w.logoUrl prop), and use that state to hide the <img> and render the existing
placeholder instead; also reset imageLoadFailed to false when w.logoUrl changes
(useEffect watching w.logoUrl) so new logos will attempt to load again.

---

Outside diff comments:
In `@src/pages/workspace/Workspace.tsx`:
- Around line 56-59: The success handler currently only calls
setCreateOpen(false) which skips cleanup of object URLs and hidden form state;
update the onSuccess path to call the shared cleanup routine (e.g.,
onCloseCreate() or resetCreateForm()) instead of or in addition to
setCreateOpen(false) so logoPreview is revoked and file state is reset—locate
the onSuccess block in Workspace.tsx and replace the direct setCreateOpen(false)
with the existing cleanup function (onCloseCreate or resetCreateForm) to ensure
consistent teardown.
- Around line 226-262: Disable the file input and upload triggers while the
mutation is in progress: when the mutation state variable creating is true, set
the hidden file input (ref fileRef) to disabled, prevent/ignore clicks on the
Upload Button (onClick openFile) and the preview button (the <button> that
currently calls openFile), and add an appropriate aria-busy/aria-disabled state
for accessibility; ensure onPickLogo cannot run when creating is true (guard at
start of onPickLogo) so the visible logoPreview cannot diverge from the
in-flight upload. Reference: fileRef, onPickLogo, openFile, logoPreview and the
creating mutation state.

---

Nitpick comments:
In `@src/lib/getImageUrl.ts`:
- Around line 6-8: The current getImageUrl logic uses new URL(url, BASE_URL)
unconditionally which mis-resolves root-relative paths when BASE_URL includes a
path prefix; update getImageUrl to first classify url: if it is an absolute URL
(starts with http:// or https://) return it as-is; if it is a root-relative path
(starts with "/" but not "//") resolve it against the origin of BASE_URL (use
new URL(url, new URL(BASE_URL).origin)) so the BASE_URL path prefix is not
dropped; otherwise (relative paths like "./img.png" or "img.png") resolve them
against the full BASE_URL as before (new URL(url, BASE_URL)); keep using the
BASE_URL constant and new URL but branch based on url form to ensure correct
composition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 433bdd93-08f8-4865-a400-86f3bbd9650e

📥 Commits

Reviewing files that changed from the base of the PR and between 9da070b and baa1556.

📒 Files selected for processing (7)
  • src/api/workspace/org.ts
  • src/components/workspace/WorkspaceCard.tsx
  • src/lib/axiosInstance.ts
  • src/lib/getImageUrl.ts
  • src/pages/workspace/Workspace.tsx
  • src/pages/workspace/WorkspaceSetting.tsx
  • src/types/workspace/workspace.ts
💤 Files with no reviewable changes (1)
  • src/lib/axiosInstance.ts

Comment thread src/api/workspace/org.ts
Comment thread src/components/workspace/WorkspaceCard.tsx Outdated
@github-actions

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@YermIm

YermIm commented Mar 11, 2026

Copy link
Copy Markdown
Collaborator

P3: 이미지 미리보기 영역이 워크스페이스 생성에서는 정사각형, 워크스페이스 관리에서는 직사각형인데 통일해도 좋을 것 같습니다! 수고하셨습니다~!

@Seojegyeong Seojegyeong left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4: 확인했습니다. 수고하셨습니다!

@github-actions

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/components/common/textarea/TextareaField.tsx (1)

42-55: 향후 개선 제안: 접근성 속성 추가 고려

현재 변경과는 무관하지만, Input.tsx와 비교했을 때 이 컴포넌트에는 aria-invalid, aria-describedby 같은 접근성 속성이 없어요. 만약 이 컴포넌트에도 에러 상태나 도움말 텍스트 기능이 필요하다면, Input.tsx와 동일한 패턴으로 확장하는 것을 고려해 주세요.

🤖 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 42 - 55, The
TextareaField textarea lacks accessibility attributes present in Input.tsx;
update the TextareaField component to accept and forward props for aria-invalid
and aria-describedby (or a single props object like error and helpId similar to
Input.tsx), add these props to the textarea element (e.g.,
aria-invalid={ariaInvalid} aria-describedby={ariaDescribedBy}), and update the
component's prop types/interface and any places that render help/error text so
the IDs used in aria-describedby match the help/error elements.
src/pages/workspace/WorkspaceSetting.tsx (4)

75-77: useEffect 의존성 배열에 fetchWorkspaceDetail 누락 검토 필요

fetchWorkspaceDetail이 컴포넌트 내부에 정의되어 있지만 의존성 배열에 포함되어 있지 않습니다. 현재는 orgId가 의존성에 있어 동작에 문제는 없지만, ESLint react-hooks/exhaustive-deps 규칙에서 경고가 발생할 수 있습니다.

useCallback으로 감싸거나, 향후 유지보수를 위해 명시적으로 의존성을 관리하는 것을 권장합니다.

♻️ useCallback 적용 예시
+ const fetchWorkspaceDetail = useCallback(async () => {
+   if (orgId === null) {
+     setErrorMsg("잘못된 워크스페이스ID 입니다");
+     return;
+   }
+   // ... 기존 로직
+ }, [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 75 - 77, The useEffect
currently calls fetchWorkspaceDetail but doesn't list it in the dependency
array, which will trigger react-hooks/exhaustive-deps warnings; fix by memoizing
fetchWorkspaceDetail with useCallback (e.g., wrap the existing
fetchWorkspaceDetail definition in useCallback and include its dependencies),
then keep [orgId, fetchWorkspaceDetail] (or just fetchWorkspaceDetail if it
already closes over orgId) as the useEffect dependency array so ESLint is
satisfied and the effect updates correctly when the callback changes.

204-210: 숨겨진 파일 입력 필드에 접근성 속성 추가 권장

화면 리더 사용자를 위해 hidden file input에도 aria-label 또는 id와 연결된 <label>을 추가하면 접근성이 향상됩니다.

♿ 접근성 개선 예시
                <input
                  ref={fileRef}
                  type="file"
                  accept="image/jpeg,image/jpg,image/png,image/webp"
                  className="hidden"
                  onChange={onPickLogo}
+                 aria-label="워크스페이스 로고 이미지 선택"
                />
🤖 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 204 - 210, The hidden
file input referenced by fileRef with onChange={onPickLogo} lacks an
accessibility label; add an accessible name by either adding aria-label (e.g.,
aria-label="Upload workspace logo") to the input or give it an id and render a
visible or screen-reader-only <label htmlFor="..."> that triggers the input,
ensuring screen reader users can activate the file picker; update the JSX around
the input and any UI element that opens the file dialog to reference the new
id/label.

23-165: 커스텀 훅으로 비즈니스 로직 분리 권장

페이지 컴포넌트에 상태 관리, API 호출, 파일 처리 등 비즈니스 로직이 많이 포함되어 있습니다. 코딩 가이드라인에 따라 useWorkspaceSetting과 같은 커스텀 훅으로 분리하면 테스트 용이성과 재사용성이 향상됩니다.

또한, 서버 상태 관리를 위해 React Query(useMutation, useQuery)를 도입하면 캐싱, 리페치, 로딩/에러 상태 관리가 더 효율적으로 처리됩니다.

As per coding guidelines, "페이지에 비즈니스 로직이 과도하지 않은지 확인. 커스텀 훅으로의 분리 여부 검토."

🤖 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 23 - 165, The
component currently mixes UI and business logic; extract state, API calls and
file handling into a custom hook (e.g., useWorkspaceSetting) that encapsulates
orgId derivation, fetchWorkspaceDetail, onSave, onDelete, onPickLogo,
onResetLogo and related state (name, desc, loading, saving, deleting,
serverLogoUrl, logoFile, logoPreview, uploading, errorMsg, fileRef) and return
handlers and state for the WorkspaceSetting component to consume; inside the
hook replace manual fetch/update/delete with React Query useQuery/useMutation
for getWorkspace, updateWorkspace, deleteWorkspace and use a stable cleanup for
URL.revokeObjectURL to avoid leaks, ensure the hook accepts workspaceId (or
reads it via useParams) and exposes a ref or openFilePicker function for the
file input.

92-96: 이미지 업로드 실패 시 에러 메시지 구체화 권장

현재 이미지 업로드가 실패해도 "변경사항 저장에 실패했습니다"라는 일반적인 에러 메시지가 표시됩니다. 사용자가 어떤 단계에서 실패했는지 알기 어렵습니다.

업로드와 저장 단계를 분리하여 에러 메시지를 구체화하면 UX가 개선됩니다.

♻️ 에러 메시지 구체화 예시
      if (logoFile) {
        setUploading(true);
-       nextLogoUrl = await uploadImage(logoFile);
+       try {
+         nextLogoUrl = await uploadImage(logoFile);
+       } catch (uploadError) {
+         toast.error(getAxiosMessage(uploadError, "이미지 업로드에 실패했습니다"));
+         return;
+       } finally {
+         setUploading(false);
+       }
-       setUploading(false);
      }
🤖 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 92 - 96, The image
upload and workspace save are lumped together, so failed uploads surface as a
generic "변경사항 저장에 실패했습니다" message; modify the save flow to call
uploadImage(logoFile) inside its own try/catch/finally around the code that sets
setUploading(true)/setUploading(false), capture and display a specific upload
error (e.g., "이미지 업로드 실패: <error message>") and abort the save if upload fails,
then perform the workspace save (e.g., saveWorkspace or the existing submit
handler) in a separate try/catch that shows a distinct save failure message;
reference the logoFile check, nextLogoUrl assignment, setUploading, and
uploadImage when making these changes.
🤖 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/WorkspaceSetting.tsx`:
- Around line 272-274: The TextareaField currently sets disabled using only
saving || deleting; update the disabled prop on the TextareaField component to
also include the uploading state (i.e., disabled={saving || deleting ||
uploading}) so it matches other inputs—locate the TextareaField instance in
WorkspaceSetting (the component with className="min-h-55 xl:min-h-93") and add
the uploading symbol to the disabled expression.
- Around line 218-226: The img element using getImageUrl(serverLogoUrl) has an
onError that only logs and leaves a broken image; update the onError handler in
the component rendering (the img near getImageUrl/serverLogoUrl) to set a
fallback state or src to the BuildingIcon (or render the BuildingIcon component)
when the image fails to load: capture e.currentTarget and replace its src with a
local fallback data URL or toggle a piece of state (e.g., logoLoadError) so the
JSX renders <BuildingIcon /> instead of the img; ensure you still use name for
alt text and avoid infinite onError loops by checking the current src before
replacing.

---

Nitpick comments:
In `@src/components/common/textarea/TextareaField.tsx`:
- Around line 42-55: The TextareaField textarea lacks accessibility attributes
present in Input.tsx; update the TextareaField component to accept and forward
props for aria-invalid and aria-describedby (or a single props object like error
and helpId similar to Input.tsx), add these props to the textarea element (e.g.,
aria-invalid={ariaInvalid} aria-describedby={ariaDescribedBy}), and update the
component's prop types/interface and any places that render help/error text so
the IDs used in aria-describedby match the help/error elements.

In `@src/pages/workspace/WorkspaceSetting.tsx`:
- Around line 75-77: The useEffect currently calls fetchWorkspaceDetail but
doesn't list it in the dependency array, which will trigger
react-hooks/exhaustive-deps warnings; fix by memoizing fetchWorkspaceDetail with
useCallback (e.g., wrap the existing fetchWorkspaceDetail definition in
useCallback and include its dependencies), then keep [orgId,
fetchWorkspaceDetail] (or just fetchWorkspaceDetail if it already closes over
orgId) as the useEffect dependency array so ESLint is satisfied and the effect
updates correctly when the callback changes.
- Around line 204-210: The hidden file input referenced by fileRef with
onChange={onPickLogo} lacks an accessibility label; add an accessible name by
either adding aria-label (e.g., aria-label="Upload workspace logo") to the input
or give it an id and render a visible or screen-reader-only <label
htmlFor="..."> that triggers the input, ensuring screen reader users can
activate the file picker; update the JSX around the input and any UI element
that opens the file dialog to reference the new id/label.
- Around line 23-165: The component currently mixes UI and business logic;
extract state, API calls and file handling into a custom hook (e.g.,
useWorkspaceSetting) that encapsulates orgId derivation, fetchWorkspaceDetail,
onSave, onDelete, onPickLogo, onResetLogo and related state (name, desc,
loading, saving, deleting, serverLogoUrl, logoFile, logoPreview, uploading,
errorMsg, fileRef) and return handlers and state for the WorkspaceSetting
component to consume; inside the hook replace manual fetch/update/delete with
React Query useQuery/useMutation for getWorkspace, updateWorkspace,
deleteWorkspace and use a stable cleanup for URL.revokeObjectURL to avoid leaks,
ensure the hook accepts workspaceId (or reads it via useParams) and exposes a
ref or openFilePicker function for the file input.
- Around line 92-96: The image upload and workspace save are lumped together, so
failed uploads surface as a generic "변경사항 저장에 실패했습니다" message; modify the save
flow to call uploadImage(logoFile) inside its own try/catch/finally around the
code that sets setUploading(true)/setUploading(false), capture and display a
specific upload error (e.g., "이미지 업로드 실패: <error message>") and abort the save
if upload fails, then perform the workspace save (e.g., saveWorkspace or the
existing submit handler) in a separate try/catch that shows a distinct save
failure message; reference the logoFile check, nextLogoUrl assignment,
setUploading, and uploadImage when making these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5050264f-cf9b-44e1-80ac-8531078333d6

📥 Commits

Reviewing files that changed from the base of the PR and between 69febd2 and 0227022.

📒 Files selected for processing (3)
  • src/components/common/input/Input.tsx
  • src/components/common/textarea/TextareaField.tsx
  • src/pages/workspace/WorkspaceSetting.tsx

Comment thread src/pages/workspace/WorkspaceSetting.tsx Outdated
Comment thread src/pages/workspace/WorkspaceSetting.tsx
@github-actions

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/pages/workspace/WorkspaceSetting.tsx (1)

48-76: 서버 상태와 업로드 상태 로직은 커스텀 훅으로 한 번 분리해두면 좋겠습니다.

fetchWorkspaceDetail, onSave, onPickLogo, object URL 정리, loading/saving/uploading/imageError까지 페이지 컴포넌트 안에 모여 있어서 상태 전이를 따라가기가 조금 무거워졌습니다. useWorkspaceSetting 같은 훅으로 분리하고 서버 요청은 query/mutation 계층으로 옮기면 테스트와 재사용, 에러 처리가 더 단순해질 것 같습니다. As per coding guidelines, "상태 관리: 서버 상태(React Query)와 전역 상태(Zustand)의 분리 여부 확인. useMutation, useQuery의 올바른 사용 확인." 및 "구조와 책임 분리: 페이지에 비즈니스 로직이 과도하지 않은지 확인. 커스텀 훅으로의 분리 여부 검토."

Also applies to: 81-166

🤖 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 48 - 76, The page
component contains mixed UI and business logic—extract fetching/updating and
upload state into a custom hook (e.g., create useWorkspaceSetting) that
encapsulates fetchWorkspaceDetail, onSave, onPickLogo, image uploading state
(loading/saving/uploading/imageError), and object URL cleanup; move server calls
to React Query hooks (useQuery for getWorkspace and useMutation for
update/upload) and have useWorkspaceSetting compose those hooks and expose state
and handlers to the component, ensure object URLs are revoked in the hook’s
cleanup/teardown logic, and update the component to only consume the hook’s
returned state and methods.
🤖 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/WorkspaceSetting.tsx`:
- Around line 220-229: Compute a resolvedLogoUrl by calling
getImageUrl(serverLogoUrl) before the JSX and use that value in the render
condition instead of serverLogoUrl; update the branch that currently renders
<img> to require resolvedLogoUrl && !imageError, use resolvedLogoUrl as the img
src, and keep the BuildingIcon fallback when resolvedLogoUrl is falsy (also
preserve the onError handler that calls setImageError). This touches
getImageUrl, serverLogoUrl, resolvedLogoUrl, imageError, setImageError, and
BuildingIcon.
- Around line 94-103: The current flow uploads via uploadImage before calling
updateWorkspace which can fail and leave an orphaned image; wrap the
upload+update sequence in a try/catch so that if updateWorkspace(orgId, {...})
throws, you call the image cleanup function (e.g., deleteImage or
removeUploadedImage) using nextLogoUrl to delete the just-uploaded file, and
rethrow or surface the original error; ensure
setUploading(true)/setUploading(false) are properly paired (use finally to clear
uploading) and only set nextLogoUrl into the payload when uploadImage succeeded
so the rollback path has the proper URL to delete.

---

Nitpick comments:
In `@src/pages/workspace/WorkspaceSetting.tsx`:
- Around line 48-76: The page component contains mixed UI and business
logic—extract fetching/updating and upload state into a custom hook (e.g.,
create useWorkspaceSetting) that encapsulates fetchWorkspaceDetail, onSave,
onPickLogo, image uploading state (loading/saving/uploading/imageError), and
object URL cleanup; move server calls to React Query hooks (useQuery for
getWorkspace and useMutation for update/upload) and have useWorkspaceSetting
compose those hooks and expose state and handlers to the component, ensure
object URLs are revoked in the hook’s cleanup/teardown logic, and update the
component to only consume the hook’s returned state and methods.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff1f9214-2b86-494e-a962-951ff7dbb954

📥 Commits

Reviewing files that changed from the base of the PR and between 0227022 and 9736711.

📒 Files selected for processing (1)
  • src/pages/workspace/WorkspaceSetting.tsx

Comment thread src/pages/workspace/WorkspaceSetting.tsx
Comment thread src/pages/workspace/WorkspaceSetting.tsx Outdated
@github-actions

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@jjjsun
jjjsun merged commit 0b1c62e into develop Mar 11, 2026
3 checks passed
@jjjsun
jjjsun deleted the feature/#78 branch March 11, 2026 17:30

@Seojegyeong Seojegyeong left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4: 확인했습니다!

@jjjsun jjjsun added the 🎨 Html&css 마크업 & 스타일링 label Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발 🎨 Html&css 마크업 & 스타일링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ [Feature] 워크스페이스 로고 업로드 API 연동 및 logoUrl 업데이트 처리

3 participants