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
@@ -0,0 +1,6 @@
# API Contract

- `PATCH /api/v1/user-profiles/me`의 기존 `statusMessage` 필드와 payload 형태를 그대로 사용한다.
- 최애곡 입력은 줄바꿈을 공백으로 정규화한 뒤 최대 40자까지만 전송한다.
- 방 수정과 노래 신청 endpoint, payload, cache invalidation에는 변경이 없다.
- `https://api.queuing.cc`의 일반적인 OpenAPI/Swagger 공개 경로는 2026-09-01 확인 시 모두 404였다. 따라서 새 서버 최대 길이를 추정해 타입을 바꾸지 않고, 기존 255자 프런트 계약보다 보수적인 40자 제한만 적용한다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Change Summary

- 최애곡 상한을 40자로 늘리고 접근성 안내와 카운터를 같은 상수로 통일했다.
- 방·친구 공개 프로필의 두 줄 최애곡 경로에서 말줄임을 제거하고 최소 두 줄 공간을 확보했다.
- 방 편집 모달을 방 본문 셸과 같은 688px/550.4px 너비로 맞췄다.
- 방 편집 select·참여 제한·footer 액션이 density별 하나의 높이 변수를 공유하도록 정리했다.
- 노래 신청의 사연 문구와 검증 오류를 `노래 선정 이유`로 바꾸고 하단 액션에 pill hover/focus 배경을 추가했다.
- YouTube URL input 글자 크기를 normal/mobile/compact 각각의 label 크기와 맞췄다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Delivery State

- status: ready
- branch: dev
- base: main
- issue:
- pr: https://github.com/Queuing-org/frontend/pull/59
- 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: targeted 6 files/44 tests, lint, 152 files/650 tests, build, diff-check, fresh review passed
- ci: GitHub Actions lint/test/build, Vercel, Vercel Preview Comments, CodeRabbit passed
- review_threads: none at publication
- next_action: human review and optional authenticated browser pixel QA
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Handoff

- Branch: `dev`
- Draft PR: #59 — https://github.com/Queuing-org/frontend/pull/59
- Implementation, targeted tests, full lint/test/build, diff-check, fresh read-only QA are complete.
- Browser visual QA remains because no browser instance was available.
- Feature slices and verification documents are pushed to `dev`.
- Existing PR #58 merged before the latest push, so its metadata was restored to the merged scope and the new commits were published in Draft PR #59.
- GitHub Actions lint/test/build, Vercel, Vercel Preview Comments, and CodeRabbit checks passed.
- Next action: human review and optional authenticated browser pixel QA.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 프로필·방 편집·노래 신청 UI 후속 정리

## Scope

- 설정 최애곡 입력 상한을 20자에서 40자로 늘리고 공개 프로필에서 말줄임 없이 여러 줄로 표시한다.
- 방 편집 모달 너비를 방 내부 메인 컨테이너 너비와 맞춘다.
- 방 편집 하단 삭제·완료 버튼 높이를 같은 화면의 드롭다운 높이와 맞춘다.
- 노래 신청의 `사연 (선택)` 문구를 `노래 선정 이유 (선택)`로 변경하고 하단 액션에 pill hover/focus 배경을 적용한다.
- YouTube URL 입력 글자 크기를 같은 필드의 라벨 크기와 맞춘다.
- `dev`에 커밋·푸시한다. 게시 중 PR #58이 먼저 병합되어 이번 변경은 새 Draft PR #59로 전달한다.

## Selected Skills

- `queuing-feature-delivery`
- `queuing-orchestrator`
- `queuing-api-boundary`
- `queuing-ui-flow`
- `frontend-architecture-guardrails`
- `queuing-qa-reviewer`

## Ownership Decisions

- 최애곡 입력 상한과 저장 전 정규화는 기존 settings form model/hook이 계속 소유한다.
- 프로필 줄바꿈은 공유 `UserProfileContent`가 소유해 방·친구 프로필에 동일하게 적용한다.
- 방 편집 치수는 `EditRoomFormModal.module.css`에서 방 메인 셸의 normal/compact 치수와 동일하게 관리한다.
- 노래 신청 문구와 입력/액션 표현은 add-track UI와 CSS module에서만 변경한다.

## Commit Slices

1. `fix(ui): 프로필과 방 편집 치수를 정리`
2. `fix(playlist): 노래 신청 문구와 입력 스타일 정리`
3. `docs(delivery): UI 후속 검증 기록`

## Acceptance Criteria

- 최애곡은 최대 40자까지 입력·저장 payload에 유지되고 카운터와 접근성 설명도 40자를 안내한다.
- 두 줄 프로필에서 최애곡은 말줄임표 없이 자연스럽게 줄바꿈되고 최소 두 줄 높이를 확보한다.
- 방 편집 모달은 normal 688px, compact 550.4px이며 좁은 화면에서는 기존 반응형 제한을 유지한다.
- 방 편집 삭제·완료 버튼은 normal 56px, compact 44.8px, mobile 52px로 같은 화면의 select 높이와 일치한다.
- 노래 신청 이유 문구, pill hover/focus, URL 입력·라벨 글자 크기가 회귀 테스트와 CSS 검토를 통과한다.
- `npm run lint`, `npm run test`, `npm run build`, `git diff --check`, fresh read-only QA가 통과한다.

## Progress

- [x] 요청과 관련 경계 확인
- [x] 구현 및 대상 테스트 보강
- [x] 전체 로컬 검증
- [x] Fresh read-only QA
- [x] `dev` 커밋·푸시 및 Draft PR #59 게시

## Residual Risk

- 배포 API의 공개 OpenAPI 경로가 404라 `statusMessage`의 현재 서버 최대 길이는 문서로 재검증하지 못했다. 프런트엔드의 과거 계약은 255자였고 이번 전송 형태는 변경하지 않는다.
- 실제 글자 폭은 언어와 글꼴에 따라 달라진다. 말줄임 대신 자연 줄바꿈을 사용해 제목 손실을 피한다.
- in-app Browser에 연결 가능한 브라우저 인스턴스가 없어 실제 화면 스크린샷 QA는 수행하지 못했다.

## Publication Note

- 기존 PR #58은 이번 커밋 push 전에 병합되었다. 병합된 PR의 제목·본문은 원래 범위로 복구하고, `origin/main..dev`의 새 커밋 3개를 Draft PR #59로 게시했다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# QA Report

## Automated Verification

- targeted: 6 files / 44 tests passed
- `npm run lint`: passed
- `npm run test`: passed — 152 files / 650 tests
- `npm run build`: passed
- `git diff --check`: passed

## Boundary Review

- Fresh read-only reviewer: `pass`; blocking finding 없음.
- `statusMessage` payload key와 update mutation/cache 흐름은 변경하지 않고 최대 길이 정규화만 40자로 확장했다.
- 방 편집 modal normal/compact 너비는 방 메인 container 계산값과 일치한다.
- select·참여 제한·footer 버튼은 `--edit-room-control-height`를 공유해 normal/mobile/compact cascade가 일치한다.
- add-track API `story` key와 submit state는 유지하고 사용자 문구와 CSS interaction만 변경했다.
- YouTube URL input과 label은 normal 16px, mobile 20px, compact 12.8px로 일치한다.

## Residual Risk

- 배포 API의 공개 OpenAPI/Swagger 경로가 404라 `statusMessage`의 실제 서버 상한은 통합 확인이 남아 있다. 과거 프런트 계약은 255자였고 이번 상한은 40자다.
- in-app Browser에 사용 가능한 브라우저가 없어 computed layout과 실제 hover를 스크린샷으로 확인하지 못했다. CSS 소스·빌드·fresh review까지만 검증했다.

## Publication

- `dev` push passed.
- 기존 PR #58이 push 전에 병합되어 새 변경은 Draft PR #59로 게시했다.
- Draft PR #59 checks passed: GitHub Actions lint/test/build, Vercel, Vercel Preview Comments, CodeRabbit.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Request Summary

- 프로필 최애곡이 20자 제한과 한 줄 높이 때문에 충분히 보이지 않는 문제를 개선한다.
- 방 편집창의 너비를 방 내부 컨테이너와 맞추고 하단 액션 높이를 드롭다운과 맞춘다.
- 노래 신청 사연 문구를 노래 선정 이유로 바꾸고 hover 배경을 pill 형태로 만든다.
- YouTube 링크 입력 글자 크기를 필드 라벨과 같게 만든다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# UI Flow

- 설정 최애곡 input과 카운터는 40자를 기준으로 동작하고 기존 즉시 draft/완료 저장 흐름을 유지한다.
- 방·친구 프로필의 최애곡은 공유 컴포넌트에서 말줄임 없이 줄바꿈하며 두 줄 분량의 기본 공간을 확보한다.
- 방 편집 모달은 방 메인 셸의 normal/compact 너비를 따르고 mobile에서는 기존 viewport 여백을 유지한다.
- 방 편집 footer 버튼은 각 density의 select 높이와 동일하다.
- 노래 신청 모달은 필드 라벨을 `노래 선정 이유 (선택)`로 표시하고 취소·큐잉 액션에 hover/focus-visible pill 배경을 제공한다.
- YouTube URL input은 normal/mobile/compact에서 각각 같은 화면의 label 글자 크기를 사용한다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Participant Identity Contract

- `ROOM_JOINED.data.participant` is the authoritative identity for the current room session.
- Initial join and every reconnect join update the room screen's current participant state.
- Cursor participant pages remain the server-state source for the list. The current joined participant is prepended only when neither its `participantId` nor non-null `userSlug` is present.
- When a fetched page already contains the current user, the fetched participant object wins so refreshed nickname/profile fields are not replaced by stale join data.
- Room access tokens remain excluded from URLs, logs, and query keys; this change adds no endpoint or payload mutation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Change Summary

- 내 참가자 행을 목록 첫 번째로 정렬하고 `(나)` 표시를 추가했다.
- 내 더보기 메뉴에서 기존 Setting/Friends modal을 열도록 연결했다.
- 최초 입장과 reconnect의 `ROOM_JOINED` participant identity를 보존해 cursor page 밖의 자기 행도 보장했다.
- participant identity, virtual list/menu, mobile/desktop modal, room join/reconnect 회귀 테스트를 추가했다.
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/59
- selected_skills: queuing-feature-delivery, queuing-api-boundary, queuing-ui-flow, frontend-architecture-guardrails, queuing-qa-reviewer
- local_qa: lint, 660 tests, and production build passed; browser session unavailable
- ci: pending
- review_threads: not-checked
- next_action: inspect PR #59 CI and review feedback when requested
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Handoff

- status: ci-pending
- completed: implementation, targeted tests, lint, full tests, build, final fresh QA pass, commit, push, Draft PR #59 update
- blocker: none
- next_action: inspect Draft PR #59 CI and review feedback when requested
- prohibited: do not force-push, merge, or mark the PR ready without explicit user request
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Room Self Participant Menu

## Scope

- 현재 로그인 사용자를 참가자 목록 첫 행으로 안정 정렬한다.
- 내 닉네임 오른쪽, 방장 왕관 왼쪽에 `(나)`를 표시한다.
- 내 참가자 행의 더보기 메뉴에서 기존 Settings/Friends modal을 연다.

## Acceptance Criteria

- 정렬은 query data를 변경하지 않고 나머지 참가자 순서를 보존한다.
- `(나)`는 `#3c3c3c`이며 방장 행은 `닉네임 → (나) → 왕관` 순서다.
- 내 행 메뉴는 `Setting`, `Friends`만 표시하고 hover, focus, Escape, outside-click 동작은 기존 참가자 메뉴와 같다.
- 데스크톱 draggable panel과 모바일 참가자 탭 모두 같은 modal owner를 사용한다.
- 다른 참가자의 관리 액션과 참가자 DOM 상한은 회귀하지 않는다.

## Decisions

- 사용자가 선택한 대로 내 행은 첫 번째 정렬만 적용하고 scroll sticky는 적용하지 않는다.
- 현재 사용자는 `currentUser.slug`와 participant `userSlug`로 판별한다.
- modal visibility는 `RoomPlaybackJoinedContent`의 local state가 소유한다.
- 실제 `SettingsModal`, `FollowModal`은 draggable transform 밖에 렌더링한다.
- 비로그인 guest에는 자기 표시와 로그인 전용 메뉴를 추가하지 않는다.

## Selected Skills

- `queuing-feature-delivery`
- `queuing-api-boundary`
- `queuing-ui-flow`
- `frontend-architecture-guardrails`
- `queuing-qa-reviewer`

## Commit Plan

1. `feat(room): 참가자 목록에 내 메뉴와 모달 진입 추가`

## Progress

- [x] request와 현재 component/modal ownership 확인
- [x] implementation 및 component tests
- [x] lint, test, build
- [x] fresh QA review — final pass
- [x] local commit
- [x] push, Draft PR #59 갱신

## Verification

- `npm test -- src/features/room/page/hooks/useRoomRealtimeEvents.test.tsx src/features/room/page/ui/RoomPlaybackScreen.test.tsx src/features/room/page/ui/RoomPlaybackJoinedContent.test.tsx src/features/room/participants/model/participantIdentity.test.ts src/features/room/participants/ui/RoomParticipantList.test.tsx src/features/room/participants/ui/RoomParticipantsPanel.test.tsx` — 6 files, 50 tests passed
- `npm run lint` — passed
- `npm run test` — 153 files, 660 tests passed
- `npm run build` — passed
- local browser smoke — browser session unavailable; component integration tests cover mobile/desktop modal ownership

## Residual Risk

- 실제 로그인 방의 시각 smoke는 사용할 수 있는 browser session이 없어 자동 수행하지 못했다.
- modal close 후 원래 참가자 trigger로 focus를 복원하는 별도 계약은 기존 home modal에도 없어 이번 변경에서는 추가하지 않았다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# QA Report

## Result

- classification: pass
- reviewer: fresh read-only agent after two targeted fix rounds
- blocking findings: none

## Findings Resolved

1. 최초 참가자 cursor page에 현재 사용자가 없으면 자기 행이 사라지는 문제
- `ROOM_JOINED.data.participant`를 화면 state에 보존하고 page 결과에 identity 중복 없이 포함했다.
- 101명 방에서 현재 참가자가 첫 page 밖인 화면 회귀 테스트를 추가했다.
2. reconnect `ROOM_JOINED`의 최신 참가자 객체를 버리는 문제
- `useRoomRealtimeEvents`가 최신 participant를 화면 callback으로 전달한다.
- 변경된 participantId와 nickname을 저장하는 reconnect 회귀 테스트를 추가했다.

## Boundary Review

- 현재 사용자는 첫 행 정렬만 적용하며 sticky row를 만들지 않는다.
- `(나)` DOM 순서와 `#3c3c3c` 색상이 요청과 일치한다.
- self menu는 `Setting`, `Friends`만 제공하고 기존 viewport portal/focus shell을 사용한다.
- 기존 Settings/Friends modal을 draggable transform 밖의 room screen owner가 렌더링한다.
- 다른 참가자 관리 액션과 virtual DOM 24개 상한을 유지한다.

## Verification

- targeted: 6 files, 50 tests passed
- lint: passed
- full test: 153 files, 660 tests passed
- production build: passed
- browser smoke: unavailable because no browser session was connected

## Residual Risk

- 실제 로그인 방과 작은 모바일 실기기의 시각 smoke가 남아 있다.
- modal close 후 참가자 trigger focus 복원은 기존 home modal에도 없는 접근성 부채다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# UI Flow

1. 참가자 목록은 로그인한 현재 사용자 행을 첫 번째로 파생 정렬한다.
2. 내 행 hover 또는 keyboard focus 시 기존과 같은 더보기 trigger가 나타난다.
3. trigger를 열면 `Setting`, `Friends` menuitem만 표시된다.
4. menuitem 선택은 menu를 닫고 방 화면의 단일 modal state를 갱신한다.
5. 방 화면은 draggable widget 밖에서 기존 `SettingsModal` 또는 `FollowModal`을 렌더링한다.
6. modal close는 room modal state를 `null`로 되돌린다.

## Ownership

- 정렬과 열린 참가자 menu: `RoomParticipantList`
- 참가자 행 trigger와 self/member menu 선택: `RoomParticipantCard`
- Settings/Friends modal visibility: `RoomPlaybackJoinedContent`
- modal 내부 tab/form/follow state: 기존 modal component
4 changes: 4 additions & 0 deletions docs/exec-plans/active/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Active Execution Plans

- [2026-09-01-room-self-participant-menu](./2026-09-01-room-self-participant-menu/plan.md): ci-pending — Draft PR #59, 내 참가자 첫 행·`(나)`·Settings/Friends modal과 reconnect identity QA 통과

- [2026-09-01-profile-room-request-polish](./2026-09-01-profile-room-request-polish/plan.md): ready — Draft PR #59, 최애곡 두 줄 표시·방 편집 치수·노래 신청 문구와 hover/타이포 구현 및 로컬·CI QA 통과

- [2026-09-01-automatic-replay-state](./2026-09-01-automatic-replay-state/plan.md): ci-pending — 자동 순환 상태를 59px 아이콘과 `100cqh` 기반 list viewport 중앙으로 교정하고 로컬 QA·fresh review 통과

- [2026-09-01-settings-badge-tooltip](./2026-09-01-settings-badge-tooltip/plan.md): ci-pending — Draft PR #58에 설정 칭호 acquisitionRate 정규화와 접근성 리스트박스·획득 상세 portal tooltip 게시 완료
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,11 @@ describe("useAddTrackAction", () => {

expect(mocks.setError).toHaveBeenCalledWith(
"story",
"사연은 30자 이하로 입력해주세요.",
"노래 선정 이유는 30자 이하로 입력해주세요.",
);
expect(mocks.notify).toHaveBeenCalledWith({
dedupeKey: "add-track:room:story",
message: "사연은 30자 이하로 입력해주세요.",
message: "노래 선정 이유는 30자 이하로 입력해주세요.",
tone: "error",
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/features/playlist/add-track/hooks/useAddTrackAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export function useAddTrackAction(slug: string, roomAccessToken: string) {

const story = form.storyValue.trim();
if (story.length > ADD_TRACK_STORY_MAX_LENGTH) {
showError("story", "사연은 30자 이하로 입력해주세요.");
showError("story", "노래 선정 이유는 30자 이하로 입력해주세요.");
return;
}

Expand Down
13 changes: 12 additions & 1 deletion src/features/playlist/add-track/ui/AddTrackFormFields.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ function renderFields(errorField: "url" | "story") {
}

describe("AddTrackFormFields", () => {
it("신청 사연을 노래 선정 이유로 안내한다", () => {
renderFields("url");

expect(screen.getByText("노래 선정 이유 (선택)")).toBeInTheDocument();
expect(
screen.getByPlaceholderText(
"이 노래를 선정한 이유나 전하고 싶은 말을 적어주세요",
),
).toBeInTheDocument();
});

it("URL 오류를 URL 입력의 aria-invalid와 SR 설명에 연결한다", () => {
renderFields("url");

Expand All @@ -33,7 +44,7 @@ describe("AddTrackFormFields", () => {
renderFields("story");

const textarea = screen.getByPlaceholderText(
"함께 듣고 싶은 이유나 전하고 싶은 말을 적어주세요",
"이 노래를 선정한 이유나 전하고 싶은 말을 적어주세요",
);
expect(textarea).toHaveAttribute("aria-invalid", "true");
expect(textarea).toHaveAttribute("aria-describedby", "add-track-error");
Expand Down
4 changes: 2 additions & 2 deletions src/features/playlist/add-track/ui/AddTrackFormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ export default function AddTrackFormFields({

<label className={styles.fieldGroup}>
<div className={styles.labelRow}>
<span className={styles.label}>사연 (선택)</span>
<span className={styles.label}>노래 선정 이유 (선택)</span>
<span className={styles.characterCount}>
{storyLength}/{storyMaxLength}
</span>
</div>
<textarea
value={storyValue}
onChange={(event) => onStoryChange(event.target.value)}
placeholder="함께 듣고 싶은 이유나 전하고 싶은 말을 적어주세요"
placeholder="이 노래를 선정한 이유나 전하고 싶은 말을 적어주세요"
className={styles.textarea}
disabled={submitting}
rows={4}
Expand Down
Loading