Skip to content

[Refactor] : thumb-up API를 thumb_logs 개별 로그 기반으로 변경 (#169)#171

Merged
GulSam00 merged 3 commits intodevelopfrom
refactor/169-thumbUpLogBased
Mar 25, 2026
Merged

[Refactor] : thumb-up API를 thumb_logs 개별 로그 기반으로 변경 (#169)#171
GulSam00 merged 3 commits intodevelopfrom
refactor/169-thumbUpLogBased

Conversation

@GulSam00
Copy link
Copy Markdown
Owner

@GulSam00 GulSam00 commented Mar 25, 2026

User description

📌 PR 제목

[Refactor] : thumb-up API를 thumb_logs 개별 로그 기반으로 변경

📌 변경 사항

  • thumb-up GET API: total_stats 단일 행 조회 → thumb_logs 전체 조회 후 앱 레벨에서 song_id별 thumb_count 합계 집계, 상위 50개 반환
  • thumb-up PATCH → POST: upsert 로직 제거, getAuthenticatedUser()로 인증 후 song_id + user_id + thumb_count 단순 insert
  • 검색 API: total_statsthumb_logs 조인 변경, 배열 합산(reduce)으로 thumb 계산
  • 클라이언트 API: patchSongThumbpostSongThumb, HTTP 메서드 PATCH → POST
  • TanStack Query 훅: import 및 mutationFn 변경
  • 타입: ThumbUpSong.total_thumbthumb_count 필드명 변경

💬 추가 참고 사항


PR Type

Enhancement, Refactor


Description

  • Refactored thumb-up API from aggregated stats to individual log-based approach

  • Changed GET endpoint to query thumb_logs table and aggregate at application level

  • Converted PATCH to POST method with simplified insert-only logic

  • Updated search API to calculate thumbs from thumb_logs array using reduce

  • Renamed field from total_thumb to thumb_count across codebase


Diagram Walkthrough

flowchart LR
  A["thumb_logs table"] -->|"query all logs"| B["Application aggregation"]
  B -->|"group by song_id"| C["Calculate thumb totals"]
  C -->|"sort & slice top 50"| D["Fetch song details"]
  D -->|"merge & return"| E["ThumbUpSong[] response"]
  F["POST request"] -->|"insert log entry"| A
Loading

File Walkthrough

Relevant files
Refactor
route.ts
Refactor thumb-up endpoint to log-based aggregation           

apps/web/src/app/api/songs/thumb-up/route.ts

  • Refactored GET endpoint to query thumb_logs table and aggregate
    results at application level instead of querying total_stats
  • Changed PATCH method to POST with simplified insert-only logic
    removing upsert complexity
  • Added getAuthenticatedUser() call for user authentication in POST
    handler
  • Updated error messages and logging from "like API" to "thumb-up API"
+60/-41 
route.ts
Update search API to use thumb_logs aggregation                   

apps/web/src/app/api/search/route.ts

  • Updated database interface to use thumb_logs array instead of
    total_stats object
  • Changed field from total_thumb to thumb_count in type definitions
  • Modified both authenticated and unauthenticated query paths to join
    thumb_logs instead of total_stats
  • Updated thumb calculation to use reduce() to sum thumb_count from logs
    array
+7/-7     
thumbSong.ts
Update client API method from PATCH to POST                           

apps/web/src/lib/api/thumbSong.ts

  • Renamed function from patchSongThumb to postSongThumb
  • Changed HTTP method from PATCH to POST in API call
+2/-2     
songThumbQuery.ts
Update TanStack Query hook to use POST method                       

apps/web/src/queries/songThumbQuery.ts

  • Updated import to use postSongThumb instead of patchSongThumb
  • Changed mutation function to call postSongThumb
  • Updated error alert message from "PATCH 실패" to "POST 실패"
+3/-3     
song.ts
Update ThumbUpSong type field name                                             

apps/web/src/types/song.ts

  • Renamed ThumbUpSong interface field from total_thumb to thumb_count
+1/-1     
PopularRankingList.tsx
Update popular ranking to use thumb_count field                   

apps/web/src/app/popular/PopularRankingList.tsx

  • Updated RankingItem component prop from item.total_thumb to
    item.thumb_count
+1/-1     

@GulSam00
Copy link
Copy Markdown
Owner Author

/describe

@GulSam00
Copy link
Copy Markdown
Owner Author

/review

@GulSam00
Copy link
Copy Markdown
Owner Author

/improve

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Refactor thumb-up API to use individual thumb_logs records

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Refactored thumb-up API from total_stats aggregation to individual thumb_logs records
• Changed POST endpoint from PATCH to POST with simplified insert logic
• Implemented app-level aggregation for song thumb counts with top 50 ranking
• Updated search API to join thumb_logs and calculate thumbs via reduce
• Renamed field from total_thumb to thumb_count across codebase
Diagram
flowchart LR
  A["thumb_logs table"] -->|"fetch all records"| B["App-level aggregation"]
  B -->|"group by song_id"| C["Calculate thumb counts"]
  C -->|"sort & slice top 50"| D["Fetch song details"]
  D -->|"merge & return"| E["ThumbUpSong[] response"]
  F["POST request"] -->|"insert log entry"| A
Loading

Grey Divider

File Changes

1. apps/web/src/app/api/songs/thumb-up/route.ts Refactor +60/-41

Refactor thumb-up endpoint to log-based aggregation

• Replaced total_stats single-row query with full thumb_logs table scan
• Implemented app-level aggregation using Map to sum thumb_count by song_id
• Changed HTTP method from PATCH to POST with simplified insert-only logic
• Removed upsert logic and now directly inserts new thumb log entries
• Added getAuthenticatedUser() authentication for POST requests

apps/web/src/app/api/songs/thumb-up/route.ts


2. apps/web/src/app/api/search/route.ts Refactor +7/-7

Update search API to use thumb_logs join

• Updated database schema reference from total_stats to thumb_logs
• Changed type definition to handle array of thumb logs instead of single object
• Replaced direct field access with reduce function to sum thumb_count values
• Applied changes to both authenticated and unauthenticated search paths

apps/web/src/app/api/search/route.ts


3. apps/web/src/lib/api/thumbSong.ts Refactor +2/-2

Update client API function to POST method

• Renamed function from patchSongThumb to postSongThumb
• Changed HTTP method from PATCH to POST in API call

apps/web/src/lib/api/thumbSong.ts


View more (3)
4. apps/web/src/queries/songThumbQuery.ts Refactor +3/-3

Update TanStack Query hook for POST method

• Updated import to use postSongThumb instead of patchSongThumb
• Changed mutation function reference in useSongThumbMutation hook
• Updated error alert message from PATCH to POST

apps/web/src/queries/songThumbQuery.ts


5. apps/web/src/types/song.ts Refactor +1/-1

Rename thumb field in type definition

• Renamed ThumbUpSong interface field from total_thumb to thumb_count

apps/web/src/types/song.ts


6. apps/web/src/app/popular/PopularRankingList.tsx Refactor +1/-1

Update component to use new thumb_count field

• Updated property reference from item.total_thumb to item.thumb_count

apps/web/src/app/popular/PopularRankingList.tsx


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 25, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

1 similar comment
@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 25, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 25, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. Unbounded thumb_logs scan 🐞 Bug ➹ Performance
Description
thumb-up GET loads the entire thumb_logs table and aggregates/sorts in application memory, so
latency and memory usage grow linearly with log volume and can time out under load. This regresses
from DB-side ordering/limiting and will get worse as logs accumulate.
Code

apps/web/src/app/api/songs/thumb-up/route.ts[R16-55]

+    // 1) thumb_logs 전체 조회
+    const { data: thumbData, error: thumbError } = await supabase
+      .from('thumb_logs')
+      .select('song_id, thumb_count');
+
+    if (thumbError) throw thumbError;
+    if (!thumbData || thumbData.length === 0) {
+      return NextResponse.json({ success: true, data: [] });
+    }
+
+    // 2) 앱에서 song_id별 합계 집계
+    const thumbMap = new Map<string, number>();
+    for (const row of thumbData) {
+      thumbMap.set(row.song_id, (thumbMap.get(row.song_id) ?? 0) + row.thumb_count);
+    }
+
+    // 3) 상위 50개 song_id 추출
+    const sorted = [...thumbMap.entries()]
+      .sort((a, b) => b[1] - a[1])
+      .slice(0, 50);
+
+    const songIds = sorted.map(([songId]) => songId);
+
+    // 4) 해당 song 상세 정보 조회
+    const { data: songs, error: songError } = await supabase
+      .from('songs')
+      .select('*')
+      .in('id', songIds);
+
+    if (songError) throw songError;
+
+    // 5) 병합 후 thumb_count 내림차순 정렬
+    const songMap = new Map(songs?.map(song => [song.id, song]));
+    const data = sorted
+      .filter(([songId]) => songMap.has(songId))
+      .map(([songId, thumbCount]) => ({
+        ...songMap.get(songId)!,
+        thumb_count: thumbCount,
+      }));
Evidence
The endpoint does an unbounded select from thumb_logs and then performs grouping and sorting in
Node/Edge runtime, which is O(N) per request in both database I/O and application CPU/memory. This
endpoint is used by the popular ranking UI, so it is on a hot path.

apps/web/src/app/api/songs/thumb-up/route.ts[16-55]
apps/web/src/app/popular/PopularRankingList.tsx[12-36]

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

### Issue description
`GET /api/songs/thumb-up` currently reads **all** rows from `thumb_logs` and aggregates/sorts in the app. This will not scale and can cause timeouts.

### Issue Context
We only need the **top 50 songs by SUM(thumb_count)**.

### Fix Focus Areas
- apps/web/src/app/api/songs/thumb-up/route.ts[16-55]

### Suggested approach
- Move aggregation to the database:
 - Create a SQL view (e.g., `song_thumb_totals(song_id, thumb_count_total)`) or an RPC function that returns `song_id` + `sum(thumb_count)` ordered desc limited 50.
 - Query that aggregated result and then fetch/join song details.
- Ensure the final list preserves the aggregated ordering (use a single query that joins songs to the aggregate if possible).

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


2. Search overfetches thumb logs 🐞 Bug ➹ Performance
Description
Search API selects thumb_logs(*) for each song and then reduces client-side to a sum, so response
size and query cost scale with the number of logs per song even though only one aggregated number is
returned. This can severely slow search and inflate bandwidth/costs as thumb_logs grows.
Code

apps/web/src/app/api/search/route.ts[R49-55]

    if (!authenticated) {
      const baseQuery = supabase.from('songs').select(
        `*, 
-        total_stats (
+        thumb_logs (
          *
        )
        `,
Evidence
Both authenticated and unauthenticated search queries embed thumb_logs ( * ), but the handler
ultimately returns only thumb (a sum). That means potentially many rows per song are
transferred/parsed only to be discarded after reduce.

apps/web/src/app/api/search/route.ts[49-57]
apps/web/src/app/api/search/route.ts[77-87]
apps/web/src/app/api/search/route.ts[99-116]
apps/web/src/app/api/search/route.ts[136-148]

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

### Issue description
Search API fetches `thumb_logs(*)` for each song and then sums them in JS. This overfetches and will become slow/expensive as logs increase.

### Issue Context
The API response only needs `thumb` as a number.

### Fix Focus Areas
- apps/web/src/app/api/search/route.ts[49-57]
- apps/web/src/app/api/search/route.ts[99-116]
- apps/web/src/app/api/search/route.ts[77-87]
- apps/web/src/app/api/search/route.ts[136-148]

### Suggested approach
- Replace embedded `thumb_logs(*)` with a DB-side aggregate:
 - Prefer joining to an aggregated view (e.g., `song_thumb_totals`) or using an RPC function that returns songs with `thumb_sum`.
 - If using a view, select `songs.*` plus `song_thumb_totals.thumb_sum` and default to 0 when null.
- Keep pagination/order behavior unchanged.

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


3. Thumb-up points unenforced 🐞 Bug ⛨ Security
Description
thumb-up POST inserts a thumb log for the authenticated user without validating point or enforcing
that the user actually spent/has enough points, so clients can mint arbitrary thumb_count (including
negative/huge values) and manipulate rankings. Because spending points is handled by a separate
endpoint, thumbs and user points can also diverge on partial failures.
Code

apps/web/src/app/api/songs/thumb-up/route.ts[R75-86]

+export async function POST(request: Request): Promise<NextResponse<ApiResponse<void>>> {
  try {
    const supabase = await createClient();
+    const userId = await getAuthenticatedUser(supabase);

    const { point, songId } = await request.json();

-    const { data } = await supabase
-      .from('total_stats')
-      .select('total_thumb')
-      .eq('song_id', songId)
-      .single();
-
-    if (data) {
-      const totalThumb = data.total_thumb + point;
-
-      const { error: updateError } = await supabase
-        .from('total_stats')
-        .update({ total_thumb: totalThumb })
-        .eq('song_id', songId);
+    const { error: insertError } = await supabase
+      .from('thumb_logs')
+      .insert({ song_id: songId, user_id: userId, thumb_count: point });

-      if (updateError) throw updateError;
-    } else {
-      const { error: insertError } = await supabase
-        .from('total_stats')
-        .insert({ song_id: songId, total_thumb: point });
-
-      if (insertError) throw insertError;
-    }
+    if (insertError) throw insertError;
Evidence
The handler trusts { point, songId } from request JSON and writes it directly to thumb_logs.
There is no server-side check for integer/positive bounds, no check that songId exists, and no
coupling to point deduction; point deduction is performed by a different API route that simply
updates the user's point value.

apps/web/src/app/api/songs/thumb-up/route.ts[75-86]
apps/web/src/utils/getAuthenticatedUser.ts[3-13]
apps/web/src/app/api/user/spend-point/route.ts[7-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
`POST /api/songs/thumb-up` writes `thumb_count` directly from client input and does not enforce any point rules. This allows abuse (free/infinite thumbs, negative thumbs) and data inconsistencies.

### Issue Context
Point deduction currently happens via `/api/user/spend-point` which updates `users.point` independently.

### Fix Focus Areas
- apps/web/src/app/api/songs/thumb-up/route.ts[75-86]
- apps/web/src/app/api/user/spend-point/route.ts[7-18]

### Suggested approach
- Add strict validation in thumb-up POST:
 - `songId` required, valid UUID format (if applicable), and exists.
 - `point` must be a positive integer within reasonable bounds.
- Enforce points server-side atomically:
 - Implement a DB transaction via Postgres function/RPC (recommended) that:
   1) checks `users.point >= point`
   2) decrements user points
   3) inserts into `thumb_logs`
 - Call that RPC from the API route and return 400/403 on insufficient points.
- If you keep separate endpoints, at minimum ensure thumb-up checks the user’s current points and fails if insufficient (still non-atomic, but blocks free thumbs).

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


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

PR Description updated to latest commit (3d30b35)

@GulSam00 GulSam00 merged commit 057b2e6 into develop Mar 25, 2026
@GulSam00 GulSam00 deleted the refactor/169-thumbUpLogBased branch March 25, 2026 16:39
Comment on lines +16 to 55
// 1) thumb_logs 전체 조회
const { data: thumbData, error: thumbError } = await supabase
.from('thumb_logs')
.select('song_id, thumb_count');

if (thumbError) throw thumbError;
if (!thumbData || thumbData.length === 0) {
return NextResponse.json({ success: true, data: [] });
}

// 2) 앱에서 song_id별 합계 집계
const thumbMap = new Map<string, number>();
for (const row of thumbData) {
thumbMap.set(row.song_id, (thumbMap.get(row.song_id) ?? 0) + row.thumb_count);
}

// 3) 상위 50개 song_id 추출
const sorted = [...thumbMap.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 50);

const songIds = sorted.map(([songId]) => songId);

// 4) 해당 song 상세 정보 조회
const { data: songs, error: songError } = await supabase
.from('songs')
.select('*')
.in('id', songIds);

if (songError) throw songError;

// 5) 병합 후 thumb_count 내림차순 정렬
const songMap = new Map(songs?.map(song => [song.id, song]));
const data = sorted
.filter(([songId]) => songMap.has(songId))
.map(([songId, thumbCount]) => ({
...songMap.get(songId)!,
thumb_count: thumbCount,
}));

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. Unbounded thumb_logs scan 🐞 Bug ➹ Performance

thumb-up GET loads the entire thumb_logs table and aggregates/sorts in application memory, so
latency and memory usage grow linearly with log volume and can time out under load. This regresses
from DB-side ordering/limiting and will get worse as logs accumulate.
Agent Prompt
### Issue description
`GET /api/songs/thumb-up` currently reads **all** rows from `thumb_logs` and aggregates/sorts in the app. This will not scale and can cause timeouts.

### Issue Context
We only need the **top 50 songs by SUM(thumb_count)**.

### Fix Focus Areas
- apps/web/src/app/api/songs/thumb-up/route.ts[16-55]

### Suggested approach
- Move aggregation to the database:
  - Create a SQL view (e.g., `song_thumb_totals(song_id, thumb_count_total)`) or an RPC function that returns `song_id` + `sum(thumb_count)` ordered desc limited 50.
  - Query that aggregated result and then fetch/join song details.
- Ensure the final list preserves the aggregated ordering (use a single query that joins songs to the aggregate if possible).

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

Comment on lines 49 to 55
if (!authenticated) {
const baseQuery = supabase.from('songs').select(
`*,
total_stats (
thumb_logs (
*
)
`,
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. Search overfetches thumb logs 🐞 Bug ➹ Performance

Search API selects thumb_logs(*) for each song and then reduces client-side to a sum, so response
size and query cost scale with the number of logs per song even though only one aggregated number is
returned. This can severely slow search and inflate bandwidth/costs as thumb_logs grows.
Agent Prompt
### Issue description
Search API fetches `thumb_logs(*)` for each song and then sums them in JS. This overfetches and will become slow/expensive as logs increase.

### Issue Context
The API response only needs `thumb` as a number.

### Fix Focus Areas
- apps/web/src/app/api/search/route.ts[49-57]
- apps/web/src/app/api/search/route.ts[99-116]
- apps/web/src/app/api/search/route.ts[77-87]
- apps/web/src/app/api/search/route.ts[136-148]

### Suggested approach
- Replace embedded `thumb_logs(*)` with a DB-side aggregate:
  - Prefer joining to an aggregated view (e.g., `song_thumb_totals`) or using an RPC function that returns songs with `thumb_sum`.
  - If using a view, select `songs.*` plus `song_thumb_totals.thumb_sum` and default to 0 when null.
- Keep pagination/order behavior unchanged.

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

Comment on lines +75 to +86
export async function POST(request: Request): Promise<NextResponse<ApiResponse<void>>> {
try {
const supabase = await createClient();
const userId = await getAuthenticatedUser(supabase);

const { point, songId } = await request.json();

const { data } = await supabase
.from('total_stats')
.select('total_thumb')
.eq('song_id', songId)
.single();

if (data) {
const totalThumb = data.total_thumb + point;

const { error: updateError } = await supabase
.from('total_stats')
.update({ total_thumb: totalThumb })
.eq('song_id', songId);
const { error: insertError } = await supabase
.from('thumb_logs')
.insert({ song_id: songId, user_id: userId, thumb_count: point });

if (updateError) throw updateError;
} else {
const { error: insertError } = await supabase
.from('total_stats')
.insert({ song_id: songId, total_thumb: point });

if (insertError) throw insertError;
}
if (insertError) throw insertError;
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

3. Thumb-up points unenforced 🐞 Bug ⛨ Security

thumb-up POST inserts a thumb log for the authenticated user without validating point or enforcing
that the user actually spent/has enough points, so clients can mint arbitrary thumb_count (including
negative/huge values) and manipulate rankings. Because spending points is handled by a separate
endpoint, thumbs and user points can also diverge on partial failures.
Agent Prompt
### Issue description
`POST /api/songs/thumb-up` writes `thumb_count` directly from client input and does not enforce any point rules. This allows abuse (free/infinite thumbs, negative thumbs) and data inconsistencies.

### Issue Context
Point deduction currently happens via `/api/user/spend-point` which updates `users.point` independently.

### Fix Focus Areas
- apps/web/src/app/api/songs/thumb-up/route.ts[75-86]
- apps/web/src/app/api/user/spend-point/route.ts[7-18]

### Suggested approach
- Add strict validation in thumb-up POST:
  - `songId` required, valid UUID format (if applicable), and exists.
  - `point` must be a positive integer within reasonable bounds.
- Enforce points server-side atomically:
  - Implement a DB transaction via Postgres function/RPC (recommended) that:
    1) checks `users.point >= point`
    2) decrements user points
    3) inserts into `thumb_logs`
  - Call that RPC from the API route and return 400/403 on insufficient points.
- If you keep separate endpoints, at minimum ensure thumb-up checks the user’s current points and fails if insufficient (still non-atomic, but blocks free thumbs).

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

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 25, 2026

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

Project Deployment Actions Updated (UTC)
singcode Ready Ready Preview, Comment Mar 25, 2026 4:59pm

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.

refactor : thumb-up API를 thumb_logs 개별 로그 기반으로 변경

1 participant