Skip to content

feat:헤더 통합 검색 및 알림 구현(#69) - #69

Merged
seongjinss555 merged 2 commits into
developfrom
feat/#69/workspace-header-search-notifications
Jul 20, 2026
Merged

feat:헤더 통합 검색 및 알림 구현(#69)#69
seongjinss555 merged 2 commits into
developfrom
feat/#69/workspace-header-search-notifications

Conversation

@seongjinss555

@seongjinss555 seongjinss555 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Pull Request

작업 내용

  • 헤더에 현재 워크스페이스 범위의 통합 검색을 추가했습니다.
  • 헤더 벨 아이콘에 개인 알림 목록, 읽음 처리, Realtime 수신을 추가했습니다.
  • 헤더의 멤버 초대 버튼을 설정의 팀원 관리 탭으로 연결했습니다.

작업 결과

  • 공지·자료실·업무·채팅·회의록·일정을 제목 또는 내용으로 검색할 수 있습니다.
  • 검색은 두 글자 이상 입력 후 300ms debounce를 거쳐 각 원본별 최대 5건을 표시합니다.
  • 다른 멤버의 공지 작성, 내 업무 배정·상태 변경 시 최근 알림 10건과 미읽음 개수를 표시합니다.
  • 알림은 수신자만 조회·읽음 처리할 수 있고, 앱 사용자의 직접 생성은 차단했습니다.

변경 사항

Added

  • notifications 테이블, RLS 정책, 공지·업무 알림 트리거 및 Realtime publication
  • 워크스페이스 통합 검색 Route Handler 및 TanStack Query 기반 검색 UI
  • TanStack Query·Supabase Realtime 기반 알림 UI

Changed

  • 헤더 검색창과 알림 아이콘을 실제 기능으로 교체했습니다.
  • 멤버 초대 버튼을 /settings?tab=members로 연결했습니다.
  • 회의록·캘린더 일정도 통합 검색 대상에 포함했습니다.

Fixed

  • 알림 트리거가 직접 INSERT 권한 제한으로 공지 등록 전체를 롤백하던 문제를 SECURITY DEFINER 트리거 함수와 공개 실행 권한 회수로 보완했습니다.

실행화면

  • 검색 창
image

-알림
image

테스트

  • npm run lint
  • npm run typecheck
  • npm run build
  • 주요 시나리오 확인
  • 영향 범위 확인

리뷰 체크리스트

  • PR base branch가 올바릅니다. (feature/* -> develop, 배포 시 develop 또는 release/* -> main)
  • 브랜치명이 Type/#issue-number/description 형식을 따릅니다.
  • 커밋 메시지가 컨벤션을 따릅니다.
  • 불필요한 console.log, 주석, 임시 코드를 제거했습니다.
  • 타입 에러와 린트 에러를 확인했습니다.
  • CodeRabbit 1차 리뷰를 확인했습니다.
  • CodeRabbit 리뷰 반영 후 Discord에 공유했습니다.
  • 최소 1명 이상의 approve 후 merge합니다.

리뷰 요청사항

  • notifications RLS와 SECURITY DEFINER 트리거 함수가 의도한 권한 경계를 유지하는지 확인 부탁드립니다.
  • Supabase SQL Editor 또는 CLI로 아래 마이그레이션 적용이 필요합니다.
    • 20260716030000_create_workspace_notifications.sql
    • 이미 첫 파일을 적용한 환경은 20260716040000_fix_notification_trigger_permissions.sql도 추가 적용

관련 이슈

Closes #69

Summary by CodeRabbit

  • 새로운 기능
    • 워크스페이스 범위 통합 검색(공지/자료/업무/채팅/회의노트/캘린더 이벤트)과 유형별 결과 링크 제공
    • 워크스페이스 알림 패널 추가: 최근 알림 최대 10건과 미읽음 수 표시
    • 알림은 개별/전체 읽음 처리 지원, 새 알림이 실시간으로 반영
    • 헤더에서 검색 패널과 알림 패널을 함께 제어
  • 개선 사항
    • 외부 클릭 또는 ESC로 패널이 자연스럽게 닫히며, 검색어 길이/결과 유무에 따라 안내 제공

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
syncly Ready Ready Preview, Comment Jul 20, 2026 12:23am

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

워크스페이스 통합 검색과 알림 기능이 추가되었습니다. 검색은 여러 콘텐츠 유형을 조회하고, 알림은 데이터베이스 트리거·Realtime·읽음 처리를 통해 헤더 패널에 표시됩니다.

Changes

워크스페이스 검색 및 알림

Layer / File(s) Summary
통합 검색 계약과 API
src/entities/workspace-search/..., src/app/api/workspaces/.../search/route.ts
검색 결과 타입과 캐시 키, 클라이언트 호출 및 멤버십 검증·카테고리별 검색·결과 병합 API를 추가했습니다.
검색 훅과 패널 UI
src/features/workspace-search/...
300ms 디바운스, 조건부 Query 요청, 검색 상태와 유형별 결과 링크 렌더링을 구현했습니다.
알림 스키마와 생성 트리거
src/shared/model/database.types.ts, supabase/migrations/*notifications.sql
notifications 테이블과 RLS를 추가하고 공지·업무 변경 알림 생성 및 Realtime publication을 구성했습니다.
알림 조회와 읽음 처리
src/entities/notification/..., src/features/workspace-notifications/model/use-workspace-notifications.ts
최근 알림 조회, 읽음 상태 갱신, Query 캐시 업데이트와 사용자별 Realtime 구독을 구현했습니다.
알림 패널과 헤더 통합
src/features/workspace-notifications/ui/NotificationPanel.tsx, src/widgets/workspace-shell/ui/WorkspaceHeader.tsx
알림 UI와 검색·알림·메뉴의 상호 배타적 열림 상태, 외부 클릭 및 Escape 처리를 연결했습니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

검색 요청 흐름

sequenceDiagram
  participant WorkspaceSearchPanel
  participant useWorkspaceSearch
  participant searchWorkspace
  participant WorkspaceSearchRoute
  WorkspaceSearchPanel->>useWorkspaceSearch: 검색어 입력
  useWorkspaceSearch->>searchWorkspace: 디바운스된 검색어 전달
  searchWorkspace->>WorkspaceSearchRoute: GET 요청
  WorkspaceSearchRoute-->>searchWorkspace: 검색 결과 반환
  searchWorkspace-->>WorkspaceSearchPanel: 결과 렌더링
Loading

알림 수신 및 표시 흐름

sequenceDiagram
  participant AnnouncementOrTask
  participant NotificationTrigger
  participant SupabaseRealtime
  participant NotificationPanel
  AnnouncementOrTask->>NotificationTrigger: 공지 또는 업무 변경
  NotificationTrigger->>SupabaseRealtime: notifications INSERT 이벤트
  SupabaseRealtime-->>NotificationPanel: 사용자 알림 전달
  NotificationPanel-->>NotificationPanel: 목록과 미읽음 개수 갱신
Loading

Possibly related PRs

  • TeampleRun/syncly#32: database.types.ts의 Supabase 데이터베이스 타입 구성과 직접 연결됩니다.
  • TeampleRun/syncly#50: WorkspaceHeader.tsx의 props 및 렌더링 변경과 연계됩니다.

Suggested reviewers: kwon812

🚥 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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed 제목이 헤더 통합 검색과 알림 기능 추가라는 핵심 변경을 간결하게 잘 요약합니다.
Description check ✅ Passed 템플릿의 필수 섹션들이 모두 포함되어 있고, 작업 내용·결과·변경 사항·테스트가 구체적으로 작성되었습니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#69/workspace-header-search-notifications

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.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/api/workspaces/`[workspaceId]/search/route.ts:
- Around line 53-56: Update the request handler’s try/catch around
getCurrentUserId so authentication failures are detected and returned as HTTP
401 instead of being converted to a 500 response. Preserve the existing error
handling for non-authentication failures, using the handler’s established
response format.

In `@src/entities/notification/api/get-notifications.ts`:
- Around line 42-58: The notification API must derive unreadCount from the full
unread set, not the latest 10 notifications: update the query flow in
getNotifications to separately count rows where read_at is null and return that
exact count. In src/entities/notification/api/get-notifications.ts lines 42-58,
apply this API change; in
src/features/workspace-notifications/model/use-workspace-notifications.ts lines
62-79, stop recalculating unreadCount from the cached list after mutations and
instead decrement by the actual number changed or invalidate the query to
refetch the server count.

In `@src/entities/notification/model/notification-query.ts`:
- Around line 1-3: Update notificationsQueryKey to accept and include viewerId
alongside workspaceId, then update every getNotifications call site and related
query/cache usage to pass getCurrentUserId() so notification data is separated
when users switch within the same QueryClient.

In `@src/features/workspace-notifications/model/use-workspace-notifications.ts`:
- Around line 32-43: Update addNotification to create and return initial
NotificationData when currentData is undefined, so the first INSERT is cached
instead of discarded. Also update the realtime subscription using
notificationsQueryKey(workspaceId) to handle both INSERT and UPDATE events,
either by invalidating the query for both or directly applying UPDATE changes.

In `@src/features/workspace-notifications/ui/NotificationPanel.tsx`:
- Around line 49-60: Update openNotification to remove event.preventDefault(),
start markOneAsRead(notification.id) without awaiting it, and close the panel
immediately; remove manual router.push so the Link handles normal,
Cmd/Ctrl-click, and new-tab navigation.
- Around line 25-31: Update formatCreatedAt to explicitly set the product time
zone to Asia/Seoul in its Intl.DateTimeFormat options, preserving the existing
Korean locale and date/time formatting.

In `@src/features/workspace-search/ui/WorkspaceSearchPanel.tsx`:
- Around line 78-119: Update the results list rendering in WorkspaceSearchPanel
so previous results remain hidden while the query is debouncing or loading.
Render the list only when the current trimmed query matches normalizedQuery, is
not loading, is not in error, and results are non-empty; keep the existing
status messages unchanged.

In `@src/widgets/workspace-shell/ui/WorkspaceHeader.tsx`:
- Around line 71-110: Update WorkspaceHeader’s mobile layout to prevent
horizontal overflow within the 320px available width: use a compact search
trigger or otherwise recompose the title and action controls so the search,
invite, notification, and profile controls fit without overflow. Preserve the
full search panel and existing desktop layout for larger breakpoints.

In `@supabase/migrations/20260716030000_create_workspace_notifications.sql`:
- Around line 48-76: Update private.prevent_notification_mutation to validate
read_at transitions: allow only a NULL old.read_at changing to the current
timestamp via now(), reject clearing it or replacing an existing value, and
preserve the existing protections for all other notification fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a0e5bbac-da45-495b-91b8-c8c93833db0b

📥 Commits

Reviewing files that changed from the base of the PR and between 91ccb4a and 80d67ba.

📒 Files selected for processing (20)
  • src/app/api/workspaces/[workspaceId]/search/route.ts
  • src/entities/notification/api/get-notifications.ts
  • src/entities/notification/api/notification-actions.ts
  • src/entities/notification/index.ts
  • src/entities/notification/model/notification-query.ts
  • src/entities/notification/model/notification.types.ts
  • src/entities/workspace-search/api/search-workspace.ts
  • src/entities/workspace-search/index.ts
  • src/entities/workspace-search/model/workspace-search-query.ts
  • src/entities/workspace-search/model/workspace-search.types.ts
  • src/features/workspace-notifications/index.ts
  • src/features/workspace-notifications/model/use-workspace-notifications.ts
  • src/features/workspace-notifications/ui/NotificationPanel.tsx
  • src/features/workspace-search/index.ts
  • src/features/workspace-search/model/use-workspace-search.ts
  • src/features/workspace-search/ui/WorkspaceSearchPanel.tsx
  • src/shared/model/database.types.ts
  • src/widgets/workspace-shell/ui/WorkspaceHeader.tsx
  • supabase/migrations/20260716030000_create_workspace_notifications.sql
  • supabase/migrations/20260716040000_fix_notification_trigger_permissions.sql

Comment thread src/app/api/workspaces/[workspaceId]/search/route.ts Outdated
Comment on lines +42 to +58
const { data, error } = await supabase
.from('notifications')
.select('id, workspace_id, type, title, body, link_path, read_at, created_at')
.eq('workspace_id', parsedWorkspaceId)
.eq('recipient_id', currentUserId)
.order('created_at', { ascending: false })
.limit(10);

if (error) {
console.error('[notification] 알림 조회 실패:', error);
throw new Error('알림을 불러오지 못했습니다.');
}

const notifications = (data ?? []).map(toNotificationItem);
return {
notifications,
unreadCount: notifications.filter((notification) => !notification.readAt).length,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== get-notifications.ts ==\n'
sed -n '1,220p' src/entities/notification/api/get-notifications.ts

printf '\n== use-workspace-notifications.ts ==\n'
sed -n '1,260p' src/features/workspace-notifications/model/use-workspace-notifications.ts

printf '\n== notification-related files ==\n'
rg -n "unreadCount|notificationsQueryKey|readAt|read_at|count\(|select\(" src/entities/notification src/features/workspace-notifications -g '!**/node_modules/**'

Repository: TeampleRun/syncly

Length of output: 9318


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== notification-actions.ts ==\n'
sed -n '1,220p' src/entities/notification/api/notification-actions.ts

printf '\n== notification.types.ts ==\n'
sed -n '1,200p' src/entities/notification/model/notification.types.ts

printf '\n== NotificationPanel.tsx ==\n'
sed -n '1,220p' src/features/workspace-notifications/ui/NotificationPanel.tsx

Repository: TeampleRun/syncly

Length of output: 7669


최근 10건에서 unreadCount를 계산하지 마세요.
읽지 않은 알림이 10건을 넘고 최신 10건이 모두 읽음이면 unreadCount가 0으로 떨어져 전체 읽음이 비활성화될 수 있습니다.

  • src/entities/notification/api/get-notifications.ts: read_at IS NULL 조건의 정확한 count를 별도로 가져와 unreadCount에 넣으세요.
  • src/features/workspace-notifications/model/use-workspace-notifications.ts: mutation 뒤에 현재 캐시 목록만으로 unreadCount를 다시 세지 말고, 실제 변경 수만큼 감소시키거나 쿼리를 무효화해 서버 count를 다시 받으세요.
📍 Affects 2 files
  • src/entities/notification/api/get-notifications.ts#L42-L58 (this comment)
  • src/features/workspace-notifications/model/use-workspace-notifications.ts#L62-L79
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/entities/notification/api/get-notifications.ts` around lines 42 - 58, The
notification API must derive unreadCount from the full unread set, not the
latest 10 notifications: update the query flow in getNotifications to separately
count rows where read_at is null and return that exact count. In
src/entities/notification/api/get-notifications.ts lines 42-58, apply this API
change; in
src/features/workspace-notifications/model/use-workspace-notifications.ts lines
62-79, stop recalculating unreadCount from the cached list after mutations and
instead decrement by the actual number changed or invalidate the query to
refetch the server count.

Source: MCP tools

Comment thread src/entities/notification/model/notification-query.ts Outdated
Comment thread src/features/workspace-notifications/ui/NotificationPanel.tsx
Comment thread src/features/workspace-notifications/ui/NotificationPanel.tsx Outdated
Comment thread src/features/workspace-search/ui/WorkspaceSearchPanel.tsx
Comment thread src/widgets/workspace-shell/ui/WorkspaceHeader.tsx Outdated
Comment on lines +48 to +76
-- 수신자는 읽음 시각만 변경할 수 있고, 알림의 내용·대상·이동 경로는 불변으로 둔다.
create function private.prevent_notification_mutation()
returns trigger
language plpgsql
security invoker
set search_path = ''
as $$
begin
if new.id is distinct from old.id
or new.recipient_id is distinct from old.recipient_id
or new.actor_id is distinct from old.actor_id
or new.workspace_id is distinct from old.workspace_id
or new.type is distinct from old.type
or new.title is distinct from old.title
or new.body is distinct from old.body
or new.link_path is distinct from old.link_path
or new.metadata is distinct from old.metadata
or new.created_at is distinct from old.created_at then
raise exception '알림의 읽음 상태 외 정보는 변경할 수 없습니다.';
end if;

return new;
end;
$$;

create trigger prevent_notification_mutation
before update on public.notifications
for each row
execute function private.prevent_notification_mutation();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

read_at을 되돌리거나 임의 시각으로 덮어쓰지 못하게 하세요.

현재 트리거는 다른 열만 보호하므로 수신자가 직접 read_at = null 또는 임의 시각을 저장할 수 있습니다. NULL → now() 전이만 허용하고 이미 읽은 상태는 불변으로 유지해야 합니다.

수정 예시
   if new.id is distinct from old.id
     ...
     raise exception '알림의 읽음 상태 외 정보는 변경할 수 없습니다.';
   end if;

+  if old.read_at is not null
+    and new.read_at is distinct from old.read_at then
+    raise exception '이미 읽은 알림의 읽음 상태는 변경할 수 없습니다.';
+  end if;
+
+  if old.read_at is null then
+    if new.read_at is null then
+      raise exception '읽음 시각이 필요합니다.';
+    end if;
+    new.read_at := now();
+  end if;
+
   return new;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- 수신자는 읽음 시각만 변경할 수 있고, 알림의 내용·대상·이동 경로는 불변으로 둔다.
create function private.prevent_notification_mutation()
returns trigger
language plpgsql
security invoker
set search_path = ''
as $$
begin
if new.id is distinct from old.id
or new.recipient_id is distinct from old.recipient_id
or new.actor_id is distinct from old.actor_id
or new.workspace_id is distinct from old.workspace_id
or new.type is distinct from old.type
or new.title is distinct from old.title
or new.body is distinct from old.body
or new.link_path is distinct from old.link_path
or new.metadata is distinct from old.metadata
or new.created_at is distinct from old.created_at then
raise exception '알림의 읽음 상태 외 정보는 변경할 수 없습니다.';
end if;
return new;
end;
$$;
create trigger prevent_notification_mutation
before update on public.notifications
for each row
execute function private.prevent_notification_mutation();
-- 수신자는 읽음 시각만 변경할 수 있고, 알림의 내용·대상·이동 경로는 불변으로 둔다.
create function private.prevent_notification_mutation()
returns trigger
language plpgsql
security invoker
set search_path = ''
as $$
begin
if new.id is distinct from old.id
or new.recipient_id is distinct from old.recipient_id
or new.actor_id is distinct from old.actor_id
or new.workspace_id is distinct from old.workspace_id
or new.type is distinct from old.type
or new.title is distinct from old.title
or new.body is distinct from old.body
or new.link_path is distinct from old.link_path
or new.metadata is distinct from old.metadata
or new.created_at is distinct from old.created_at then
raise exception '알림의 읽음 상태 외 정보는 변경할 수 없습니다.';
end if;
if old.read_at is not null
and new.read_at is distinct from old.read_at then
raise exception '이미 읽은 알림의 읽음 상태는 변경할 수 없습니다.';
end if;
if old.read_at is null then
if new.read_at is null then
raise exception '읽음 시각이 필요합니다.';
end if;
new.read_at := now();
end if;
return new;
end;
$$;
create trigger prevent_notification_mutation
before update on public.notifications
for each row
execute function private.prevent_notification_mutation();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260716030000_create_workspace_notifications.sql` around
lines 48 - 76, Update private.prevent_notification_mutation to validate read_at
transitions: allow only a NULL old.read_at changing to the current timestamp via
now(), reject clearing it or replacing an existing value, and preserve the
existing protections for all other notification fields.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/app/api/workspaces/[workspaceId]/search/route.ts (1)

88-168: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

검색 데이터베이스 쿼리 최적화 및 커넥션 풀 관리.

현재 통합 검색을 위해 11개의 테이블/컬럼에 대해 ilike 검색을 Promise.all로 병렬 실행하고 있습니다.
이는 서버리스 환경에서 검색 요청 1건당 11개의 데이터베이스 커넥션(또는 트랜잭션)을 순간적으로 점유하게 되며, 여러 사용자가 동시에 검색할 경우 Supabase의 Connection Pool 한도를 쉽게 고갈시킬 위험이 있습니다.

운영 안정성을 위해 장기적으로 다음과 같은 아키텍처 개선을 고려해 보세요:

  • Full Text Search & RPC: PostgreSQL의 to_tsvector/to_tsquery를 활용하고, 검색 로직을 데이터베이스 내부의 단일 RPC(Stored Procedure)로 통합하여 한 번의 호출로 처리합니다.
  • 통합 검색용 Materialized View: 검색 대상 데이터를 모아둔 Materialized View나 별도의 검색 전용 테이블을 구축하여 단일 쿼리로 조회할 수 있도록 변경합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/workspaces/`[workspaceId]/search/route.ts around lines 88 - 168,
Replace the 11 parallel Supabase queries in the integrated search flow with a
single database call, preferably an RPC backed by PostgreSQL full-text search
that covers the existing announcement, resource, task, chat message, meeting
note, and calendar event fields. Update the surrounding result mapping to
consume the unified response while preserving workspace scoping, search
behavior, and the existing result limit.
src/widgets/workspace-shell/ui/WorkspaceHeader.tsx (1)

89-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tailwind CSS v4 문법에 맞게 CSS 변수 참조 방식을 간소화하세요.

Tailwind CSS v4에서는 CSS 변수를 참조할 때 [var(--variable)] 형태의 임의 값(Arbitrary values) 구문 대신 괄호를 사용하는 (--variable) 단축 문법을 권장합니다.

♻️ 제안하는 수정안
         <Link
           href={`/workspaces/${workspaceId}/settings?tab=members`}
-          className="flex h-10 shrink-0 items-center gap-2 rounded-xl bg-[var(--color-brand)] px-3 text-sm font-bold whitespace-nowrap text-white hover:bg-indigo-500 sm:px-4"
+          className="flex h-10 shrink-0 items-center gap-2 rounded-xl bg-(--color-brand) px-3 text-sm font-bold whitespace-nowrap text-white hover:bg-indigo-500 sm:px-4"
         >
           <UserRoundPlus className="h-4 w-4" aria-hidden="true" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/widgets/workspace-shell/ui/WorkspaceHeader.tsx` around lines 89 - 95,
Update the className on the invite Link in WorkspaceHeader to use Tailwind CSS
v4’s parenthesized CSS-variable syntax for the brand background color, replacing
the current arbitrary-value var(--color-brand) form while preserving the other
classes and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/workspace-notifications/model/use-workspace-notifications.ts`:
- Around line 51-63: Update updateNotification so replacing an existing
notification also recalculates unreadCount from the resulting notifications
array, preserving the current count when no notification data exists and keeping
the optimistic cache update consistent with each item’s read state.

---

Outside diff comments:
In `@src/app/api/workspaces/`[workspaceId]/search/route.ts:
- Around line 88-168: Replace the 11 parallel Supabase queries in the integrated
search flow with a single database call, preferably an RPC backed by PostgreSQL
full-text search that covers the existing announcement, resource, task, chat
message, meeting note, and calendar event fields. Update the surrounding result
mapping to consume the unified response while preserving workspace scoping,
search behavior, and the existing result limit.

In `@src/widgets/workspace-shell/ui/WorkspaceHeader.tsx`:
- Around line 89-95: Update the className on the invite Link in WorkspaceHeader
to use Tailwind CSS v4’s parenthesized CSS-variable syntax for the brand
background color, replacing the current arbitrary-value var(--color-brand) form
while preserving the other classes and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eada443a-828e-4289-b090-e12c31cb7593

📥 Commits

Reviewing files that changed from the base of the PR and between 80d67ba and 7c6d5c9.

📒 Files selected for processing (7)
  • src/app/api/workspaces/[workspaceId]/search/route.ts
  • src/entities/notification/api/get-notifications.ts
  • src/entities/notification/model/notification-query.ts
  • src/features/workspace-notifications/model/use-workspace-notifications.ts
  • src/features/workspace-notifications/ui/NotificationPanel.tsx
  • src/features/workspace-search/ui/WorkspaceSearchPanel.tsx
  • src/widgets/workspace-shell/ui/WorkspaceHeader.tsx

Comment on lines +51 to +63
function updateNotification(
currentData: NotificationData | undefined,
notification: NotificationItem,
): NotificationData | undefined {
if (!currentData) return currentData;

return {
...currentData,
notifications: currentData.notifications.map((item) =>
item.id === notification.id ? notification : item,
),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Realtime 이벤트 수신 시 미읽음 카운트(unreadCount)도 함께 갱신해 UI 깜빡임을 방지하세요.

UPDATE 이벤트가 발생해 알림의 읽음 상태가 변경되었을 때, notifications 배열의 항목은 올바르게 교체되지만 unreadCount는 갱신되지 않고 있습니다. 직후에 호출되는 invalidateQueries를 통해 백그라운드 패치가 완료되면 최종 숫자는 맞춰지지만, 네트워크 요청이 처리되는 동안 배지 숫자가 이전 상태에 머무르며 깜빡일 수 있습니다.

💡 제안하는 수정안 (로컬 캐시 낙관적 업데이트)
 function updateNotification(
   currentData: NotificationData | undefined,
   notification: NotificationItem,
 ): NotificationData | undefined {
   if (!currentData) return currentData;
 
+  const oldItem = currentData.notifications.find((item) => item.id === notification.id);
+  let unreadCount = currentData.unreadCount;
+
+  if (oldItem) {
+    const wasUnread = !oldItem.readAt;
+    const isNowRead = !!notification.readAt;
+    
+    if (wasUnread && isNowRead) unreadCount = Math.max(0, unreadCount - 1);
+    else if (!wasUnread && !isNowRead) unreadCount += 1;
+  }
+
   return {
     ...currentData,
     notifications: currentData.notifications.map((item) =>
       item.id === notification.id ? notification : item,
     ),
+    unreadCount,
   };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function updateNotification(
currentData: NotificationData | undefined,
notification: NotificationItem,
): NotificationData | undefined {
if (!currentData) return currentData;
return {
...currentData,
notifications: currentData.notifications.map((item) =>
item.id === notification.id ? notification : item,
),
};
}
function updateNotification(
currentData: NotificationData | undefined,
notification: NotificationItem,
): NotificationData | undefined {
if (!currentData) return currentData;
const oldItem = currentData.notifications.find((item) => item.id === notification.id);
let unreadCount = currentData.unreadCount;
if (oldItem) {
const wasUnread = !oldItem.readAt;
const isNowRead = !!notification.readAt;
if (wasUnread && isNowRead) unreadCount = Math.max(0, unreadCount - 1);
else if (!wasUnread && !isNowRead) unreadCount += 1;
}
return {
...currentData,
notifications: currentData.notifications.map((item) =>
item.id === notification.id ? notification : item,
),
unreadCount,
};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspace-notifications/model/use-workspace-notifications.ts`
around lines 51 - 63, Update updateNotification so replacing an existing
notification also recalculates unreadCount from the resulting notifications
array, preserving the current count when no notification data exists and keeping
the optimistic cache update consistent with each item’s read state.

@wjswlgh96 wjswlgh96 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

확인했습니다~ 고생하셨습니다^^

@Kwon812 Kwon812 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

고생하셨습니다~

@seongjinss555
seongjinss555 merged commit dab6849 into develop Jul 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants