Skip to content

Commit 0259683

Browse files
committed
feat: add use-search-params-utils
1 parent ffcb2c3 commit 0259683

File tree

9 files changed

+99
-89
lines changed

9 files changed

+99
-89
lines changed

src/pages/home/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export * from './ui';
22
export * from './model';
3-
export * from './lib';

src/pages/home/lib/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/pages/home/lib/updateUrl.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/pages/home/model/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
export * from './useInitialQueryParams';
21
export * from './useTags';

src/pages/home/model/useInitialQueryParams.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/pages/home/ui/post-manager-page.tsx

Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { useEffect, useState } from 'react';
22
import { Plus } from 'lucide-react';
3-
import { useLocation, useNavigate } from 'react-router-dom';
4-
import { useInitialQueryParams } from '../model/useInitialQueryParams';
5-
import { updateUrl } from '../lib/updateUrl';
3+
import { useSearchParams } from 'react-router-dom';
64
import { Button, Card } from '@/shared/ui';
75
import { PostTable } from '@/features/(post)/list-posts';
86
import { usePosts } from '@/features/(post)/list-posts';
@@ -28,33 +26,31 @@ import { useTags } from '../model';
2826
import type { Post } from '@/entities/post';
2927
import type { Comment } from '@/entities/comment';
3028
import type { User } from '@/entities/user';
29+
import { useSearchParamsUtils } from '@/shared/lib/router';
3130

3231
export function PostsManagerPage() {
33-
const navigate = useNavigate();
34-
const location = useLocation();
35-
const queryParams = new URLSearchParams(location.search);
36-
const initial = useInitialQueryParams();
32+
const [searchParams] = useSearchParams();
33+
const initialSkip = parseInt(searchParams.get('skip') || '0');
34+
const initialLimit = parseInt(searchParams.get('limit') || '10');
35+
const initialSearch = searchParams.get('search') || '';
36+
const initialSortBy = (searchParams.get('sortBy') as any) || 'none';
37+
const initialSortOrder = (searchParams.get('sortOrder') as any) || 'asc';
38+
const initialTag = searchParams.get('tag') || '';
3739

3840
// 상태 관리
39-
const { limit, skip, next, prev, setPageSize, setSkip } = usePagination(
40-
initial.limit,
41-
initial.skip,
42-
);
43-
const { searchQuery, setQuery } = usePostSearch(initial.searchQuery);
41+
const { limit, skip, setPageSize, setSkip } = usePagination(initialLimit, initialSkip);
42+
const { searchQuery, setQuery } = usePostSearch(initialSearch);
4443
const [selectedPost, setSelectedPost] = useState<Post | null>(null);
4544
const {
4645
sortBy,
4746
sortOrder,
4847
setBy: setSortBy,
4948
setOrder: setSortOrder,
50-
} = usePostSort(
51-
(queryParams.get('sortBy') as any) || 'none',
52-
(queryParams.get('sortOrder') as any) || 'asc',
53-
);
49+
} = usePostSort(initialSortBy, initialSortOrder);
5450
const [showAddDialog, setShowAddDialog] = useState(false);
5551
const [showEditDialog, setShowEditDialog] = useState(false);
5652
const { tags } = useTags();
57-
const { selectedTag, setTag: setSelectedTag } = useTagFilter(queryParams.get('tag') || '');
53+
const { selectedTag, setTag: setSelectedTag } = useTagFilter(initialTag);
5854
const [selectedComment, setSelectedComment] = useState<Comment | null>(null);
5955

6056
const [showAddCommentDialog, setShowAddCommentDialog] = useState(false);
@@ -71,9 +67,7 @@ export function PostsManagerPage() {
7167
sortOrder: (sortOrder as any) || 'asc',
7268
});
7369

74-
const updateURL = () => {
75-
updateUrl(navigate, { skip, limit, search: searchQuery, sortBy, sortOrder, tag: selectedTag });
76-
};
70+
const { update: updateParams } = useSearchParamsUtils();
7771

7872
const onPostDeleted = async (_id: number) => {
7973
void refetch();
@@ -107,33 +101,23 @@ export function PostsManagerPage() {
107101
await userModal.show(user.id);
108102
};
109103

110-
useEffect(() => {
111-
updateURL();
112-
}, [skip, limit, sortBy, sortOrder, selectedTag, searchQuery]);
104+
// URL → 상태 동기화만 유지. 상태 → URL은 이벤트 핸들러에서 부분 업데이트로 처리
113105

114106
useEffect(() => {
115-
const params = new URLSearchParams(location.search);
116-
setSkip(parseInt(params.get('skip') || String(initial.skip)));
117-
setPageSize(parseInt(params.get('limit') || String(initial.limit)));
118-
setQuery(params.get('search') || initial.searchQuery);
119-
setSortBy((params.get('sortBy') as any) || initial.sortBy);
120-
setSortOrder((params.get('sortOrder') as any) || initial.sortOrder);
121-
setSelectedTag(params.get('tag') || initial.selectedTag);
122-
}, [
123-
location.search,
124-
setQuery,
125-
setSortBy,
126-
setSortOrder,
127-
setSelectedTag,
128-
initial.skip,
129-
initial.limit,
130-
initial.searchQuery,
131-
initial.sortBy,
132-
initial.sortOrder,
133-
initial.selectedTag,
134-
setPageSize,
135-
setSkip,
136-
]);
107+
const s = parseInt(searchParams.get('skip') || String(initialSkip));
108+
const l = parseInt(searchParams.get('limit') || String(initialLimit));
109+
const q = searchParams.get('search') || initialSearch;
110+
const sb = (searchParams.get('sortBy') as any) || initialSortBy;
111+
const so = (searchParams.get('sortOrder') as any) || initialSortOrder;
112+
const t = searchParams.get('tag') || initialTag;
113+
114+
if (s !== skip) setSkip(s);
115+
if (l !== limit) setPageSize(l);
116+
if (q !== searchQuery) setQuery(q);
117+
if (sb !== sortBy) setSortBy(sb as any);
118+
if (so !== sortOrder) setSortOrder(so as any);
119+
if (t !== selectedTag) setSelectedTag(t);
120+
}, [searchParams]);
137121

138122
const renderPostTable = () => (
139123
<PostTable
@@ -143,7 +127,7 @@ export function PostsManagerPage() {
143127
selectedTag={selectedTag}
144128
onClickTag={(tag) => {
145129
setSelectedTag(tag);
146-
updateURL();
130+
updateParams({ tag }, { push: true });
147131
}}
148132
onOpenDetail={(post) => openPostDetail(post)}
149133
onOpenUser={(userId) => openUserModal({ id: userId } as User)}
@@ -203,14 +187,20 @@ export function PostsManagerPage() {
203187
tags={tags}
204188
onChange={(value) => {
205189
setSelectedTag(value);
206-
updateURL();
190+
updateParams({ tag: value, skip: 0 }, { push: true });
207191
}}
208192
/>
209193
<SortSelect
210194
sortBy={sortBy as any}
211195
sortOrder={sortOrder as any}
212-
onChangeBy={(v) => setSortBy(v as any)}
213-
onChangeOrder={(v) => setSortOrder(v as any)}
196+
onChangeBy={(v) => {
197+
setSortBy(v as any);
198+
updateParams({ sortBy: v as any, skip: 0 }, { push: true });
199+
}}
200+
onChangeOrder={(v) => {
201+
setSortOrder(v as any);
202+
updateParams({ sortOrder: v as any, skip: 0 }, { push: true });
203+
}}
214204
/>
215205
</div>
216206

@@ -220,9 +210,9 @@ export function PostsManagerPage() {
220210
limit={limit}
221211
skip={skip}
222212
total={total}
223-
onPrev={prev}
224-
onNext={next}
225-
onChangeLimit={(v) => setPageSize(v)}
213+
onPrev={() => updateParams({ skip: Math.max(0, skip - limit) }, { push: true })}
214+
onNext={() => updateParams({ skip: Math.min(total, skip + limit) }, { push: true })}
215+
onChangeLimit={(v) => updateParams({ limit: v, skip: 0 }, { push: true })}
226216
/>
227217
</div>
228218
</Card.Content>

src/shared/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './router';

src/shared/lib/router/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './use-search-params';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useSearchParams } from 'react-router-dom';
2+
3+
type Primitive = string | number | boolean;
4+
export type ParamsPatch = Record<string, Primitive | null | undefined>;
5+
6+
export type UpdateParamsOptions = {
7+
push?: boolean; // default: false (replace)
8+
skipNull?: boolean; // default: true (null/undefined 시 삭제)
9+
};
10+
11+
export function useSearchParamsUtils() {
12+
const [searchParams, setSearchParams] = useSearchParams();
13+
14+
const update = (patch: ParamsPatch, options?: UpdateParamsOptions) => {
15+
const { push = false, skipNull = true } = options ?? {};
16+
const next = new URLSearchParams(searchParams);
17+
18+
Object.entries(patch).forEach(([key, value]) => {
19+
if (value === null || value === undefined) {
20+
if (skipNull) next.delete(key);
21+
else next.set(key, '');
22+
return;
23+
}
24+
next.set(key, String(value));
25+
});
26+
27+
const currentStr = searchParams.toString();
28+
const nextStr = next.toString();
29+
if (currentStr !== nextStr) setSearchParams(next, { replace: !push });
30+
};
31+
32+
const set = (key: string, value: Primitive | null | undefined, options?: UpdateParamsOptions) => {
33+
return update({ [key]: value as any }, options);
34+
};
35+
36+
const remove = (key: string, options?: UpdateParamsOptions) => {
37+
const next = new URLSearchParams(searchParams);
38+
next.delete(key);
39+
const currentStr = searchParams.toString();
40+
const nextStr = next.toString();
41+
if (currentStr !== nextStr) setSearchParams(next, { replace: !(options?.push ?? false) });
42+
};
43+
44+
const toObject = () => {
45+
const obj: Record<string, string> = {};
46+
searchParams.forEach((value, key) => {
47+
obj[key] = value;
48+
});
49+
return obj;
50+
};
51+
52+
const get = (key: string) => searchParams.get(key);
53+
54+
return { params: searchParams, update, set, remove, get, toObject } as const;
55+
}

0 commit comments

Comments
 (0)