Skip to content

refactor: 마지막 아이템 스크롤 전용 로직을 공용 쿼리 액션 훅으로 통합 - #381

Merged
kanghaeun merged 4 commits into
devfrom
refactor/372-replace-scroll-to-last-with-query-action
Jul 25, 2026
Merged

refactor: 마지막 아이템 스크롤 전용 로직을 공용 쿼리 액션 훅으로 통합#381
kanghaeun merged 4 commits into
devfrom
refactor/372-replace-scroll-to-last-with-query-action

Conversation

@kanghaeun

@kanghaeun kanghaeun commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

작업 요약

  • 토너먼트 생성 화면의 전용 스크롤 쿼리(?scrollToLast=true) + 전용 훅(useScrollToLast)을 프로젝트에 이미 있는 쿼리 기반 1회성 액션 공용 훅 useQueryAction(?action=<value>)으로 통합

작업 상세 내용

마지막 아이템 스크롤 기능만을 위한 전용 쿼리 파라미터와 훅이 별도로 존재했으나, 프로젝트에 이미 동일한 목적(쿼리 기반 1회성 액션 처리)의 공용 훅 useQueryAction이 있어 중복 구조였습니다. 이를 해결하기 위해 전용 로직을 공용 훅으로 통합합니다.

공용 액션으로 통합

  • consts/queryAction.tsSCROLL_TO_LAST 액션 값 추가
  • 프로듀서(위시에서 담기 성공, 알림 SSE 딥링크)를 ?action=scroll-to-last로 변경
  • TournamentCreateClient에서 useScrollToLastuseQueryAction으로 교체
  • 캐러셀은 scrollToLast만 첫 렌더 seed로 사용하므로 onScrolled 정리 로직 제거
// before
?scrollToLast=true  →  useScrollToLast()

// after
?action=scroll-to-last  →  useQueryAction(SCROLL_TO_LAST)

정리

  • 미사용 useScrollToLast.ts 삭제
  • 참조되지 않던 useBasketCarousel.ts 함께 삭제

연관 이슈

Summary by CodeRabbit

  • 개선 사항
    • 토너먼트 생성 화면에서 새 항목 추가 후 마지막 바구니로 자동 스크롤되는 동작을 개선했습니다.
    • 관련 화면 이동 및 알림 딥링크의 스크롤 동작을 일관된 방식으로 처리합니다.
    • 불필요한 스크롤 및 캐러셀 제어 로직을 정리해 동작 안정성을 높였습니다.

kanghaeun and others added 2 commits July 25, 2026 00:21
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
piki Ready Ready Preview, Comment Jul 25, 2026 12:01pm

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

전용 scrollToLast 쿼리와 useScrollToLast 훅을 제거하고, QUERY_ACTIONscroll-to-last 액션으로 마지막 바구니 스크롤 흐름을 통합했다. 관련 네비게이션과 SSE 딥링크도 공용 액션 쿼리를 사용한다.

Changes

토너먼트 마지막 항목 스크롤

Layer / File(s) Summary
공용 액션 계약과 트리거 생성
apps/web/src/consts/queryAction.ts, apps/web/src/app/tournament/[id]/create/by-wish/..., apps/web/src/hooks/useNotificationSSE.ts
SCROLL_TO_LAST 액션을 추가하고, 위시 기반 생성 및 알림 딥링크의 쿼리를 action=scroll-to-last로 변경했다.
생성 화면의 액션 소비
apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx, apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx, apps/web/src/app/tournament/[id]/create/_hooks/*
생성 화면이 useQueryAction의 활성 상태를 캐러셀에 전달하며, 콜백 기반 스크롤 완료 처리를 제거했다. 전용 스크롤 및 캐러셀 훅도 삭제했다.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: WEB

Suggested reviewers: ychany, soyeong0115, iodio89

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 마지막 아이템 스크롤 로직을 공용 쿼리 액션으로 통합하는 핵심 변경을 정확히 요약합니다.
Linked Issues check ✅ Passed 직접 이슈 #372의 요구사항인 SCROLL_TO_LAST 추가, 쿼리 전환, useQueryAction 교체, 훅 삭제가 모두 반영되었습니다.
Out of Scope Changes check ✅ Passed 요약된 변경은 모두 토너먼트 생성 스크롤 액션 통합 범위에 맞고, 별도 무관한 변경은 보이지 않습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 refactor/372-replace-scroll-to-last-with-query-action

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.

@github-actions
github-actions Bot requested review from soyeong0115 and ychany July 24, 2026 15:31
@github-actions github-actions Bot added the refactor Extra attention is needed label Jul 24, 2026
@github-actions

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

@iOdiO89

iOdiO89 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@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

🤖 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
`@apps/web/src/app/tournament/`[id]/create/_components/TournamentCreateClient.tsx:
- Around line 33-35: Update the TournamentCreateClient flow using useQueryAction
so the scroll-to-last action remains observable by the Carousel after the query
parameter is removed. Either explicitly reset the action with setIsActive(false)
at the appropriate time or preserve scrollToLast as query-driven state until the
new basket is added, ensuring query-only navigation scrolls to the last basket.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4fff47d1-784a-462d-828b-f7321b6c6fcb

📥 Commits

Reviewing files that changed from the base of the PR and between 0a504e9 and 26c5114.

📒 Files selected for processing (7)
  • apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx
  • apps/web/src/app/tournament/[id]/create/_hooks/useBasketCarousel.ts
  • apps/web/src/app/tournament/[id]/create/_hooks/useScrollToLast.ts
  • apps/web/src/app/tournament/[id]/create/by-wish/_hooks/usePostTournamentItemsByWish.ts
  • apps/web/src/consts/queryAction.ts
  • apps/web/src/hooks/useNotificationSSE.ts
💤 Files with no reviewable changes (2)
  • apps/web/src/app/tournament/[id]/create/_hooks/useScrollToLast.ts
  • apps/web/src/app/tournament/[id]/create/_hooks/useBasketCarousel.ts

Comment on lines +33 to +35
const { isActive: scrollToLast } = useQueryAction({
action: QUERY_ACTION.VALUE.SCROLL_TO_LAST,
});

@coderabbitai coderabbitai Bot Jul 25, 2026

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

echo "== locate files =="
fd -a 'TournamentCreateClient\.tsx|useQueryAction\.ts|TournamentItemBasketCarousel\.(tsx|ts)$' . | sed 's#^\./##'

echo "== outline candidate files =="
for f in $(fd 'TournamentCreateClient\.tsx|useQueryAction\.ts|TournamentItemBasketCarousel\.(tsx|ts)$' .); do
  echo "--- $f ---"
  wc -l "$f"
  ast-grep outline "$f" || true
done

echo "== relevant snippets =="
for f in $(fd 'TournamentCreateClient\.tsx|useQueryAction\.ts|TournamentItemBasketCarousel\.(tsx|ts)$' .); do
  echo "--- $f lines 1-240 ---"
  sed -n '1,240p' "$f" | nl -ba
done

echo "== search query action usages =="
rg -n "useQueryAction|QUERY_ACTION\.VALUE\.SCROLL_TO_LAST|scrollToLast|scroll-to-last|prevItemCountRef|useQuery" apps/web/src -S

Repository: TeamPiKi/client

Length of output: 1697


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  "apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx"
  "apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx"
  "apps/web/src/hooks/useQueryAction.ts"
)

for f in "${files[@]}"; do
  echo "--- $f line count ---"
  wc -l "$f"
  echo "--- $f content ---"
  awk '{printf "%6d\t%s\n", NR, $0}' "$f"
done

echo "== targeted searches =="
rg -n "useQueryAction|QUERY_ACTION\.VALUE\.SCROLL_TO_LAST|scrollToLast|scroll-to-last|prevItemCountRef|useQuery" apps/web/src -S || true

echo "== package/runtime availability =="
node --version 2>/dev/null || true
python3 --version 2>/dev/null || true

Repository: TeamPiKi/client

Length of output: 29130


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('apps/web/src/hooks/useQueryAction.ts')
src = p.read_text()
print('useQueryAction uses useState initializer:', 'useState(() => searchParams.get(paramKey) === action)' in src)
print('useQueryAction effect replaces URL immediately:', '.router.replace(clearPath ?? pathname, { scroll: false })' in src)
print('useQueryAction effect does not call setIsActive:', 'setIsActive(' in src and 'onAction?.();\n    router.replace' in src)

t = Path('apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx').read_text()
print('carousel reads scrollToLast only into prevItemCountRef initializer:', t.count('scrollToLast') == 1 and 'prevItemCountRef.current = scrollToLast' in t)
print('carousel scroll effect compares items.length with prev count:', 'items.length > prevItemCountRef.current' in t)

client = Path('apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx').read_text()
print('client forwards isActive to carousel:', 'scrollToLast={scrollToLast}' in client)

p = Path('apps/web/src/hooks/useNotificationSSE.ts').read_text()
print('notification hook contains SCROLL_TO_LAST_QUERY constant:', 'SCROLL_TO_LAST_QUERY' in p)
lines = p.splitlines()
for i,l in enumerate(lines, start=1):
    if 'SCROLL_TO_LAST_QUERY' in l or 'TOURNAMENT_CREATE(tournamentId)' in l:
        start=max(1,i-6); end=min(len(lines),i+10)
        print(f'-- useNotificationSSE.ts lines {start}-{end} --')
        for j in range(start,end+1):
            print(f'{j:5d}\t{lines[j-1]}')
PY

python3 - <<'PY'
from pathlib import Path

# Model Next.js route reuse behavior relevant to the concern:
# - app/[id]/create/page.tsx is a route, not an app-level layout; same route with same app dir
#   causes the client component's state initialized from searchParams to persist; URL changes update searchParams hooks.
# - useQueryAction's first-effect removes the query while isActive stays initialized true.
# - Carousel receives true once; ref stores 0 as first prevItemCountRef.current, then scroll effect consumes items.length > 0 once.
# - A later same-route query-only addition sets searchParams to include action, but the hook's state is already true and the
#   removal effect immediately clears it before the user has a chance to observe scrolling.
print('concern_model:')
print('1. first URL without action: isActive false, prevItemCountRef initial = items.length, no scroll when item appended.')
print('2. user appends item: isActive false, scroll condition: appended 1 > previous N? false; no scroll.')
print('3. notification URL with action while component mounted: inactive state remains true already; query-removing effect triggers immediately; active indicator disappears before carousel can observe second append.')
PY

Repository: TeamPiKi/client

Length of output: 3026


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,90p' apps/web/src/app/tournament/\[id\]/create/by-wish/_hooks/usePostTournamentItemsByWish.ts
sed -n '1,95p' apps/web/src/hooks/usePostWishLink.ts

python3 - <<'PY'
from pathlib import Path
for p in [
    Path('apps/web/src/app/tournament/[id]/create/by-wish/_hooks/usePostTournamentItemsByWish.ts'),
    Path('apps/web/src/hooks/usePostWishLink.ts'),
]:
    print(f'--- {p} ---')
    text = p.read_text()
    for needle in ['scroll-to-last', 'SCROLL_TO_LAST', 'TOURNAMENT_CREATE', 'router.push', 'queryClient']:
        print(f'{needle}:', text.find(needle))
PY

Repository: TeamPiKi/client

Length of output: 3156


동일한 생성 라우트에서 딥링크 스크롤을 반영하도록 해 주세요.

useQueryAction?action=scroll-to-last 쿼리를 감지한 뒤 즉시 URL에서 제거하더라도 기존 생성 페이지에는 prevItemCountRef가 이미 최신 아이템 수로 초기화되어 있어 새 바구니가 추가되더라도 마지막 바구니로 스크롤되지 않습니다. 생성 화면도 같은 라우트에서 query-only navigation을 지원하므로 setIsActive(false)를 호출하거나, action 감지 시 scrollToLast를 query 상태로 관리해 Carousel에서 실제 스크롤을 처리하도록 바꿔주세요.

🤖 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
`@apps/web/src/app/tournament/`[id]/create/_components/TournamentCreateClient.tsx
around lines 33 - 35, Update the TournamentCreateClient flow using
useQueryAction so the scroll-to-last action remains observable by the Carousel
after the query parameter is removed. Either explicitly reset the action with
setIsActive(false) at the appropriate time or preserve scrollToLast as
query-driven state until the new basket is added, ensuring query-only navigation
scrolls to the last basket.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

확인해봤는데 useQueryAction이 useState lazy initializer로 마운트 시점에 쿼리를 스냅샷한 뒤 제거하기 때문에, 쿼리가 URL에서 사라져도 scrollToLast는 true로 유지되어 Carousel에서 계속 관찰 가능해요. Carousel도 이 값을 prevItemCountRef 초기값(마운트 1회)에서만 소비하고 이후엔 아이템 카운트 증가로만 스크롤하므로 리셋이 없어도 부작용이 없고, 리팩토링 전 useScrollToLast와 동작이 동일합니다

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

hook=$(fd -a '^useQueryAction\.ts$' apps/web/src/hooks | head -n 1)
carousel=$(fd -a '^TournamentItemBasketCarousel\.tsx$' apps/web/src/app/tournament | head -n 1)

printf '%s\n' "== useQueryAction outline =="
ast-grep outline "$hook" --view expanded
printf '%s\n' "== useQueryAction =="
nl -ba "$hook"

printf '%s\n' "== carousel outline =="
ast-grep outline "$carousel" --view expanded
printf '%s\n' "== carousel relevant state/effects =="
rg -n -C 8 'scrollToLast|prevItemCountRef|useEffect|scrollTo' "$carousel"

printf '%s\n' "== all useQueryAction call sites and implementations =="
rg -n -C 3 '\buseQueryAction\s*\(' apps/web/src --glob '*.{ts,tsx}'

Length of output: 1246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

hook=$(fd -a '^useQueryAction\.ts$' apps/web/src/hooks | head -n 1)
carousel=$(fd -a '^TournamentItemBasketCarousel\.tsx$' apps/web/src/app/tournament | head -n 1)

echo '== useQueryAction source =='
awk '{printf "%6d\t%s\n", NR, $0}' "$hook"

echo '== carousel source: relevant lifecycle =='
awk '
  /scrollToLast|prevItemCountRef|useEffect|scrollTo/ { start=NR-10; if (start<1) start=1; end=NR+16 }
  NR>=start && NR<=end { lines[NR]=$0 }
  END { for (i=1; i<=NR; i++) if (i in lines) printf "%6d\t%s\n", i, lines[i] }
' "$carousel"

echo '== query-action callers (context) =='
rg -n -C 4 '\buseQueryAction\s*\(' apps/web/src --glob '*.{ts,tsx}' || true

Length of output: 11131


@kanghaeun, 확인 결과 최초 마운트 경로에 대한 설명은 맞습니다. 다만 동일하게 마운트된 생성 화면에서의 query-only navigation 문제는 남아 있습니다.

  • useQueryAction.ts Line 38의 lazy initializer는 첫 렌더에서만 실행됩니다.
  • 이후 searchParamsaction=scroll-to-last가 추가되면 Line 40-45의 effect는 URL만 제거하며 setIsActive(true)를 호출하지 않습니다.
  • 따라서 기존 isActivefalse인 상태에서는 scrollToLast가 계속 false입니다.
  • 캐러셀도 Line 45에서 scrollToLast를 ref 초기값으로 한 번만 사용하고, Line 50-63의 effect는 scrollToLast 변경을 관찰하지 않습니다.

즉, 새로 진입하여 마운트되는 흐름에서는 리팩터링 전과 동일하지만, 코멘트에서 언급한 것처럼 같은 생성 라우트가 유지된 채 action 쿼리만 추가될 수 있다면 마지막 바구니 스크롤은 트리거되지 않습니다. 해당 navigation이 실제로 발생하지 않는다는 보장이 있다면 알려주세요.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

맞아요 SSE 알림 토스트의 바로가기 로 이미 머물러 있는 create 화면에 진입하면 말씀하신 query-only navigation이 발생할 수 있습니다 (보장 없음). 다만 그 경로는 리팩터링 전에도 스크롤이 트리거되지 않던 경로예요 query-only 진입에서도 스크롤이 되게 만드는 건 기존에 없던 동작의 신규 개선이라 별도 이슈로 다루겠습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@kanghaeun, understood. Since query-only navigation did not trigger this behavior before the refactor, I agree it is out of scope for this PR and should not block it.

Would you like me to create a follow-up GitHub issue for supporting action=scroll-to-last on same-route query-only navigation from SSE notification deep links?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

제가 만들겠습니다

@kanghaeun
kanghaeun merged commit 62300b6 into dev Jul 25, 2026
7 checks passed
@kanghaeun
kanghaeun deleted the refactor/372-replace-scroll-to-last-with-query-action branch July 25, 2026 12:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: 토너먼트 생성 scrollToLast 쿼리를 useQueryAction으로 대체

2 participants