From d1205c69f5831877ffef74ced08cdb36b5956a88 Mon Sep 17 00:00:00 2001 From: Ryu Tae Hyun Date: Tue, 8 Sep 2026 21:35:59 +0900 Subject: [PATCH 1/6] =?UTF-8?q?fix(room):=20=EA=B0=9C=EC=9D=B8=20=ED=81=90?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=98=84=EC=9E=AC=20=EC=8B=A0=EC=B2=AD=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=EC=A4=91=EB=B3=B5=EC=9D=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../room/queue/hooks/useRoomQueuePanel.test.tsx | 14 ++++++++++++++ src/features/room/queue/hooks/useRoomQueuePanel.ts | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/features/room/queue/hooks/useRoomQueuePanel.test.tsx b/src/features/room/queue/hooks/useRoomQueuePanel.test.tsx index 1619470..ca0ed2a 100644 --- a/src/features/room/queue/hooks/useRoomQueuePanel.test.tsx +++ b/src/features/room/queue/hooks/useRoomQueuePanel.test.tsx @@ -267,6 +267,20 @@ describe("useRoomQueuePanel query visibility", () => { expect(result.current.currentEntry).toBeNull(); }); + it("개인 큐의 현재 entryId만 모든 페이지에서 제외하고 같은 영상의 별도 신청과 서버 카운트를 유지한다", () => { + const repeated = { ...pendingEntry, entryId: "another-request" }; + vi.mocked(useMyRoomQueue).mockReturnValue({ + data: { pages: [{ items: [pendingEntry], totalPendingCount: 1 }, { items: [repeated], totalPendingCount: 1 }] }, + error: null, fetchNextQueuePage: vi.fn(), hasNextPage: false, isFetchingNextPage: false, isLoading: false, isRefetching: false, isFetchNextPageError: false, refetch: vi.fn(), + } as unknown as ReturnType); + const { result, rerender } = renderHook(({ currentEntry }) => useRoomQueuePanel({ currentEntry, currentUser: { nickname: "사용자", profileImageUrl: null, slug: "user", userId: 1 }, isCurrentUserLoading: false, roomMeta: null, roomAccessToken: "secret", roomSlug: "room" }), { initialProps: { currentEntry: pendingEntry } }); + act(() => result.current.setActiveTab("mine")); + expect(result.current.myEntries.map((entry) => entry.entryId)).toEqual(["another-request"]); + expect(result.current.myPendingCount).toBe(1); + rerender({ currentEntry: repeated }); + expect(result.current.myEntries.map((entry) => entry.entryId)).toEqual(["entry-1"]); + }); + it("자동 순환 현재곡은 전체 트랙 카드에서 숨기고 전체 탭 상태만 노출한다", () => { const automaticReplayEntry = { ...pendingEntry, diff --git a/src/features/room/queue/hooks/useRoomQueuePanel.ts b/src/features/room/queue/hooks/useRoomQueuePanel.ts index 8e4e69b..275cf95 100644 --- a/src/features/room/queue/hooks/useRoomQueuePanel.ts +++ b/src/features/room/queue/hooks/useRoomQueuePanel.ts @@ -102,8 +102,9 @@ export function useRoomQueuePanel({ allQueueQuery.data?.pages[0]?.totalPendingCount ?? 0; const isOwner = isRoomOwner(roomMeta?.owner, currentUser); const myEntries = useMemo( - () => myQueueData?.pages.flatMap((page) => page.items) ?? [], - [myQueueData?.pages], + () => myQueueData?.pages.flatMap((page) => page.items) + .filter((entry) => entry.entryId !== currentEntry?.entryId) ?? [], + [myQueueData?.pages, currentEntry?.entryId], ); const myPendingCount = isCurrentUserLoading || (Boolean(currentUser) && !myQueueData) From 357562c9b2dbde6b4ada81648e68cf651f22f21b Mon Sep 17 00:00:00 2001 From: Ryu Tae Hyun Date: Tue, 8 Sep 2026 21:36:01 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix(follow):=20=EC=B9=9C=EA=B5=AC=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=AC=B8=EA=B5=AC=EC=99=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=ED=8C=A8=EB=84=90=20=ED=81=AC=EA=B8=B0?= =?UTF-8?q?=EB=A5=BC=20=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/follow/ui/FollowPresenceCard.test.tsx | 12 ++++++------ src/features/follow/ui/FollowPresenceCard.tsx | 1 + src/features/follow/ui/FollowProfileModal.module.css | 7 ++++--- src/features/follow/ui/FollowProfileModal.test.tsx | 6 +++--- src/features/follow/ui/FollowProfileModal.tsx | 5 +++-- src/features/follow/ui/FollowUserCard.module.css | 4 ++-- src/features/follow/ui/FollowUserCard.tsx | 3 +++ .../ui/floating-panel/FloatingPanelShell.module.css | 2 ++ src/shared/ui/floating-panel/FloatingPanelShell.tsx | 3 +++ 9 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/features/follow/ui/FollowPresenceCard.test.tsx b/src/features/follow/ui/FollowPresenceCard.test.tsx index e9112d4..6e78098 100644 --- a/src/features/follow/ui/FollowPresenceCard.test.tsx +++ b/src/features/follow/ui/FollowPresenceCard.test.tsx @@ -21,7 +21,7 @@ const baseUser: FollowUser = { }; describe("FollowPresenceCard", () => { - it("온라인 사용자가 참여 중인 방은 상태 문구 없이 화살표 링크만 제공한다", () => { + it("온라인 사용자가 참여 중인 방은 상태 문구와 화살표 링크를 제공한다", () => { render( { />, ); - expect(screen.queryByText("새벽 재즈 참여 중")).not.toBeInTheDocument(); + expect(screen.getByText("새벽 재즈 참여 중")).toBeInTheDocument(); expect( screen.getByRole("link", { name: "새벽 재즈 방으로 이동" }), ).toHaveAttribute("href", "/room/late-night-jazz"); @@ -41,17 +41,17 @@ describe("FollowPresenceCard", () => { ).toHaveAttribute("aria-describedby", tooltip.id); }); - it("방에 참여하지 않은 온라인 사용자는 접근성 라벨이 있는 초록점만 표시한다", () => { + it("방에 참여하지 않은 온라인 사용자는 접근성 라벨이 있는 초록점과 문구를 표시한다", () => { const { container } = render(); - expect(screen.queryByText("온라인")).not.toBeInTheDocument(); + expect(screen.getByText("온라인")).toBeInTheDocument(); expect(screen.queryByRole("link")).not.toBeInTheDocument(); expect( container.querySelector('[data-online="true"][aria-label="온라인"]'), ).toBeInTheDocument(); }); - it("오프라인이면 stale room을 숨기고 접근성 라벨이 있는 빨간점만 표시한다", () => { + it("오프라인이면 stale room을 숨기고 접근성 라벨이 있는 빨간점과 문구를 표시한다", () => { const { container } = render( { />, ); - expect(screen.queryByText("오프라인")).not.toBeInTheDocument(); + expect(screen.getByText("오프라인")).toBeInTheDocument(); expect(screen.queryByText("새벽 재즈 참여 중")).not.toBeInTheDocument(); expect(screen.queryByRole("link")).not.toBeInTheDocument(); expect( diff --git a/src/features/follow/ui/FollowPresenceCard.tsx b/src/features/follow/ui/FollowPresenceCard.tsx index 525846c..d50ece3 100644 --- a/src/features/follow/ui/FollowPresenceCard.tsx +++ b/src/features/follow/ui/FollowPresenceCard.tsx @@ -17,6 +17,7 @@ export default function FollowPresenceCard({ return ( onSelect(user, trigger) : undefined } diff --git a/src/features/follow/ui/FollowProfileModal.module.css b/src/features/follow/ui/FollowProfileModal.module.css index 88f0f97..81bad9d 100644 --- a/src/features/follow/ui/FollowProfileModal.module.css +++ b/src/features/follow/ui/FollowProfileModal.module.css @@ -20,8 +20,7 @@ } .profilePanelContent { - padding: 0; - overflow: hidden; + overflow-y: auto; } .dialog[data-height-constrained="true"] .profilePanelContent { @@ -33,7 +32,7 @@ display: flex; box-sizing: border-box; flex-direction: column; - height: 100%; + min-height: 100%; padding: 16px 18px 56px; background: #ffffff; color: #3c3c3c; @@ -58,6 +57,8 @@ width: 100%; min-width: 0; height: 28px; + flex-shrink: 0; + white-space: nowrap; padding: 4px 28px; border: 0.5px solid #e9ebee; border-radius: 4px; diff --git a/src/features/follow/ui/FollowProfileModal.test.tsx b/src/features/follow/ui/FollowProfileModal.test.tsx index 1db6a86..02aaff6 100644 --- a/src/features/follow/ui/FollowProfileModal.test.tsx +++ b/src/features/follow/ui/FollowProfileModal.test.tsx @@ -105,7 +105,7 @@ describe("FollowProfileModal", () => { expect(dialog.firstElementChild).toHaveStyle({ height: "160px" }); }); - it("모바일에서는 12px 상하 안전 여백을 제외하고 380px 기본 높이를 유지한다", () => { + it("모바일에서는 12px 상하 안전 여백을 제외하고 431px 기본 높이를 유지한다", () => { Object.defineProperty(window, "innerWidth", { configurable: true, value: 375 }); Object.defineProperty(window, "innerHeight", { configurable: true, value: 500 }); @@ -121,7 +121,7 @@ describe("FollowProfileModal", () => { name: "공개 닉네임 프로필 상세", }); expect(dialog).not.toHaveAttribute("data-height-constrained"); - expect(dialog.firstElementChild).toHaveStyle({ height: "380px" }); + expect(dialog.firstElementChild).toHaveStyle({ height: "431px" }); }); it("프로필 통계와 음악력 값을 2줄 텍스트로 표시한다", () => { @@ -155,7 +155,7 @@ describe("FollowProfileModal", () => { expect( screen.getByRole("dialog", { name: "공개 닉네임 프로필 상세" }) .firstElementChild, - ).toHaveStyle({ height: "304px", width: "240px" }); + ).toHaveStyle({ height: "344.8px", width: "240px" }); expect(screen.queryByRole("button", { name: "음악력 올리기" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "음악력 내리기" })).not.toBeInTheDocument(); }); diff --git a/src/features/follow/ui/FollowProfileModal.tsx b/src/features/follow/ui/FollowProfileModal.tsx index 8d0a720..e25f764 100644 --- a/src/features/follow/ui/FollowProfileModal.tsx +++ b/src/features/follow/ui/FollowProfileModal.tsx @@ -30,8 +30,8 @@ import { import styles from "./FollowProfileModal.module.css"; const PROFILE_PANEL_SIZE = { - compact: { height: 304, width: 240 }, - normal: { height: 380, width: 300 }, + compact: { height: 344.8, width: 240 }, + normal: { height: 431, width: 300 }, } as const; const PROFILE_PANEL_SAFE_MARGIN = { compact: 142.4, @@ -156,6 +156,7 @@ export default function FollowProfileModal({ onBlocked, onClose, user }: Props) > diff --git a/src/features/follow/ui/FollowUserCard.module.css b/src/features/follow/ui/FollowUserCard.module.css index bb05d5b..1d1ad9c 100644 --- a/src/features/follow/ui/FollowUserCard.module.css +++ b/src/features/follow/ui/FollowUserCard.module.css @@ -101,12 +101,12 @@ .status { color: #8d8d8d; font-size: 12px; - font-weight: var(--fw-medium); + font-weight: var(--fw-regular); line-height: 1.4; } .status[data-in-room="true"] { - color: #2f86ed; + color: #3b82f6; } .roomAction { diff --git a/src/features/follow/ui/FollowUserCard.tsx b/src/features/follow/ui/FollowUserCard.tsx index dea3477..116a56a 100644 --- a/src/features/follow/ui/FollowUserCard.tsx +++ b/src/features/follow/ui/FollowUserCard.tsx @@ -16,6 +16,7 @@ type Props = { href: string; label: string; }; + statusLabel?: string; trailingAction?: ReactNode; }; @@ -26,6 +27,7 @@ export default function FollowUserCard({ profileImageUrl, roomLink, trailingAction, + statusLabel, }: Props) { const roomTooltipId = useId(); const profileImageSrc = profileImageUrl || "/Basic_Profile.png"; @@ -52,6 +54,7 @@ export default function FollowUserCard({ {nickname} + {statusLabel ? {statusLabel} : null} ); diff --git a/src/shared/ui/floating-panel/FloatingPanelShell.module.css b/src/shared/ui/floating-panel/FloatingPanelShell.module.css index 9853742..c19ff1f 100644 --- a/src/shared/ui/floating-panel/FloatingPanelShell.module.css +++ b/src/shared/ui/floating-panel/FloatingPanelShell.module.css @@ -121,3 +121,5 @@ padding: 16px; } } + +.panel .content[data-content-padding="none"] { padding: 0; } diff --git a/src/shared/ui/floating-panel/FloatingPanelShell.tsx b/src/shared/ui/floating-panel/FloatingPanelShell.tsx index 739f998..19003b8 100644 --- a/src/shared/ui/floating-panel/FloatingPanelShell.tsx +++ b/src/shared/ui/floating-panel/FloatingPanelShell.tsx @@ -7,6 +7,7 @@ type Props = { children: ReactNode; compactHeader?: boolean; contentClassName?: string; + contentPadding?: "default" | "none"; density?: "auto" | "compact" | "normal"; height: number; width: number; @@ -16,6 +17,7 @@ export default function FloatingPanelShell({ children, compactHeader = false, contentClassName, + contentPadding = "default", density = "auto", height, width, @@ -56,6 +58,7 @@ export default function FloatingPanelShell({
{children} From 782fc425d346c9d33597d9d3fb0a19b376dad8ea Mon Sep 17 00:00:00 2001 From: Ryu Tae Hyun Date: Tue, 8 Sep 2026 21:36:31 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat(playlist):=20=EC=9E=90=EC=A3=BC=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=ED=95=9C=20=EC=9D=8C=EC=95=85=EA=B3=BC=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=20=EA=B2=80=EC=83=89=20=EC=BD=A4=EB=B3=B4?= =?UTF-8?q?=EB=B0=95=EC=8A=A4=EB=A5=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/auth/logout/model/useLogout.ts | 5 +- .../add-track/hooks/useAddTrackAction.ts | 2 + .../add-track/ui/AddTrackFormFields.test.tsx | 6 +- .../add-track/ui/AddTrackFormFields.tsx | 15 +- .../add-track/ui/TrackSearchInput.module.css | 94 ++++++ .../add-track/ui/TrackSearchInput.test.tsx | 222 +++++++++++++ .../add-track/ui/TrackSearchInput.tsx | 310 ++++++++++++++++++ .../api/fetchTrackSuggestions.test.ts | 32 ++ .../playlist/api/fetchTrackSuggestions.ts | 21 ++ .../playlist/model/trackSuggestionKeys.ts | 8 + .../playlist/model/trackSuggestions.test.ts | 25 ++ .../playlist/model/trackSuggestions.ts | 55 ++++ .../playlist/model/useTrackSuggestions.ts | 70 ++++ .../ui/CurrentRequesterCard.tsx | 2 +- src/features/room/queue/ui/RoomQueueCard.tsx | 2 +- .../room/queue/ui/RoomQueueHistoryCard.tsx | 2 +- .../queue/ui/RoomQueueVirtualization.test.tsx | 2 +- .../profile/hooks/useAccountExit.test.tsx | 64 ++++ .../user/profile/hooks/useWithdrawMe.ts | 5 +- src/features/user/session/api/me.ts | 5 +- src/features/user/session/hooks/useMe.ts | 2 +- .../OverflowMarquee.module.css | 17 + .../OverflowMarquee.test.tsx | 0 .../ui/overflow-marquee}/OverflowMarquee.tsx | 9 +- 24 files changed, 947 insertions(+), 28 deletions(-) create mode 100644 src/features/playlist/add-track/ui/TrackSearchInput.module.css create mode 100644 src/features/playlist/add-track/ui/TrackSearchInput.test.tsx create mode 100644 src/features/playlist/add-track/ui/TrackSearchInput.tsx create mode 100644 src/features/playlist/api/fetchTrackSuggestions.test.ts create mode 100644 src/features/playlist/api/fetchTrackSuggestions.ts create mode 100644 src/features/playlist/model/trackSuggestionKeys.ts create mode 100644 src/features/playlist/model/trackSuggestions.test.ts create mode 100644 src/features/playlist/model/trackSuggestions.ts create mode 100644 src/features/playlist/model/useTrackSuggestions.ts create mode 100644 src/features/user/profile/hooks/useAccountExit.test.tsx rename src/{features/room/ui => shared/ui/overflow-marquee}/OverflowMarquee.module.css (70%) rename src/{features/room/ui => shared/ui/overflow-marquee}/OverflowMarquee.test.tsx (100%) rename src/{features/room/ui => shared/ui/overflow-marquee}/OverflowMarquee.tsx (95%) diff --git a/src/features/auth/logout/model/useLogout.ts b/src/features/auth/logout/model/useLogout.ts index bbe9cf7..201bb98 100644 --- a/src/features/auth/logout/model/useLogout.ts +++ b/src/features/auth/logout/model/useLogout.ts @@ -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"; @@ -11,8 +12,10 @@ export function useLogout() { return useMutation({ 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() }); }, }); diff --git a/src/features/playlist/add-track/hooks/useAddTrackAction.ts b/src/features/playlist/add-track/hooks/useAddTrackAction.ts index 5cb3f8a..8fd22e1 100644 --- a/src/features/playlist/add-track/hooks/useAddTrackAction.ts +++ b/src/features/playlist/add-track/hooks/useAddTrackAction.ts @@ -5,6 +5,7 @@ import { type InfiniteData, useQueryClient } from "@tanstack/react-query"; import type { StompSubscription } from "@stomp/stompjs"; import { publishAddTrack } from "@/src/features/playlist/api/websocket/publishAddTrack"; import { fetchRoomQueuePage } from "@/src/features/playlist/api/fetchRoomQueue"; +import { trackSuggestionKeys } from "@/src/features/playlist/model/trackSuggestionKeys"; import { playlistKeys } from "@/src/features/playlist/model/queryKeys"; import type { RoomQueuePage, @@ -152,6 +153,7 @@ export function useAddTrackAction(slug: string, roomAccessToken: string) { scheduleQueryInvalidation({ queryClient, queryKeys: [ + trackSuggestionKeys.frequentRoot(), playlistKeys.roomQueuePrefix(roomSlug), playlistKeys.roomPlaybackPrefix(roomSlug), ], diff --git a/src/features/playlist/add-track/ui/AddTrackFormFields.test.tsx b/src/features/playlist/add-track/ui/AddTrackFormFields.test.tsx index 3ec5df5..980c255 100644 --- a/src/features/playlist/add-track/ui/AddTrackFormFields.test.tsx +++ b/src/features/playlist/add-track/ui/AddTrackFormFields.test.tsx @@ -1,5 +1,6 @@ import { fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; +import { createTestQueryClient, createTestQueryClientWrapper } from "@/src/shared/test/queryClient"; import AddTrackFormFields from "./AddTrackFormFields"; function renderFields( @@ -26,6 +27,7 @@ function renderFields( submitting={false} value="" />, + { wrapper: createTestQueryClientWrapper(createTestQueryClient()) }, ); return { onQueueModeChange }; } @@ -47,7 +49,7 @@ describe("AddTrackFormFields", () => { expect( screen.getByPlaceholderText( - "함께 듣고 싶은 영상 또는 재생목록 URL을 붙여넣으세요", + "노래 제목을 검색하거나 영상·재생목록 URL을 붙여넣으세요", ), ).toBeInTheDocument(); }); @@ -56,7 +58,7 @@ describe("AddTrackFormFields", () => { renderFields("url"); const input = screen.getByPlaceholderText( - "함께 듣고 싶은 영상 또는 재생목록 URL을 붙여넣으세요", + "노래 제목을 검색하거나 영상·재생목록 URL을 붙여넣으세요", ); expect(input).toHaveAttribute("aria-invalid", "true"); expect(input).toHaveAttribute("aria-describedby", "add-track-error"); diff --git a/src/features/playlist/add-track/ui/AddTrackFormFields.tsx b/src/features/playlist/add-track/ui/AddTrackFormFields.tsx index 43a1c96..c8df3da 100644 --- a/src/features/playlist/add-track/ui/AddTrackFormFields.tsx +++ b/src/features/playlist/add-track/ui/AddTrackFormFields.tsx @@ -1,5 +1,6 @@ "use client"; +import TrackSearchInput from "./TrackSearchInput"; import styles from "./AddTrackModal.module.css"; import type { AddTrackErrorField } from "../hooks/useAddTrackForm"; import type { YouTubeQueueMode } from "../model/parseYouTubeQueueSource"; @@ -39,7 +40,7 @@ export default function AddTrackFormFields({ <> {playlistDetected ? ( diff --git a/src/features/playlist/add-track/ui/TrackSearchInput.module.css b/src/features/playlist/add-track/ui/TrackSearchInput.module.css new file mode 100644 index 0000000..4f8bc0e --- /dev/null +++ b/src/features/playlist/add-track/ui/TrackSearchInput.module.css @@ -0,0 +1,94 @@ +.dropdown { + position: fixed; + z-index: 60; + overflow-y: auto; + overscroll-behavior: contain; + border: 1px solid #d9d9d9; + border-radius: 8px; + background: #fff; + color: #3c3c3c; + box-shadow: 0 8px 24px #0002; +} +.row { + display: flex; + align-items: center; + height: 48px; + padding: 0 16px; + gap: 12px; + cursor: pointer; +} +.row:hover, +.row[aria-selected="true"] { + background: #e9ebee; +} +.row[aria-disabled="true"] { + cursor: default; + opacity: 0.55; +} +.history { + flex: 0 0 auto; + width: 20px; + height: 20px; +} +.thumbnail { + flex: 0 0 auto; + width: 32px; + height: 32px; + border-radius: 4px; + object-fit: cover; +} +.meta { + flex: 1; + min-width: 0; +} +.title { + font-size: 16px; + font-weight: var(--fw-regular); + line-height: 1.3; +} +.title mark { + background: transparent; + color: #3b82f6; + font-weight: inherit; +} +.channel { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 14px; + font-weight: var(--fw-regular); + color: #555555; + line-height: 1.3; +} +.status { + padding: 16px; + font-size: 14px; +} +@media (min-width: 481px) and (max-height: 900px) { + .row { + height: 38.4px; + padding: 0 12.8px; + gap: 9.6px; + } + .history { + width: 16px; + height: 16px; + } + .thumbnail { + width: 25.6px; + height: 25.6px; + border-radius: 3.2px; + } + .title { + font-size: 12.8px; + } + .channel, + .status { + font-size: 11.2px; + } +} + +.titleText { + display: block; +} diff --git a/src/features/playlist/add-track/ui/TrackSearchInput.test.tsx b/src/features/playlist/add-track/ui/TrackSearchInput.test.tsx new file mode 100644 index 0000000..8dff972 --- /dev/null +++ b/src/features/playlist/add-track/ui/TrackSearchInput.test.tsx @@ -0,0 +1,222 @@ +import { + act, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; +import { useState } from "react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + createTestQueryClient, + createTestQueryClientWrapper, +} from "@/src/shared/test/queryClient"; +import { ApiError } from "@/src/shared/api/api-error"; +import { + fetchFrequentTracks, + searchYouTubeVideos, +} from "../../api/fetchTrackSuggestions"; +import TrackSearchInput from "./TrackSearchInput"; +const auth = vi.hoisted(() => ({ slug: "me" as string | null })); +vi.mock("@/src/features/user/session/hooks/useMe", () => ({ + useMe: () => ({ + data: auth.slug ? { slug: auth.slug } : null, + isError: false, + }), +})); +vi.mock("../../api/fetchTrackSuggestions", () => ({ + fetchFrequentTracks: vi.fn(), + searchYouTubeVideos: vi.fn(), +})); +const frequent = { + videoId: "video", + provider: "YOUTUBE" as const, + title: "자주 듣는 곡", + thumbnailUrl: null, + durationMs: 1000, + requestCount: 2, +}; +const video = { + videoId: "result", + title: "검색 결과", + channelTitle: "채널", + thumbnailUrl: "https://img.youtube.com/vi/result/default.jpg", + durationMs: 1000, +}; +function Harness() { + const [value, setValue] = useState(""); + return ( + <> + +