'use client'; import './style.scss'; import { usePathname, useSearchParams } from 'next/navigation'; import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { BoardSort, PostSearchType } from '@/constants/forum'; import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { fetchApi } from '@/lib/utils/client'; import useErrorAlert from '@/hooks/useErrorAlert'; import Loading from '@/app/component/Loading'; import Pagination from '@/app/component/Pagination'; import { BoardPostsResponse } from '@/types/response/forum/board'; import Post from '@/types/forum/post'; import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import LatestListLayout from './_component/LatestListLayout'; type ViewProps = { _query: { page: number; perPage: number; sort?: BoardSort; search: PostSearchType; keyword?: string; }, _postList: BoardPostsResponse }; export default function View({ _query, _postList }: ViewProps) { const pathname = usePathname(); const searchParams = useSearchParams(); const { setError } = useErrorAlert(); const [loading, setLoading] = useState(false); const [total, setTotal] = useState(_postList.total); const [list, setList] = useState(_postList.list); const [page, setPage] = useState(_query.page); const [perPage, setPerPage] = useState(_query.perPage); const [sort, setSort] = useState(_query.sort); const [search, setSearch] = useState(_query.search); const [keyword, setKeyword] = useState(_query.keyword); const [params, setParams] = useState>({}); const [searchDialogOpen, setSearchDialogOpen] = useState(false); const isMounted = useRef(false); const searchRef = useRef(search); const keywordRef = useRef(keyword); searchRef.current = search; keywordRef.current = keyword; const startIndex = useMemo(() => total - ((page - 1) * perPage), [total, page, perPage]); // 상태 => URL 동기화 useEffect(() => { const alreadyParams = new URLSearchParams(searchParams.toString()); Object.entries(params).forEach(([k, v]) => { if (v) { alreadyParams.set(k, v); } else { alreadyParams.delete(k); } }); const queryString = `?${alreadyParams.toString()}`; if (window.location.search !== queryString) { window.history.replaceState(null, '', `${pathname}${queryString}`); } }, [page, perPage, sort, search, keyword, params, pathname, searchParams]); const handleFetchPosts = useCallback(async () => { try { setLoading(true); const queryParams = new URLSearchParams(); queryParams.set('page', String(page)); queryParams.set('perPage', String(perPage)); if (sort !== undefined && sort !== null) { queryParams.set('sort', String(sort)); } if (searchRef.current !== undefined && searchRef.current !== null) { queryParams.set('search', String(searchRef.current)); } if (keywordRef.current) { queryParams.set('keyword', keywordRef.current); } const res = await fetchApi(`/api/forum/posts?${queryParams.toString()}`); if (!res.data) { setError('게시글을 불러올 수 없습니다.'); } else { setTotal(res.data.total); setList(res.data.list); } } catch (err) { if (err instanceof Error) { setError(err.message || '알 수 없는 오류가 발생했습니다.'); } } finally { setLoading(false); } }, [page, perPage, sort]); const handlePageChange = useCallback((page: number) => { setPage(page); setParams((prev) => ({ ...prev, page: String(page) })); }, []); const handleChange = useCallback((e: React.ChangeEvent) => { const { name, value } = e.target; switch (name) { case 'sort': setSort(Number(value) as BoardSort); break; case 'perPage': setPerPage(Number(value)); break; case 'search': setSearch(Number(value) as PostSearchType); break; case 'keyword': setKeyword(value); break; } if (['perPage', 'search', 'keyword'].includes(name)) { handlePageChange(1); } setParams((prev) => ({ ...prev, [name]: value })); }, [handlePageChange]); const handleSearch = useCallback((e: React.FormEvent) => { e.preventDefault(); handleFetchPosts(); }, [handleFetchPosts]); const handleSearchDialog = useCallback((e: React.FormEvent) => { e.preventDefault(); handleFetchPosts(); setSearchDialogOpen(false); }, [handleFetchPosts]); useEffect(() => { if (!isMounted.current) { isMounted.current = true; return; } handleFetchPosts(); }, [page, perPage, sort, handleFetchPosts]); return (
{loading && }

토론

{/* 정렬 */}
{/* 출력 수 */}
{/* 게시글 목록 */} {/* 검색 */}
{/* 모바일: 검색 아이콘 → Dialog */} 게시글 검색
{/* 데스크톱: 인라인 검색 폼 */}
); }