태그를 선택해 클래식을 검색할 수 있어야 함
/tags
/tags?tag1=태그1
/tags?tag1=태그1&tag2=태그
/tags?tag1=태그1&tag2=태그&tag3=태그3
/tags?tag2=태그2&tag3=태그3
or /tags?tag1=태그2&tag2=태그2
/tags?tag1=태그1&tag3=태그3
or /tags?tag1=태그1&tag2=태그3
app/tags/page.tsx
export default async function TagsPage(props: Props) {
const supabase = createServerClient();
const selectedTags = [
props?.searchParams?.tag1 ?? '',
props?.searchParams?.tag2 ?? '',
props?.searchParams?.tag3 ?? ''
];
const { data } = await supabase.from('allClassics').select();
const tags = [...new Set(data?.map(classic => classic.tags).flat())];
return (
<div className='p-4'>
<TagList tags={tags} selectedTags={selectedTags} />
<ClassicList classics={data ?? []} selectedTags={selectedTags} />
</div>
)
}
app/tags/TagList.tsx
export default function TagList({ tags, selectedTags }: Props) {
const router = useRouter();
const searchParams = useSearchParams();
const handleTagClick = (tagName: string) => {
// URLSearchParams 객체 생성
const params = new URLSearchParams(searchParams.toString());
// 다음에 추가할 태그 인덱스 변수 1로 초기화
let nextTagIndex = 1;
// tag1, tag2, tag3이 존재하고 index가 3 이하인 동안 index 값을 증가시켜 다음 추가할 태그의 인덱스 결정
while (params.has(`tag${nextTagIndex}`) && nextTagIndex <= 3) {
nextTagIndex++;
}
// params를 배열로 변환 후 태그 이름과 일치하는 키를 찾기 (이미 선택된 태그)
const existingTagIndex = Array.from(params.keys())
.find((key) => key.startsWith('tag') && params.get(key) === tagName);
// 이미 선택한 태그인 경우 해당 키를 params에서 삭제
// 추가할 수 있는 경우 index가 3 이하인 경우 새로운 태그를 추가
if (existingTagIndex) {
params.delete(existingTagIndex);
} else if (nextTagIndex <= 3) {
params.set(`tag${nextTagIndex}`, tagName);
}
// params를 문자열로 변환
const newSearchParams = params.toString();
// 새로운 URL로 이동
router.push(`tags?${newSearchParams}`);
};
return (
<div className="flex flex-wrap items-center">
{tags.map(tag => (
<button
key={tag}
onClick={() => handleTagClick(tag)}
className="rounded-sm p-1 bg-white mr-1 mb-1"
>
{tag}
</button>
))}
</div>
);
}
이미 선택된 태그 삭제 후 다시 새 태그를 누르면 이런식으로
/tags?tag1=%EC%8A%AC%ED%94%94&tag3=%EC%9A%B8%EC%A0%81%ED%95%A8
= /tags?tag1=슬픔&tag3=울적함
순서가 뒤죽박죽?이 됨
app/tags/page.tsx
type Props = {
params?: { num?: string; };
searchParams?: { tag1?: string, tag2?: string, tag3?: string };
};
export default async function TagsPage(props: Props) {
const supabase = createServerClient();
const selectedTags = [
decodeURIComponent(props?.searchParams?.tag1 ?? ''),
decodeURIComponent(props?.searchParams?.tag2 ?? ''),
decodeURIComponent(props?.searchParams?.tag3 ?? '')
];
const { data } = await supabase.from('allClassics').select();
return (
<div className='p-4'>
<TagsContainer classics={data ?? []} selectedTags={selectedTags} />
</div>
);
}
app/tags/TagsContainer.tsx
디자인이 조금 허전해서 classics 페이지에도 사용했던 ClassicSearchForm
컴포넌트를 재사용 했다. 일단 검색창에 검색어가 있으면 검색어에 해당하는 태그 배열을 필터해서 보여주는데, 없다면 url을 확인해서 searchParams에 해당하는 태그로 필터한다.
결국 검색을 하거나 태그를 클릭하거나 둘 중 하나밖에 못하기 때문에 허접하지만.. 나중에 어떻게 할지 좀 더 생각해 보고 싶다.
function TagsContainer({ classics, selectedTags }: TagContainerProps) {
const [tagInput, setTagInput] = useState('');
const tags = [...new Set(classics?.map(classic => classic.tags).flat())];
const filteredClassics = tagInput
? classics.filter(classic => classic.tags.includes(tagInput))
: selectedTags[0] === ''
? classics
: classics.filter(classic => selectedTags.some(tag => classic.tags.includes(tag)));
return (
<>
<ClassicSearchForm
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onClick={() => setTagInput('')}
placeholder='클래식 태그 검색하기'
/>
<TagList tags={tags} selectedTags={selectedTags} />
<ClassicList classics={filteredClassics} selectedTags={selectedTags} />
</>
);
}
app/tags/TagList.tsx
function TagList({ tags, selectedTags }: TagListProps) {
const router = useRouter();
const searchParams = useSearchParams();
const handleTagClick = (tagName: string) => {
const encodedTagName = encodeURIComponent(tagName);
const params = new URLSearchParams(searchParams.toString());
let nextTagIndex = 1;
while (params.has(`tag${nextTagIndex}`) && nextTagIndex <= 3) {
nextTagIndex++;
}
const existingTagIndex = Array.from(params.keys())
.find((key) => key.startsWith('tag') && params.get(key) === encodedTagName);
if (existingTagIndex) {
params.delete(existingTagIndex);
} else if (nextTagIndex <= 3) {
params.set(`tag${nextTagIndex}`, encodedTagName);
}
const newSearchParams = params.toString();
router.push(`tags?${newSearchParams}`);
};
return (
<div className="flex flex-wrap items-center">
{tags.map(tag => (
<button
key={tag}
onClick={() => handleTagClick(tag)}
className={`rounded-sm p-1 mr-1 mb-1 ${selectedTags.includes(tag) ? 'bg-violet-400 text-white' : 'bg-white'}`}
>
{tag}
</button>
))}
</div>
);
}
자바스크립트로 기본값을 설정할 때, 예전엔 ||
연산자를 사용했는데 요즘 ??
(Nullish 병합 연산자)를 자주 사용한다. 둘 다 비슷한데 차이점을 몰라서 찾아봤다!
??
: 왼쪽 피연산자가 null
또는 undefined
인 경우 오른쪽 피연산자를 반환||
: 왼쪽 피연산자 값이 falsy 한 값인 경우 오른쪽 피연산자를 반환이번 작업에선 URLSearchParams의 get()
함수를 사용하는데, 이 함수는 파라미터로 전달한 값이 존재하지 않을 경우 null을 반환한다. 이 때 값이 존재하지 않을 경우 대체 값을 제공할 때 ??
연산자를 사용하는 것이 적합하다 생각했다.
Failed to fetch RSC payload. Falling back to browser navigation. TypeError: Failed to execute 'fetch' on 'Window': Failed to read the 'headers' property from 'RequestInit': String contains non ISO-8859-1 code point.
한글로 검색하면 문자 인코딩 에러가 뜬다. 그래서 태그를 클릭해 searchParams를 설정해 검색할 때 URL tag를 encodeURIComponent()
함수로 인코딩하고, 스타일을 위해 decodeURIComponent()
함수를 사용해 원래의 문자열로 복원했다!