Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/queuing-qa-reviewer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Do not use it as a vague second implementation pass. QA must compare concrete bo
- Room reconnect tests prove join handshake -> single topic subscription -> room read invalidation order, and route cleanup proves explicit leave or cancelled-join cleanup.
- Queue pagination tests prove first-page-only entry, cursor/revision pairing, `totalPendingCount`, conflict reset, and inclusion of every pending personal entry in reorder payloads regardless of `ownerOrdered`.
- Drag-and-drop regressions require a repeated real or faithful pointer cycle that checks the original row count, computed visibility, and cleared inline transform/animation after every drop; a mocked overlay prop or callback-only test is insufficient evidence.
- Account exit QA must leave an identity GET in flight, complete logout/withdrawal, then resolve the old response. Assert that the GET was cancelled, `me` stays null, and account-specific suggestion caches are removed. Setting identity data to null alone does not cancel a pending query.
- Account-specific failures require same-account and same-backend-state comparisons before assigning a frontend root cause.
- Shared controls still behave consistently across home and search.
- Hover-only controls remain reachable by focus where practical.
Expand Down
2 changes: 2 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Vercel Edge Config -> Next.js request gate (`src/proxy.ts`)
- Brand-only presentation such as the main logo may live in `src/shared/ui`; authenticated actions and room navigation remain in their owning auth and room features.
- Domain-neutral management-menu focus, outside-click, Escape, placement, visual shell behavior, and opt-in viewport portal positioning live in `src/shared/ui/management-menu`; room and follow features provide only their allowed actions.
- Room thumbnail file validation, temporary upload selection, and the shared temporary-upload mutation live in `src/features/room/hooks`; create and update flows own only their submit orchestration.
- Domain-neutral overflow marquee presentation lives in `src/shared/ui/overflow-marquee`; queue cards and playlist suggestions share hover, keyboard activation, and reduced-motion behavior.
- Domain-neutral floating-panel chrome and drag handles live in `src/shared/ui/floating-panel`; room and follow features own placement state and panel-specific content.
- ESLint rejects imports from `src/shared` into `src/features` or `src/app`, and rejects imports from the room feature back into the home feature.

Expand All @@ -54,6 +55,7 @@ Vercel Edge Config -> Next.js request gate (`src/proxy.ts`)
- STOMP subscriptions deliver real-time events; handlers must reconcile those events with query cache and screen state deliberately.
- App-wide follow presence and room membership use separate STOMP clients because their authentication, ownership, and reconnect lifecycles are independent. A terminal room event such as `user.session-replaced` must not stop the follow presence transport.
- Room membership owns `roomAccessToken` in memory plus room-scoped `sessionStorage`. The token is issued by `ROOM_JOINED`, authenticates room topic subscriptions and room-internal REST requests, survives transport reconnects, and is never part of a URL, log, or TanStack Query key.
- Room-session nickname projections live in the room domain. Timestamped room events reconcile REST caches and project chat display names; the session projection survives transport reconnects and is cleared on explicit leave. Stored chat history remains unchanged.
- Local component state owns transient UI state such as modal visibility, hover state, inputs, and local panel behavior.
- `FollowModal` owns the selected follow user and originating card trigger while its nested profile dialog is open, so close and block flows can restore focus without list-owned expansion state.
- `localStorage` is reserved for persistence that must survive navigation or reload, such as scoped room interaction state.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 로그아웃 뒤 지연된 본인 조회가 계정을 복원하는 경합

## Problem
로그아웃·탈퇴 성공 직전 진행 중이던 me 조회가 늦게 완료되면 UI가 이전 계정을 다시 로그인 상태로 판단할 수 있었다.

## Previous Behavior
성공 처리에서 me를 null로 저장하고 일부 계정별 캐시를 제거했지만 진행 중 조회는 유지했다.

## Previous Code
```ts
qc.setQueryData(userKeys.me(), null);
```

## Updated Code
```ts
await qc.cancelQueries({ queryKey: userKeys.me(), exact: true });
qc.setQueryData(userKeys.me(), null);
qc.removeQueries({ queryKey: trackSuggestionKeys.all() });
```
본인 API도 QueryFunctionContext.signal을 Axios까지 전달한다.

## Problem in the Previous Code
setQueryData는 진행 중 queryFn을 취소하지 않는다. queryFn의 늦은 성공이 null보다 뒤에 캐시에 기록된다.

## Evidence
- 독립 QA에서 실제 QueryClient의 me null → 과거 fetch resolve → 이전 계정 복원을 재현.
- `useAccountExit.test.tsx`에서 실제 logout/withdraw mutation hook과 지연 Promise를 결합하여 signal.aborted, 최종 me null, 검색 캐시 제거를 검증.
- 실서비스 계정으로 재현하지 않았음. 원인은 클라이언트 캐시 경합으로 한정.

## Cause or Remaining Hypotheses
진행 중 조회를 취소하지 않은 캐시 초기화 순서가 확인된 원인. 서버 로그아웃 동작에 대한 문제는 주장하지 않는다.

## Solution Options
- me만 null: 이후 성공 응답에 덮임.
- 모든 캐시 삭제: 영향 범위가 크고 불필요한 공용 캐시까지 버림.
- 본인 조회 취소 후 null 및 관련 개인 캐시 삭제: 선택.

## Chosen Solution and Rationale
기존 계정 종료 mutation이 순서를 소유하도록 제한했다. signal 전파와 TanStack 취소를 함께 적용해 전송 및 캐시 반영을 중단한다.

## Result
지연 응답이 계정을 복원하지 않으며 자주 신청한 곡·검색 결과가 다음 계정으로 노출되지 않는다.

## Reusable Rule
계정 종료 시 진행 중 identity 조회를 먼저 취소하고 identity/개인 캐시를 정리한다. 캐시를 비우는 것만으로 취소가 되지 않는다.

## Skill or Team Spec Updates
- queuing-qa-reviewer: 로그아웃/탈퇴 지연 identity 응답 검사 추가.

## Verification
- 전체 `npm test`: 711개 통과(추가 후 최종 숫자는 실행 계획 QA 참조).
- `npm run lint`, `npm run build` 통과.
- 브라우저 연결 없음으로 실계정 수동 검증 미실행.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# API 계약

2026-09-08 GitHub API로 Queuing-org/backend main의 Controller/Response 원문 확인.

- `src/main/java/queuing/core/room/queue/presentation/controller/UserFrequentlyQueuedTrackController.java`
- `src/main/java/queuing/core/room/queue/presentation/controller/YoutubeVideoSearchController.java`
- 같은 queue 패키지 `presentation/response/FrequentlyQueuedTrackResponse.java`, `YoutubeVideoSearchResponse.java`
- `room/participant/application/event/payload/RoomParticipantNicknameChangedEventData.java`

## 검색
- GET `/api/v1/user-profiles/me/frequent-tracks`: 로그인 쿠키, 파라미터 없음. 현재 보관 재생 기록 기반 신청 횟수/최근 신청 순 최대 8개. provider/videoId/title/thumbnailUrl(nullable)/durationMs/requestCount.
- GET `/api/v1/youtube/videos?query=…&size=8`: 로그인 쿠키. trim 후 1~100자. videoId/title/channelTitle/thumbnailUrl/durationMs. 새 요청은 cursor 생략.
- 두 응답 모두 기존 `{ result: { items, hasNext: false, nextCursor: null } }` 계약. UI에서 8개로 상한을 한 번 더 적용.
- quota: HTTP 503, code `room.youtube-api-quota-exceeded`. Query 자동 retry/focus/reconnect 재조회 비활성. HTTP 429는 공유 Axios의 기존 Retry-After/최대 2회 정책을 유지.
- `AbortSignal`을 두 GET으로 전달. 로그인 계정 slug를 query key에 포함. 로그아웃/탈퇴는 진행 중 me 조회 취소 후 me null 및 검색 캐시 삭제.
- frequent staleTime 30초, YouTube staleTime 5분. 신청 결과/재생 기록 리셋 시 frequent 무효화.

## 실시간 이름
- 기존 room topic의 `ROOM_PARTICIPANT_NICKNAME_CHANGED`, roomSlug/timestamp/data.userSlug/data.nickname.
- 유한한 양수 timestamp, 정확한 현재 방, 비어 있지 않은 slug/nickname 검증. 동일 user timestamp <= 최신 이벤트는 무시.
- 참가자 전체 페이지, 개인/전체 큐, 현재 곡, 방장, 해당 프로필, me 이름만 패치. 재검증 결과에도 방 세션 최신 이름을 적용.
- 서버 조회에는 닉네임 버전이 없으므로 세션이 수신한 이벤트를 우선. 재연결 중 놓친 새 이벤트와 오래된 REST 응답을 완전히 구분할 수 없는 서버 계약상 한계가 있음.
- 채팅은 저장된 원문을 변경하지 않고 표시 배열에서만 senderSlug로 이름을 보정. 사용자별 추가 조회 없음.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 변경 요약
- d1205c6: 개인 큐 전체 페이지에서 현재 entryId만 제외. 같은 영상 별도 신청과 카운트 보존.
- 357562c: 친구 상태 문구·색상·폰트 복원, 프로필 크기와 단일 패딩 소유권, overflow 스크롤.
- 782fc42: 노래 검색·자주 신청한 음악·취소/IME/계정 캐시·공통 marquee와 회귀 테스트.
- 3cf2641: 방 실시간 이름 투영·전체 캐시 페이지 보정·기존/추가 채팅·재연결/퇴장 테스트.
- ARCHITECTURE: 공통 marquee 위치 및 방 범위 nickname projection 소유권 명시.
- incident: 로그아웃 지연 identity 응답 경합과 QA 규칙.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Delivery State
- status: ci-pending
- branch: dev
- base: main
- issue: 없음
- pr: https://github.com/Queuing-org/frontend/pull/62
- selected_skills: plan.md 참조
- local_qa: pass — lint, 713 tests, build, 독립 QA
- ci: pending
- review_threads: 없음
- next_action: PR #62 CI/리뷰 확인, 브라우저 연결 후 시각·터치 검증
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Handoff
- 사용자 계획의 네 범위 구현·기능별 커밋 및 독립 QA pass 완료.
- 최종 lint/713 tests/build 통과. 실제 브라우저 연결이 없어 시각·터치 검증은 미실행.
- dev push 및 Draft PR #62 생성 완료: https://github.com/Queuing-org/frontend/pull/62. CI 대기 상태에서 후속 리뷰/시각 검증 진행.
- API와 UI 계약, 회귀 근거, 중간 실패는 같은 run의 api-contract.md/ui-flow.md/qa-report.md 참조.
29 changes: 29 additions & 0 deletions docs/exec-plans/active/2026-09-08-track-search-realtime/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 노래 신청 검색·방 실시간 표시·친구 UI 통합 개선

## 범위와 기준
- 빈 입력: 자주 신청한 음악 8개. 제목: 300ms debounce YouTube 검색 8개. URL 선택 후 사연 유지.
- 계정별 캐시, 취소/IME/오류/키보드/portal 및 reduced motion 지원.
- 개인 큐 현재 entryId 중복 제거, 친구 상태 문구 복원, 프로필 300×431/240×344.8.
- 방 닉네임 이벤트 검증 및 최신 이름을 참가자/큐/프로필/세션/기존·추가 채팅에 반영.

## 선택 스킬
queuing-feature-delivery, queuing-orchestrator, queuing-api-boundary, queuing-ui-flow, frontend-architecture-guardrails, queuing-qa-reviewer, queuing-incident-curator, browser:control-in-app-browser.

## 커밋 순서
1. fix(room): 개인 큐 현재 곡 중복 제거 + 회귀 테스트
2. fix(follow): 친구 상태·프로필 치수와 패딩 소유권 + 테스트
3. feat(playlist): 음악 검색 콤보박스·공통 marquee·계정 캐시 + 테스트
4. fix(room): 닉네임 이벤트와 표시 동기화 + 테스트
5. docs(delivery): 통합 QA 및 전달 증거

## 검증
npm run lint, npm test, npm run build, 브라우저 시각 검증, 독립 read-only QA.

## 진행
- dev clean 시작. 백엔드 main Controller/DTO 계약 확인 완료.
- 네 기능 구현 및 기능별 코드·테스트 커밋 완료.
- 독립 QA의 ArrowUp/로그아웃 경합 지적 수정 후 재검토 pass.
- 최종 전체 713개 테스트(작업자 4개), lint, 단독 build 통과. 검증 중간 실패와 재실행은 qa-report.md 참조.
- 브라우저 runtime은 연결 없음(browsers.list=[]). 실계정/시각 검증 미실행을 명시하고 draft 전달.
- 계정 종료 지연 응답 incident 및 QA 재사용 규칙 기록.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# QA Report

## 판정
pass — 독립 read-only QA 완료. 코드/자동 검증에 차단 사항 없음. 실제 브라우저 검증은 아래 제한으로 남김.

## 최종 검증
- `npm run lint`: 통과.
- `npm test -- --maxWorkers=4`: 160개 파일, 713개 테스트 통과(108.24초).
- `npm run build`: 단독 실행 통과. Next.js 16.1.1 Turbopack compile/TypeScript/static generation 완료.
- `git diff --check`: 통과.

## 직접 검증한 회귀
- 검색 API cursor 생략/size8/signal/응답 해제/8개 상한.
- 빈 입력 자동 포커스, 키보드 선택과 URL 변경·선정 이유 보존, 300ms debounce, IME 요청/Enter 차단.
- 검색 교체 시 abort, 역순 응답 미노출, 다른 계정 cache 분리, 미로그인 목록 비활성.
- SoundCloud 선택 차단, 첫 ArrowUp 마지막 지원 행, Escape 두 단계, Tab와 바깥 클릭, quota 자동 반복 없음.
- 로그아웃·탈퇴 mutation 성공 뒤 이전 me 응답이 도착해도 me null 및 검색 cache 제거 유지.
- 개인 큐 모든 페이지에서 현재 entryId만 제외, 같은 영상의 별도 신청 유지, 서버 totalPendingCount 유지, 곡 전환 재계산.
- 친구 room/online/offline 상태 및 알 수 없는 상태, 프로필 기본/축소/높이 제한 자동 테스트.
- 닉네임 participant/queue 전체 페이지, 현재 곡·방장·profile·me, 진행 중 GET 취소 및 후속 cache 응답 보정.
- 다른 방·잘못된 payload·중복·역순 이름 이벤트 무시. 기존 구독 유지, reconnect 시 보정 유지, leave 시 정리.
- 기존/추가 chat 투영에서 원문·내용·삭제 상태·식별자 보존. 기존 채팅 스크롤 테스트를 포함한 전체 suite 통과.

## 독립 리뷰와 수정
- reviewer `qa_review` 초기 fix: 최초 ArrowUp 인덱스 오류 및 logout/withdraw의 me 지연 응답 경합.
- 두 문제 수정 및 회귀 테스트 추가. reviewer 재검토 최종 pass.
- API의 quota는 HTTP 503임을 backend Controller 원문으로 확인하여 공통 429 정책과의 비충돌 검증.

## 중간 실패와 재검증
- 초기 테스트 실행: 기존 chat hook 목업의 messages 누락, 추가 frequent invalidation에 대한 옛 기대값, 신규 API test envelope 오류. 실제 계약에 맞게 테스트 수정 후 통과.
- 초기 전체 실행은 711개 통과. 마지막 두 회귀 테스트 추가 후 빌드/테스트 동시 실행에서 기존 RoomFormModal 테스트 타임아웃 발생.
- 해당 검증 프로세스만 중단하고 작업자 4개로 전체 테스트 후 단독 빌드를 수행하여 713개/빌드 모두 통과. 동시 실행 자원 경합과 일치하는 증거이며 애플리케이션 원인으로 주장하지 않음.
- 샌드박스 안 첫 build는 출력 없이 정지. 이번 작업 프로세스만 종료 후 escalated build에서 정상 완료.
- 초기 보조 `npx tsc --noEmit`은 테스트 목업 타입 오류와 초기 잘못된 wrapper import 등으로 실패. wrapper import는 수정됨. 전달 기준인 Next production TypeScript/build는 최종 통과했으며 전체 테스트 파일에 대한 standalone tsc 통과는 주장하지 않음.

## 미실행·잔여 제한
- Browser skill bootstrap 후 getForUrl은 No browser is available, browsers.list는 [] 반환. 연결 요청을 남겼지만 브라우저가 없어 실제 시각/터치/실계정 API·소켓 테스트 미실행.
- 지정 치수/색상/CSS·DOM 구조는 코드와 자동 테스트로 확인했으나 실제 픽셀·작은 화면 목록 scroll·프로필 하단·채팅 scroll 보존은 브라우저에서 후속 확인 필요.
- 닉네임 버전 없는 REST 계약상 reconnect 중 누락된 더 최신 이벤트와 오래된 REST 결과를 완전히 구분할 수 없음. 세션 수신 최신 이벤트 우선 원칙.
12 changes: 12 additions & 0 deletions docs/exec-plans/active/2026-09-08-track-search-realtime/ui-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# UI 흐름

- TrackSearchInput가 열림/IME/활성 행/위치만 소유한다. 신청 폼의 URL과 선정 이유는 기존 useAddTrackForm이 소유한다.
- 자동 포커스/빈 값은 frequent, trim 제목은 300ms 이후 YouTube. URL 및 URL처럼 보이는 잘못된 값은 검색하지 않는다.
- 목록 선택은 URL 변경과 닫기만 수행. 실제 전송은 기존 큐잉 버튼.
- body portal fixed 위치에 입력 너비와 하단 좌표를 사용하고 viewport 잔여 높이를 max-height로 적용. 방향키/Enter/Tab/바깥 클릭/Escape 지원. Escape는 열린 목록부터 소비한다.
- 행 48px, 아이콘 20px/간격12px/좌우16px. 검색 썸네일32px/radius4px. 제목16px regular, 채널14px #555555. 축소 모드80%.
- 공통 marquee는 hover와 활성 행에서만 순환. reduced-motion에서는 animation none, 수동 가로 스크롤.
- 입력/재생목록 영역에서 선정 이유까지 기존 40px CSS 간격 유지.
- 친구 상태는 확인된 online/room으로만 표시. 오프라인이면 stale room 숨김.
- 친구 프로필300×431/240×344.8, shell contentPadding=none으로 패딩 소유권 일원화. 패널 내용은 항상 세로 스크롤 가능하여 큰 통계도 접근 가능.
- 닉네임은 방 캐시와 profile fallback context, 고정 본인 참가자 표시 및 chat 표시 투영에 반영. 원본 chat/식별자/scrollToLatestKey 유지.
2 changes: 2 additions & 0 deletions docs/exec-plans/active/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Active Execution Plans

- [2026-09-08-track-search-realtime](./2026-09-08-track-search-realtime/plan.md): ci-pending — Draft PR #62, 노래 검색·친구 UI·큐 중복·실시간 닉네임 통합 개선, 713 tests/lint/build/독립 QA 통과

- [2026-09-05-room-queue-modal-polish](./2026-09-05-room-queue-modal-polish/plan.md): ci-pending — Draft PR #61, 내 노래 현재곡 중복·신청 폼 간격·방 modal dim/높이·Friends 상세 padding 교정 및 로컬 QA 통과

- [2026-09-02-youtube-playlist-import](./2026-09-02-youtube-playlist-import/plan.md): ci-pending — Draft PR #60, 재생목록 감지 시 현재 영상/재생목록 노래 추가 선택을 요구하고 후속 로컬 QA 통과
Expand Down
5 changes: 4 additions & 1 deletion src/features/auth/logout/model/useLogout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { logoutApi } from "../api/logout";
import { trackSuggestionKeys } from "@/src/features/playlist/model/trackSuggestionKeys";
import { userKeys } from "@/src/features/user/model/queryKeys";
import { badgeKeys } from "@/src/features/badge/model/queryKeys";

Expand All @@ -11,8 +12,10 @@ export function useLogout() {

return useMutation<void, Error, void>({
mutationFn: () => logoutApi(),
onSuccess: () => {
onSuccess: async () => {
await qc.cancelQueries({ queryKey: userKeys.me(), exact: true });
qc.setQueryData(userKeys.me(), null);
qc.removeQueries({ queryKey: trackSuggestionKeys.all() });
qc.removeQueries({ queryKey: badgeKeys.me() });
},
});
Expand Down
Loading