diff --git a/src/features/home/components/SearchResultGallery.test.tsx b/src/features/home/components/SearchResultGallery.test.tsx new file mode 100644 index 0000000..5d01421 --- /dev/null +++ b/src/features/home/components/SearchResultGallery.test.tsx @@ -0,0 +1,66 @@ +import { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { SearchResultItem } from '@/features/search/api/searchRecords'; +import { SearchResultGallery } from './SearchResultGallery'; + +const ITEMS: SearchResultItem[] = Array.from({ length: 4 }, (_, index) => ({ + recordId: index + 1, + similarity: 1 - index * 0.1, + place: { + placeId: index + 10, + name: `장소 ${index + 1}`, + address: `주소 ${index + 1}`, + lat: 37.5, + lng: 127, + }, + matchedContext: { + contextId: index + 20, + body: `맥락 ${index + 1}`, + createdAt: '2026-08-12T00:00:00+09:00', + }, + keywords: [], + keywordStatus: 'COMPLETED', + createdAt: '2026-08-12T00:00:00+09:00', +})); + +let container: HTMLDivElement; +let root: Root; + +beforeEach(() => { + (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(() => { + act(() => root.unmount()); + container.remove(); +}); + +describe('SearchResultGallery', () => { + it('가로 탐색 가능한 결과 영역과 응답 순서의 카드를 렌더한다', () => { + const onSelectRecord = vi.fn(); + + act(() => { + root.render(); + }); + + const gallery = container.querySelector('.pl-results__gallery'); + const cards = Array.from(container.querySelectorAll('.pl-results__card')); + + expect(gallery?.getAttribute('role')).toBe('region'); + expect(gallery?.getAttribute('aria-label')).toBe('검색 결과 4곳'); + expect(gallery?.tabIndex).toBe(0); + expect(cards.map((card) => card.textContent)).toEqual([ + expect.stringContaining('장소 1/4'), + expect.stringContaining('장소 2/4'), + expect.stringContaining('장소 3/4'), + expect.stringContaining('장소 4/4'), + ]); + + act(() => cards[2].click()); + expect(onSelectRecord).toHaveBeenCalledWith(3); + }); +}); diff --git a/src/features/home/components/SearchResultGallery.tsx b/src/features/home/components/SearchResultGallery.tsx index 9646a52..86fa9e1 100644 --- a/src/features/home/components/SearchResultGallery.tsx +++ b/src/features/home/components/SearchResultGallery.tsx @@ -26,21 +26,27 @@ interface SearchResultGalleryProps { * `POST /search/records`는 "내 기록의 맥락"만 검색하므로(api-contract.md "Place · 지도 · 검색" * 표) matchedContext.body는 항상 본인 Context 원문이라 공개 범위 규칙(타인 Context 원문 비공개) * 위반이 아니다. attachment='flat'을 쓴 이유는 lifted보다 회전·그림자가 절제돼 있어 좁은 - * 카드(w-72) 안에서 다른 항목(이름·위치·키워드)과 부딪히지 않기 때문 — 디자인 시안이 도착하면 - * 이 선택은 다시 확인한다. + * 카드 안에서 다른 항목(이름·위치·키워드)과 부딪히지 않기 때문 — 디자인 시안이 도착하면 + * 이 선택은 다시 확인한다. S15P11A705-437부터 카드 폭은 고정값이 아니라 갤러리 가용 폭의 + * 3등분이다. 바깥 결과 영역이 좁아져도 세 번째 카드만 잘리는 대신 세 카드가 함께 줄어든다. * editable을 넘기지 않는다(기본 false) — 검색 결과 카드는 Context를 고치는 자리가 아니다. */ export function SearchResultGallery({ items, onSelectRecord }: SearchResultGalleryProps) { const total = items.length; return ( -
+
{items.map((item, index) => (