Skip to content

feat : to-sing 곡 순서 변경 및 삭제 낙관적 업데이트 적용 (#230)#231

Open
GulSam00 wants to merge 1 commit into
developfrom
feat/230-toSingOptimisticUpdate
Open

feat : to-sing 곡 순서 변경 및 삭제 낙관적 업데이트 적용 (#230)#231
GulSam00 wants to merge 1 commit into
developfrom
feat/230-toSingOptimisticUpdate

Conversation

@GulSam00
Copy link
Copy Markdown
Owner

@GulSam00 GulSam00 commented May 7, 2026

User description

📌 변경 사항

  • useDeleteToSingSongMutation onMutate 버그 수정: old.filter() 결과를 return 하지 않아 undefined가 캐시에 설정되던 문제 수정
  • useDeleteToSingSongMutation onSettled에서 invalidateQueries(['toSingSong']) 제거 — 낙관적 업데이트가 서버 재요청으로 덮어씌워지던 문제 수정
  • usePatchToSingSongMutation onSettled에서 invalidateQueries(['toSingSong']) 제거 — 동일 이유
  • 삭제/순서 변경 실패 시 onError에서 이전 상태 롤백은 그대로 유지

💬 추가 참고 사항


PR Type

Bug fix, Enhancement


Description

  • Fix useDeleteToSingSongMutation missing return statement in onMutate

  • Remove unnecessary invalidateQueries calls from both mutations

  • Prevent optimistic updates from being overwritten by server refetches

  • Maintain rollback functionality on mutation failures


Diagram Walkthrough

flowchart LR
  A["Delete/Patch Mutation"] -->|onMutate| B["Cancel & Cache Previous State"]
  B -->|Optimistic Update| C["Update UI Immediately"]
  C -->|onError| D["Rollback to Previous State"]
  C -->|onSuccess| E["Keep Optimistic Update"]
  E -->|onSettled| F["Invalidate searchSong Only"]
Loading

File Walkthrough

Relevant files
Bug fix
tosingSongQuery.ts
Fix optimistic updates and remove query invalidation         

apps/web/src/queries/tosingSongQuery.ts

  • Add missing return statement in useDeleteToSingSongMutation onMutate
    callback to properly filter songs
  • Remove invalidateQueries(['toSingSong']) from onSettled in
    useDeleteToSingSongMutation to prevent optimistic update override
  • Remove invalidateQueries(['toSingSong']) from onSettled in
    usePatchToSingSongMutation for consistency
  • Remove commented-out debounce logic from onSettled callback
+1/-12   

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 7, 2026

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

Project Deployment Actions Updated (UTC)
singcode Ready Ready Preview, Comment May 7, 2026 5:16am

@GulSam00
Copy link
Copy Markdown
Owner Author

GulSam00 commented May 7, 2026

/describe

@GulSam00
Copy link
Copy Markdown
Owner Author

GulSam00 commented May 7, 2026

/review

@GulSam00
Copy link
Copy Markdown
Owner Author

GulSam00 commented May 7, 2026

/improve

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Fix optimistic updates for to-sing song mutations

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• Fix useDeleteToSingSongMutation missing return statement in onMutate
• Remove redundant invalidateQueries calls from both mutations
• Prevent optimistic updates from being overwritten by server refetch
• Maintain rollback functionality on mutation failure
Diagram
flowchart LR
  A["Delete/Patch Mutation"] -->|onMutate| B["Cancel & Cache Previous State"]
  B -->|setQueryData| C["Apply Optimistic Update"]
  C -->|onError| D["Rollback to Previous State"]
  C -->|onSuccess| E["Keep Optimistic Data"]
  E -->|onSettled| F["Invalidate searchSong Only"]
Loading

Grey Divider

File Changes

1. apps/web/src/queries/tosingSongQuery.ts 🐞 Bug fix +1/-12

Fix optimistic updates and remove redundant cache invalidation

• Add missing return statement in useDeleteToSingSongMutation onMutate callback to properly
 filter and cache deleted song
• Remove invalidateQueries(['toSingSong']) from onSettled in useDeleteToSingSongMutation to
 prevent overwriting optimistic updates
• Remove invalidateQueries(['toSingSong']) from onSettled in usePatchToSingSongMutation for
 consistency
• Remove commented-out debounce logic that was no longer needed

apps/web/src/queries/tosingSongQuery.ts


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 7, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (1)

Context used

Grey Divider


Action required

1. old.filter without null-guard 📎 Requirement gap ☼ Reliability
Description
The optimistic delete updater assumes old is always an array, but React Query can pass undefined
when the cache is empty/unset, causing a runtime exception. This can prevent the song from being
removed immediately and break the expected optimistic-update behavior for deletion.
Code

apps/web/src/queries/tosingSongQuery.ts[R60-62]

     queryClient.setQueryData(['toSingSong'], (old: ToSingSong[]) => {
-        old.filter(song => song.songs.id !== songId);
+        return old.filter(song => song.songs.id !== songId);
     });
Evidence
PR Compliance ID 2 requires delete to optimistically remove the song immediately; however the
updater can throw if old is undefined, preventing the UI/cache update. The changed line uses
old.filter(...) without handling an unset cache value.

to-sing 페이지 곡 삭제 시 낙관적 업데이트 적용 및 실패 롤백
apps/web/src/queries/tosingSongQuery.ts[60-62]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The optimistic delete cache updater calls `old.filter(...)` without guarding for `old` possibly being `undefined`.
## Issue Context
React Query's `setQueryData` updater may receive `undefined` if the query cache is not yet set. A runtime error here breaks the optimistic delete behavior required for the to-sing page.
## Fix Focus Areas
- apps/web/src/queries/tosingSongQuery.ts[60-62]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reorder uses stale weights 🐞 Bug ≡ Correctness
Description
usePatchToSingSongMutation replaces the cached list with newItems but never updates the moved item’s
order_weight, and this PR removed the post-mutation invalidate of ['toSingSong'], so cached
order_weight stays stale for up to 5 minutes. Subsequent drag/drop operations compute newWeight from
these stale order_weight values and can persist incorrect weights to the server, causing ordering to
“jump” after a later refetch/reload.
Code

apps/web/src/queries/tosingSongQuery.ts[L109-111]

-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: ['toSingSong'] });
-    },
Evidence
The reorder UI computes the next mutation’s newWeight from the current cached items’ order_weight.
The patch mutation’s optimistic update only reorders the array (newItems) and does not adjust
order_weight for the moved song, and patchToSingSong returns void so nothing updates the cache. With
invalidateQueries(['toSingSong']) removed and refetch-on-focus disabled + 5min staleTime, the stale
order_weight persists and directly affects subsequent newWeight calculations.

apps/web/src/hooks/useToSingSong.ts[38-65]
apps/web/src/queries/tosingSongQuery.ts[76-101]
apps/web/src/lib/api/tosing.ts[18-21]
apps/web/src/query.tsx[10-18]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
After a successful reorder, the cache keeps the previous `order_weight` values because the optimistic update only reorders `newItems` and `patchToSingSong` returns `void`. Since this PR removed `invalidateQueries(['toSingSong'])`, subsequent reorder operations compute `newWeight` from stale `order_weight` values and can send incorrect weights to the server.
### Issue Context
- `useToSingSong` computes `newWeight` using neighbor items’ `order_weight`.
- `usePatchToSingSongMutation` sets `['toSingSong']` cache to `newItems` as-is.
- `QueryClient` defaults prevent fast refetches (`refetchOnWindowFocus: false`, `staleTime: 5m`).
### Fix Focus Areas
- apps/web/src/queries/tosingSongQuery.ts[80-100]
- apps/web/src/hooks/useToSingSong.ts[43-64]
### Suggested fix
1. In `usePatchToSingSongMutation.onMutate`, destructure `{ songId, newWeight, newItems }` (not just `newItems`).
2. Before calling `setQueryData`, create `optimisticItems` that updates the moved item’s `order_weight` to `newWeight` (e.g., `map` by `songs.id`), then set the cache to `optimisticItems`.
3. Optionally, add a debounced `invalidateQueries(['toSingSong'])` in `onSuccess` (not `onSettled`) if you still want periodic server reconciliation without overriding in-flight optimistic interactions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Unawaited cancelQueries 🐞 Bug ☼ Reliability ⭐ New
Description
onMutate에서 queryClient.cancelQueries(...)await하지 않아, 진행 중이던 toSingSong fetch가 낙관적 업데이트 이후에
완료되며 캐시를 덮어쓸 가능성이 남아 있습니다. 같은 repo의 다른 optimistic mutation은 await cancelQueries로 이 순서를 보장하고 있습니다.
Code

apps/web/src/queries/tosingSongQuery.ts[R58-60]

      queryClient.cancelQueries({ queryKey: ['toSingSong'] });
      const prev = queryClient.getQueryData(['toSingSong']);
      queryClient.setQueryData(['toSingSong'], (old: ToSingSong[]) => {
-        old.filter(song => song.songs.id !== songId);
Evidence
toSingSong delete/patch 모두 cancelQueries 직후에 getQueryData로 스냅샷을 잡고 setQueryData로 optimistic
update를 적용합니다. repo 내 useDeleteMyReportMutation은 동일 패턴에서 await cancelQueries를 사용해 먼저 취소가 완료되도록
보장합니다.

apps/web/src/queries/tosingSongQuery.ts[55-64]
apps/web/src/queries/tosingSongQuery.ts[80-95]
apps/web/src/queries/reportSongQuery.ts[45-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`useDeleteToSingSongMutation` / `usePatchToSingSongMutation`의 `onMutate`에서 `cancelQueries`를 await하지 않아, optimistic update 직후에 in-flight query 결과가 캐시를 덮어쓸 여지가 있습니다.

### Issue Context
같은 코드베이스의 다른 optimistic mutation은 `await queryClient.cancelQueries(...)`를 사용합니다.

### Fix Focus Areas
- apps/web/src/queries/tosingSongQuery.ts[55-64]
- apps/web/src/queries/tosingSongQuery.ts[80-95]

### Concrete fix idea
- 두 onMutate 모두 다음처럼 변경:
 - `await queryClient.cancelQueries({ queryKey: ['toSingSong'] });`
- 그 다음에 `getQueryData` 스냅샷/`setQueryData` optimistic update를 수행하세요.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 518938c

Results up to commit 518938c


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (1)

Context used

Action required
1. old.filter without null-guard 📎 Requirement gap ☼ Reliability
Description
The optimistic delete updater assumes old is always an array, but React Query can pass undefined
when the cache is empty/unset, causing a runtime exception. This can prevent the song from being
removed immediately and break the expected optimistic-update behavior for deletion.
Code

apps/web/src/queries/tosingSongQuery.ts[R60-62]

      queryClient.setQueryData(['toSingSong'], (old: ToSingSong[]) => {
-        old.filter(song => song.songs.id !== songId);
+        return old.filter(song => song.songs.id !== songId);
      });
Evidence
PR Compliance ID 2 requires delete to optimistically remove the song immediately; however the
updater can throw if old is undefined, preventing the UI/cache update. The changed line uses
old.filter(...) without handling an unset cache value.

to-sing 페이지 곡 삭제 시 낙관적 업데이트 적용 및 실패 롤백
apps/web/src/queries/tosingSongQuery.ts[60-62]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The optimistic delete cache updater calls `old.filter(...)` without guarding for `old` possibly being `undefined`.

## Issue Context
React Query's `setQueryData` updater may receive `undefined` if the query cache is not yet set. A runtime error here breaks the optimistic delete behavior required for the to-sing page.

## Fix Focus Areas
- apps/web/src/queries/tosingSongQuery.ts[60-62]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reorder uses stale weights 🐞 Bug ≡ Correctness
Description
usePatchToSingSongMutation replaces the cached list with newItems but never updates the moved item’s
order_weight, and this PR removed the post-mutation invalidate of ['toSingSong'], so cached
order_weight stays stale for up to 5 minutes. Subsequent drag/drop operations compute newWeight from
these stale order_weight values and can persist incorrect weights to the server, causing ordering to
“jump” after a later refetch/reload.
Code

apps/web/src/queries/tosingSongQuery.ts[L109-111]

-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: ['toSingSong'] });
-    },
Evidence
The reorder UI computes the next mutation’s newWeight from the current cached items’ order_weight.
The patch mutation’s optimistic update only reorders the array (newItems) and does not adjust
order_weight for the moved song, and patchToSingSong returns void so nothing updates the cache. With
invalidateQueries(['toSingSong']) removed and refetch-on-focus disabled + 5min staleTime, the stale
order_weight persists and directly affects subsequent newWeight calculations.

apps/web/src/hooks/useToSingSong.ts[38-65]
apps/web/src/queries/tosingSongQuery.ts[76-101]
apps/web/src/lib/api/tosing.ts[18-21]
apps/web/src/query.tsx[10-18]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
After a successful reorder, the cache keeps the previous `order_weight` values because the optimistic update only reorders `newItems` and `patchToSingSong` returns `void`. Since this PR removed `invalidateQueries(['toSingSong'])`, subsequent reorder operations compute `newWeight` from stale `order_weight` values and can send incorrect weights to the server.

### Issue Context
- `useToSingSong` computes `newWeight` using neighbor items’ `order_weight`.
- `usePatchToSingSongMutation` sets `['toSingSong']` cache to `newItems` as-is.
- `QueryClient` defaults prevent fast refetches (`refetchOnWindowFocus: false`, `staleTime: 5m`).

### Fix Focus Areas
- apps/web/src/queries/tosingSongQuery.ts[80-100]
- apps/web/src/hooks/useToSingSong.ts[43-64]

### Suggested fix
1. In `usePatchToSingSongMutation.onMutate`, destructure `{ songId, newWeight, newItems }` (not just `newItems`).
2. Before calling `setQueryData`, create `optimisticItems` that updates the moved item’s `order_weight` to `newWeight` (e.g., `map` by `songs.id`), then set the cache to `optimisticItems`.
3. Optionally, add a debounced `invalidateQueries(['toSingSong'])` in `onSuccess` (not `onSettled`) if you still want periodic server reconciliation without overriding in-flight optimistic interactions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

Comment on lines 60 to 62
queryClient.setQueryData(['toSingSong'], (old: ToSingSong[]) => {
old.filter(song => song.songs.id !== songId);
return old.filter(song => song.songs.id !== songId);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. old.filter without null-guard 📎 Requirement gap ☼ Reliability

The optimistic delete updater assumes old is always an array, but React Query can pass undefined
when the cache is empty/unset, causing a runtime exception. This can prevent the song from being
removed immediately and break the expected optimistic-update behavior for deletion.
Agent Prompt
## Issue description
The optimistic delete cache updater calls `old.filter(...)` without guarding for `old` possibly being `undefined`.

## Issue Context
React Query's `setQueryData` updater may receive `undefined` if the query cache is not yet set. A runtime error here breaks the optimistic delete behavior required for the to-sing page.

## Fix Focus Areas
- apps/web/src/queries/tosingSongQuery.ts[60-62]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

alert(error.message ?? 'PATCH 실패');
queryClient.setQueryData(['toSingSong'], context?.prev);
},
onSettled: () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Reorder uses stale weights 🐞 Bug ≡ Correctness

usePatchToSingSongMutation replaces the cached list with newItems but never updates the moved item’s
order_weight, and this PR removed the post-mutation invalidate of ['toSingSong'], so cached
order_weight stays stale for up to 5 minutes. Subsequent drag/drop operations compute newWeight from
these stale order_weight values and can persist incorrect weights to the server, causing ordering to
“jump” after a later refetch/reload.
Agent Prompt
### Issue description
After a successful reorder, the cache keeps the previous `order_weight` values because the optimistic update only reorders `newItems` and `patchToSingSong` returns `void`. Since this PR removed `invalidateQueries(['toSingSong'])`, subsequent reorder operations compute `newWeight` from stale `order_weight` values and can send incorrect weights to the server.

### Issue Context
- `useToSingSong` computes `newWeight` using neighbor items’ `order_weight`.
- `usePatchToSingSongMutation` sets `['toSingSong']` cache to `newItems` as-is.
- `QueryClient` defaults prevent fast refetches (`refetchOnWindowFocus: false`, `staleTime: 5m`).

### Fix Focus Areas
- apps/web/src/queries/tosingSongQuery.ts[80-100]
- apps/web/src/hooks/useToSingSong.ts[43-64]

### Suggested fix
1. In `usePatchToSingSongMutation.onMutate`, destructure `{ songId, newWeight, newItems }` (not just `newItems`).
2. Before calling `setQueryData`, create `optimisticItems` that updates the moved item’s `order_weight` to `newWeight` (e.g., `map` by `songs.id`), then set the cache to `optimisticItems`.
3. Optionally, add a debounced `invalidateQueries(['toSingSong'])` in `onSuccess` (not `onSettled`) if you still want periodic server reconciliation without overriding in-flight optimistic interactions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 7, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 7, 2026

Persistent review updated to latest commit 518938c

@qodo-code-review
Copy link
Copy Markdown

PR Description updated to latest commit (518938c)

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.

to-sing 페이지 곡 순서 변경 및 삭제 낙관적 업데이트 적용

1 participant