feat:헤더 통합 검색 및 알림 구현(#69) - #69
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthrough워크스페이스 통합 검색과 알림 기능이 추가되었습니다. 검색은 여러 콘텐츠 유형을 조회하고, 알림은 데이터베이스 트리거·Realtime·읽음 처리를 통해 헤더 패널에 표시됩니다. Changes워크스페이스 검색 및 알림
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: 결과 렌더링
알림 수신 및 표시 흐름sequenceDiagram
participant AnnouncementOrTask
participant NotificationTrigger
participant SupabaseRealtime
participant NotificationPanel
AnnouncementOrTask->>NotificationTrigger: 공지 또는 업무 변경
NotificationTrigger->>SupabaseRealtime: notifications INSERT 이벤트
SupabaseRealtime-->>NotificationPanel: 사용자 알림 전달
NotificationPanel-->>NotificationPanel: 목록과 미읽음 개수 갱신
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (20)
src/app/api/workspaces/[workspaceId]/search/route.tssrc/entities/notification/api/get-notifications.tssrc/entities/notification/api/notification-actions.tssrc/entities/notification/index.tssrc/entities/notification/model/notification-query.tssrc/entities/notification/model/notification.types.tssrc/entities/workspace-search/api/search-workspace.tssrc/entities/workspace-search/index.tssrc/entities/workspace-search/model/workspace-search-query.tssrc/entities/workspace-search/model/workspace-search.types.tssrc/features/workspace-notifications/index.tssrc/features/workspace-notifications/model/use-workspace-notifications.tssrc/features/workspace-notifications/ui/NotificationPanel.tsxsrc/features/workspace-search/index.tssrc/features/workspace-search/model/use-workspace-search.tssrc/features/workspace-search/ui/WorkspaceSearchPanel.tsxsrc/shared/model/database.types.tssrc/widgets/workspace-shell/ui/WorkspaceHeader.tsxsupabase/migrations/20260716030000_create_workspace_notifications.sqlsupabase/migrations/20260716040000_fix_notification_trigger_permissions.sql
| 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, |
There was a problem hiding this comment.
🎯 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.tsxRepository: 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
| -- 수신자는 읽음 시각만 변경할 수 있고, 알림의 내용·대상·이동 경로는 불변으로 둔다. | ||
| 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(); |
There was a problem hiding this comment.
🗄️ 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.
| -- 수신자는 읽음 시각만 변경할 수 있고, 알림의 내용·대상·이동 경로는 불변으로 둔다. | |
| 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.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/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 valueTailwind 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
📒 Files selected for processing (7)
src/app/api/workspaces/[workspaceId]/search/route.tssrc/entities/notification/api/get-notifications.tssrc/entities/notification/model/notification-query.tssrc/features/workspace-notifications/model/use-workspace-notifications.tssrc/features/workspace-notifications/ui/NotificationPanel.tsxsrc/features/workspace-search/ui/WorkspaceSearchPanel.tsxsrc/widgets/workspace-shell/ui/WorkspaceHeader.tsx
| 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, | ||
| ), | ||
| }; | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
Pull Request
작업 내용
작업 결과
변경 사항
Added
notifications테이블, RLS 정책, 공지·업무 알림 트리거 및 Realtime publicationChanged
/settings?tab=members로 연결했습니다.Fixed
SECURITY DEFINER트리거 함수와 공개 실행 권한 회수로 보완했습니다.실행화면
-알림

테스트
npm run lintnpm run typechecknpm run build리뷰 체크리스트
feature/*->develop, 배포 시develop또는release/*->main)Type/#issue-number/description형식을 따릅니다.console.log, 주석, 임시 코드를 제거했습니다.리뷰 요청사항
notificationsRLS와 SECURITY DEFINER 트리거 함수가 의도한 권한 경계를 유지하는지 확인 부탁드립니다.20260716030000_create_workspace_notifications.sql20260716040000_fix_notification_trigger_permissions.sql도 추가 적용관련 이슈
Closes #69
Summary by CodeRabbit