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
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
- query: 첫 페이지도 `size=100`; 다음 페이지는 숫자 `cursorId`와 `size=100`
- every request forwards React Query `AbortSignal`
- response: `ApiResponse<{ items, hasNext, nextCursor }>` unwrap
- entry: numeric `id`, flat track metadata, timing fields, and `source: USER_REQUESTED | AUTOMATIC_REPLAY`
- entry: numeric `id`, flat track metadata, timing fields, and `playbackOrigin: USER_REQUESTED | AUTOMATIC_REPLAY`
- backend page/item order is newest-first; selector exposes oldest-first
- 내 노래 탭은 `playbackOrigin=USER_REQUESTED`이면서 응답의 nullable `addedByUserSlug`와 현재 사용자의 공개 slug가 정확히 일치하는 항목만 소비하며, nickname이나 숫자 ID로 대체하지 않는다.
- `hasNext=false`, missing cursor, or an already requested cursor stops pagination
- TanStack infinite query stores at most 5 pages and reset returns to the newest first page

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
- branch: dev
- base: main
- issue:
- pr: https://github.com/Queuing-org/frontend/pull/57
- pr: https://github.com/Queuing-org/frontend/pull/58
- selected_skills: queuing-feature-delivery, queuing-orchestrator, queuing-api-boundary, queuing-ui-flow, frontend-architecture-guardrails, queuing-qa-reviewer
- local_qa: test 152/621 passed; lint passed; build passed; fresh QA pass
- local_qa: test 152/628 passed; lint passed; build passed; diff-check passed; fresh QA pass
- ci: pending
- review_threads: not-started
- next_action: monitor PR #57 checks and review feedback
- next_action: PR #58 checks와 review feedback 확인
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- 과거곡은 `queue-history`, 대기곡과 내 신청곡은 기존 cursor query를 스크롤 경계에서 한 페이지씩 조회한다.
- history 5페이지/500곡, 각 목록 DOM 40행 상한과 대기곡 한정 DnD를 유지한다.
- 곡 전환·재연결·종료 시 history 최신 창과 관련 캐시를 일관되게 갱신한다.
- 내 노래 탭에도 로그인 사용자가 신청한 history와 현재곡을 같은 시간축으로 노출한다.
- 지난 곡 카드는 현재 재생 카드와 같은 회색 배경으로 구분한다.

## Selected skills

Expand All @@ -21,7 +23,9 @@
- history API가 slug, access token, `cursorId`, `size=100`, AbortSignal 계약을 지킨다.
- 누락·반복 cursor를 중단하고 history 응답을 중복 제거한 시간순 최대 500곡으로 만든다.
- 전체 탭의 현재곡 앵커, 상단 prepend 보정, 하단 자동 조회, 방향별 실패/재시도가 동작한다.
- 내 신청곡 탭은 곡 전환 때 탭과 스크롤을 유지하며 DnD 중에는 자동 조회하지 않는다.
- 내 신청곡 탭은 공개 slug가 일치하는 history/current만 노출하고 DnD 중에는 자동 조회하지 않는다.
- 전체/내 노래 양쪽에서 위로 과거곡, 아래로 대기곡 페이지를 조회한다.
- 지난 곡은 현재 재생 카드와 같은 `#f7f7f9` 배경을 사용한다.
- track 이벤트·직접 skip·재연결은 history를 첫 페이지로 reset하고 terminal cleanup은 history 캐시도 제거한다.
- `npm run test`, `npm run lint`, `npm run build`와 fresh QA review가 통과한다.

Expand All @@ -30,6 +34,8 @@
1. `feat(playlist): 재생 기록 조회와 캐시 생명주기 추가`
2. `feat(queue): 재생목록 양방향 무한스크롤 적용`
3. `docs(delivery): 양방향 무한스크롤 게시 상태 기록`
4. `feat(queue): 내 노래 재생 기록 노출`
5. `docs(delivery): 내 노래 기록 게시 상태 갱신`

## Progress

Expand All @@ -39,12 +45,14 @@
- [x] UI·스크롤·가상화 구현
- [x] targeted/full QA와 fresh review
- [x] commit, push, Draft PR
- [x] 내 노래 history 후속 구현과 회귀 QA

## Decisions

- React Query가 history 서버 상태와 5페이지 sliding window를 소유한다.
- queue panel hook이 탭, 조회 상태, mutation busy 상태를 조합하고 UI가 실제 scroll container와 anchor 보정을 소유한다.
- history/current는 정적 비-sortable 구간, pending queue만 기존 sortable 구간으로 둔다.
- 개인 history는 새 API를 추정하지 않고 기존 history 응답의 `playbackOrigin=USER_REQUESTED`, `addedByUserSlug`와 로그인 사용자의 공개 slug를 정확히 비교한다.

## Verification

Expand All @@ -54,6 +62,10 @@
- fresh read-only QA: `pass`; virtual geometry, stale anchor, retry, refetch busy, tail clamp findings closed
- manual browser QA: unavailable because the in-app browser had no connected runtime; no desktop/mobile/public/private claim is made
- delivery: `dev` pushed and Draft PR #57 opened against `main`
- follow-up delivery: `dev` pushed and Draft PR #58 opened against `main`
- follow-up targeted: room/playlist 26 files, 101 tests passed; final focused 2 files, 22 tests passed
- follow-up full QA: 152 files, 628 tests passed; lint passed; build passed; `git diff --check` passed
- follow-up fresh read-only QA: `pass`; 개인 필터, 짧은 목록 wheel, anchor/latch, DnD, 회색 상태 검토 완료

## Residual risk

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# QA Report

## Result

- classification: `pass`
- blocking findings: 없음

## Boundary review

- backend history 응답의 `addedByUserSlug`, `playbackOrigin`, `startOffsetMs`와 frontend type이 일치한다.
- 내 노래 탭은 `USER_REQUESTED`이면서 현재 사용자의 공개 slug와 일치하는 history만 노출한다.
- 자동재생·타인·비로그인 cached history는 개인 목록에서 제외한다.
- 개인 시간축은 history → 본인 현재곡 → 본인 pending 순서이며 DnD와 삭제는 기존 pending 구간에만 유지한다.
- 전체/내 노래 모두 상단 history와 하단 queue 페이지네이션을 사용한다.
- 개인 history가 비어 스크롤 높이가 생기지 않아도 위쪽 wheel로 다음 과거 페이지를 요청한다.
- history와 active current는 같은 `#f7f7f9` 배경을 사용한다.
- 목록별 가상화 DOM 40행 상한을 유지한다.

## Verification

- `npm run test -- --run src/features/room/queue src/features/playlist`: 26 files, 101 tests passed
- focused final regression: 2 files, 22 tests passed
- `npm run test -- --reporter=dot`: 152 files, 628 tests passed
- `npm run lint`: passed
- `npm run build`: passed
- `git diff --check`: passed
- fresh read-only QA: `pass`

## Residual risk

- 연결 가능한 인증 runtime이 없어 실제 공개/비공개 방과 모바일 터치 입력은 브라우저 실측하지 못했다.
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@

## Mine tab

- no history/current rows.
- current track changes do not switch tabs or scroll.
- 기존 history query를 공유하되 현재 사용자의 공개 slug와 일치하는 history만 렌더링한다.
- 현재곡도 본인 신청곡일 때만 history와 pending 사이에 렌더링한다.
- 진입과 본인 현재곡 변경 시 history/current 경계를 최상단에 맞춘다.
- 지난 곡과 현재곡은 같은 회색 배경을 쓰고 정렬·삭제 대상에서 제외한다.
- 상단에서 개인 history, 하단에서 개인 pending queue를 한 페이지씩 조회한다.
- within 96px of bottom, fetch one personal queue page unless loading, refetching, dragging, or mutating.

## Failure and accessibility
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# API Contract

## Room playback

- endpoint: `GET /api/v1/rooms/{slug}/playback`
- authoritative field: `currentEntry.status.playbackOrigin`
- values:
- `USER_REQUESTED`: 기존 현재곡 카드 노출
- `AUTOMATIC_REPLAY`: 현재곡 카드 제외 및 자동 순환 상태 노출
- queue/history 항목에는 status origin이 없을 수 있으므로 `PlaylistEntryStatus.playbackOrigin`은 optional로 모델링한다.

## Realtime cache

- `TRACK_STARTED` 이벤트 자체에는 playback origin이 없다.
- 같은 entry를 갱신할 때 기존 playback cache의 `status.playbackOrigin`을 보존한다.
- 새 entry의 출처는 예약된 playback query invalidation 뒤 REST 응답이 확정한다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Delivery State

- status: ci-pending
- branch: dev
- base: main
- issue:
- pr: https://github.com/Queuing-org/frontend/pull/58
- selected_skills: queuing-feature-delivery, queuing-orchestrator, queuing-api-boundary, queuing-ui-flow, frontend-architecture-guardrails, queuing-qa-reviewer, browser:control-in-app-browser
- local_qa: pass — viewport-height follow-up targeted 2 files/17 tests, lint, build, diff-check, fresh QA 4 files/38 tests pass
- ci: previous head pass; viewport-height follow-up pending
- review_threads: 사용자 화면 확인에서 재현한 history branch 중앙 정렬 finding resolved; GitHub CLI 인증 만료로 원격 thread 조회 불가
- next_action: viewport-height follow-up commit을 push하고 PR #58의 새 CI를 확인한다.
91 changes: 91 additions & 0 deletions docs/exec-plans/active/2026-09-01-automatic-replay-state/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 자동 순환 재생 상태와 칭호 굵기 정리

## Scope

- `/api/v1/rooms/{slug}/playback`의 `currentEntry.status.playbackOrigin`을 현재 재생 출처의 기준으로 사용한다.
- `AUTOMATIC_REPLAY` 현재곡은 전체 트랙의 일반 곡 카드에서 제외하고 애니메이션 상태로 안내한다.
- 지난 곡과 대기곡은 자동 순환 중에도 유지한다.
- 설정 칭호 선택기의 획득 칭호 전용 bold 규칙을 제거해 주변 입력 타이포와 통일한다.

## Acceptance Criteria

- `AUTOMATIC_REPLAY` 현재곡 제목과 현재 재생 카드는 전체 트랙에 렌더링하지 않는다.
- 전체 트랙에는 막대 3개가 움직이는 `현재 자동 재생 중입니다` 상태를 표시한다.
- 모션 감소 환경에서는 막대 애니메이션을 중지한다.
- 내 노래 탭에는 자동 순환 상태와 현재곡을 표시하지 않는다.
- 수동 재생 현재곡, 지난 곡, 대기곡, 큐 조작 동작은 유지한다.
- 같은 현재곡의 실시간 `TRACK_STARTED` 캐시 갱신에서 REST로 받은 playback origin을 보존한다.
- 칭호 옵션은 획득 여부와 무관하게 기존 trigger와 같은 medium 굵기를 사용한다.

## Selected Skills

- queuing-feature-delivery
- queuing-orchestrator
- queuing-api-boundary
- queuing-ui-flow
- frontend-architecture-guardrails
- queuing-qa-reviewer
- browser:control-in-app-browser

## Commit Slices

1. `fix(settings): 칭호 선택기 글자 굵기를 맞춤`
2. `feat(queue): 자동 순환 재생 상태를 표시`
3. `docs(delivery): 자동 재생 상태 게시 결과를 기록`

## Progress

- [x] 현재 branch/worktree와 Draft PR #58 확인
- [x] playback API, realtime cache, queue panel state ownership 확인
- [x] 칭호 굵기와 자동 순환 UI 구현
- [x] targeted/full local verification
- [x] fresh read-only QA
- [x] commit, push, Draft PR #58 갱신

## Verification

- targeted Vitest: playback API, realtime reducer, queue panel hook/view/list
- `npm run lint`
- `npm run test`
- `npm run build`
- `git diff --check`
- fresh read-only QA

결과:

- targeted Vitest: 5 files / 35 tests pass
- lint: pass
- full test: 152 files / 649 tests pass
- build: pass
- git diff --check: pass
- fresh QA: 7 files / 58 tests 및 diff-check pass, blocker 없음
- browser visual QA: 브라우저 연결 없음으로 미실행

## Residual Risk

- 실제 backend 자동 순환 응답과 브라우저 높이별 중앙 정렬은 로컬 fixture 기준이다.
- 현재 세션에 연결된 브라우저가 없어 실제 화면 캡처 QA는 수행하지 못했다.

## Follow-up: visual size and alignment

- [x] 사용자 runtime screenshot으로 과도한 원 크기와 상단 치우침 재현
- [x] 원 59x59px, 내부 padding 12px, 막대 width 3px 적용
- [x] 자동재생 단독 상태가 list area의 실제 높이를 채우도록 `height: 100%` 적용
- [x] targeted 2 files / 17 tests, lint, build, diff-check 통과
- [x] fresh QA의 DnD live-region test selector finding 1건 수정 후 재검증 pass

## Superseded follow-up: parent-owned centering

- [x] 사용자 runtime screenshot으로 `height: 100%` 수정이 실제 중앙 정렬에 실패함을 재현
- [x] 자동재생 단독 상태의 parent flex 방식을 시도하고 로컬 검증
- [x] history가 존재하는 runtime에서 조건이 false가 되어 동작하지 않음을 후속 screenshot으로 확인
- [x] 조건부 parent flex 구현과 전용 테스트 제거

## Follow-up: list viewport height centering

- [x] history가 화면 위로 스크롤된 상태에서도 timeline branch가 존재함을 원인으로 확정
- [x] list area를 size query container로 지정
- [x] history/pending 유무와 관계없이 자동재생 상태를 `100cqh`로 렌더링
- [x] history branch도 `fillAvailableSpace=true`인지 회귀 테스트로 고정
- [x] targeted 2 files / 17 tests, lint, build, diff-check 통과
- [x] fresh QA 4 files / 38 tests pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# QA Report

## Automated verification

- targeted Vitest: 5 files / 35 tests pass
- playback API 응답의 nested playback origin
- 같은 현재곡의 `TRACK_STARTED` origin 보존
- queue panel의 전체/내 탭 자동 순환 분기
- 자동 순환 상태·현재곡 카드 제외·manual timeline 보존
- panel view flag 전달
- `npm run lint`: pass
- `npm run test`: 152 files / 649 tests pass
- `npm run build`: pass
- `git diff --check`: pass

## Fresh read-only QA

- result: pass
- independent targeted verification: 7 files / 58 tests pass
- findings: blocker 없음
- reviewed boundaries: playback origin, realtime cache, 전체/내 탭, history/pending/manual current, 상태 접근성·reduced motion, 칭호 선택 표현

## Visual QA

- in-app Browser와 외부 browser 연결을 조회했으나 사용 가능한 browser가 없어 실제 화면 캡처 확인은 수행하지 못했다.
- CSS의 기본/compact 크기와 `prefers-reduced-motion` 규칙, DOM의 `role=status`와 장식 아이콘 숨김은 코드와 테스트로 확인했다.

## Residual risk

- 실제 방에서 backend가 `AUTOMATIC_REPLAY`를 반환하는 통합 흐름과 패널 높이별 중앙 정렬은 게시 후 브라우저에서 확인이 필요하다.

## Visual follow-up

- 사용자 runtime screenshot에서 원형 아이콘 과대 크기와 빈 상태 상단 치우침을 확인했다.
- 원 59x59px, padding 12px, 막대 width 3px을 기본/compact 구간에서 동일하게 유지한다.
- standalone 상태에만 `height: 100%`를 적용하고 timeline 상태에는 기존 min-height를 유지한다.
- targeted Vitest: 2 files / 17 tests pass
- lint: pass
- build: pass
- diff-check: pass
- fresh QA: DnD live-region selector finding 1건 수정 후 2 files / 17 tests 및 diff-check pass
- 수정 후 실제 브라우저 screenshot은 현재 세션에서 확인하지 못했다.

## Superseded parent-owned centering follow-up

- 사용자 runtime screenshot에서 자식 `height: 100%`가 콘텐츠 높이만 차지해 중앙 정렬에 실패함을 확인했다.
- 자동재생 단독 상태일 때 list area를 column flex container로 만들고 standalone 상태를 `flex: 1`로 배치했다.
- 로컬 검증은 통과했지만 실제 runtime에는 화면 위 history가 존재해 단독 상태 조건이 false였고 중앙 정렬이 적용되지 않았다.
- 해당 조건부 flex 구현은 다음 follow-up에서 제거했다.

## List viewport height follow-up

- list area를 size query container로 지정했다.
- no-history와 timeline 분기 모두 자동재생 상태에 `fillAvailableSpace=true`를 전달한다.
- 자동재생 상태는 `100%` fallback 뒤 `100cqh`로 list viewport 한 화면 높이를 차지한다.
- history는 위, pending은 아래에 유지되고 current boundary를 상단 정렬하면 아이콘·문구가 현재 list viewport 중앙에 온다.
- targeted Vitest: 2 files / 17 tests pass
- lint: pass
- build: pass
- diff-check: pass
- fresh QA: 4 files / 38 tests pass
- residual: jsdom은 container query unit의 실제 layout을 계산하지 않는다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Review Findings

## Resolved actionable

1. 자동 재생 원형 아이콘이 화면 기준보다 크다.
- 현재: 기본 80x80, compact 64x64, 막대 기본 4px/compact 3.2px
- 요구: 원 59x59, 내부 padding 12px, 막대 width 3px
2. 자동 재생 전용 빈 상태가 목록 영역 세로 중앙에 오지 않는다.
- 원인: `min-height: 100%`만 사용해 flex로 계산된 scroll area의 실제 높이를 채우지 못한다.
- 수정: standalone 상태가 scroll area의 definite height를 채우도록 `height: 100%`를 사용한다.

## QA finding

- 최초 회귀 테스트가 dnd-kit live region과 자동재생 상태의 중복 `role=status`를 전역 조회해 실패했다.
- 자동재생 문구에서 가장 가까운 status 컨테이너로 조회 범위를 좁혔다.
- 독립 targeted 2 files / 17 tests와 diff-check 재검증 결과 pass.

## Superseded attempt

3. `height: 100%` 후속 수정 뒤에도 자동 재생 상태가 세로 중앙에 오지 않는다.
- 사용자 runtime screenshot으로 동일 현상을 재현했다.
- 원인: flex로 계산된 scroll area 안에서 자식의 percentage height가 콘텐츠 높이를 벗어나지 못했다.
- 시도: 자동 재생 단독 상태일 때만 list area를 column flex container로 만들고 상태를 `flex: 1`로 채웠다.
- 폐기 이유: runtime에서는 history가 존재해 단독 상태 조건이 false였고 중앙 정렬이 적용되지 않았다.

## Operational

- `gh auth status`에서 활성 계정 token 만료를 확인했다.
- 로컬 구현·검증·커밋은 가능하며, push와 PR 갱신 가능 여부는 Git credential을 별도로 확인한다.

## Resolved after runtime screenshot

4. parent flex 후속 수정 뒤에도 자동 재생 상태가 세로 중앙에 오지 않는다.
- 실제 원인: history가 있으면 `RoomQueueListSection`이 자동 재생 상태를 `fillAvailableSpace=false`로 렌더링한다.
- history는 현재 경계 위로 스크롤되어 screenshot에 보이지 않지만 해당 분기 때문에 상태는 intrinsic height만 차지한다.
- 수정: list area를 size query container로 만들고 자동 재생 상태를 모든 timeline 분기에서 `100cqh`로 채운다.
- 검증: 독립 targeted 4 files / 38 tests, lint, build, diff-check, fresh QA pass.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# UI Flow

## 전체 트랙

- `useRoomQueuePanel`이 playback origin을 해석해 자동 순환 현재곡을 일반 current entry에서 제외한다.
- `RoomQueuePanelView`는 자동 순환 상태 flag를 list section에 전달한다.
- list section은 현재곡 카드 대신 3개 막대 아이콘과 `현재 자동 재생 중입니다` 상태를 표시한다.
- 자동 재생 아이콘은 59x59px 원, 내부 padding 12px, 3px 막대 세 개를 사용한다.
- 지난 곡 또는 대기곡이 있으면 그대로 유지하며 자동 순환 상태는 history/current/pending 경계에 표시한다.
- list area는 size query container이며 자동 순환 상태는 history/pending 유무와 관계없이 `100cqh`로 현재 목록 viewport 한 화면을 차지한다.
- current boundary가 viewport 상단에 정렬되면 자동 순환 아이콘·문구는 해당 화면의 세로·가로 중앙에 오고, history는 위·pending은 아래에 유지된다.

## 내 노래

- 자동 순환은 사용자 신청곡이 아니므로 현재곡과 전용 상태를 모두 숨긴다.
- 기존 사용자 요청 history/current/pending 필터는 유지한다.

## Accessibility

- 상태 컨테이너는 `role=status`로 읽힌다.
- 장식 막대는 접근성 트리에서 숨긴다.
- `prefers-reduced-motion: reduce`에서는 애니메이션을 중지한다.
Loading