Skip to content

Commit 110085f

Browse files
committed
fix: 주문 목록에서 입력 즉시 필터링하는 대신 검색 버튼을 클릭해야 필터링하도록 수정
1 parent 085dc0c commit 110085f

1 file changed

Lines changed: 115 additions & 52 deletions

File tree

  • apps/pyconkr-admin/src/components/pages/shop/order

apps/pyconkr-admin/src/components/pages/shop/order/list.tsx

Lines changed: 115 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { useBackendAdminClient, useListPaginatedQuery, useListQuery } from "@frontend/common/hooks/useAdminAPI";
2+
import { RestartAlt } from "@mui/icons-material";
23
import {
4+
Button,
35
Chip,
46
CircularProgress,
57
MenuItem,
@@ -14,7 +16,7 @@ import {
1416
Typography,
1517
} from "@mui/material";
1618
import { ErrorBoundary, Suspense } from "@suspensive/react";
17-
import { FC, useMemo } from "react";
19+
import { FC, useEffect, useMemo, useState } from "react";
1820
import { Link, useSearchParams } from "react-router-dom";
1921

2022
import { AdminFilterFieldset } from "@apps/pyconkr-admin/components/elements/admin_filter_fieldset";
@@ -32,55 +34,103 @@ const DEFAULT_PAGE_SIZE = 50;
3234

3335
type StatusFilter = "all" | PaymentStatus;
3436

37+
type FilterState = {
38+
name: string;
39+
email: string;
40+
imp_id: string;
41+
status: StatusFilter;
42+
category_group_id: string;
43+
category_id: string;
44+
first_paid_at_after: string;
45+
first_paid_at_before: string;
46+
status_changed_at_after: string;
47+
status_changed_at_before: string;
48+
};
49+
50+
const FILTER_KEYS: (keyof FilterState)[] = [
51+
"name",
52+
"email",
53+
"imp_id",
54+
"status",
55+
"category_group_id",
56+
"category_id",
57+
"first_paid_at_after",
58+
"first_paid_at_before",
59+
"status_changed_at_after",
60+
"status_changed_at_before",
61+
];
62+
63+
const readFilters = (params: URLSearchParams): FilterState => ({
64+
name: params.get("name") ?? "",
65+
email: params.get("email") ?? "",
66+
imp_id: params.get("imp_id") ?? "",
67+
status: (params.get("status") ?? "all") as StatusFilter,
68+
category_group_id: params.get("category_group_id") ?? "",
69+
category_id: params.get("category_id") ?? "",
70+
first_paid_at_after: params.get("first_paid_at_after") ?? "",
71+
first_paid_at_before: params.get("first_paid_at_before") ?? "",
72+
status_changed_at_after: params.get("status_changed_at_after") ?? "",
73+
status_changed_at_before: params.get("status_changed_at_before") ?? "",
74+
});
75+
3576
const InnerOrderList: FC = ErrorBoundary.with(
3677
{ fallback: ErrorFallback },
3778
Suspense.with({ fallback: <CircularProgress /> }, () => {
3879
const client = useBackendAdminClient();
3980
const [searchParams, setSearchParams] = useSearchParams();
4081

41-
const nameQuery = searchParams.get("name") ?? "";
42-
const emailQuery = searchParams.get("email") ?? "";
43-
const impIdQuery = searchParams.get("imp_id") ?? "";
44-
const statusQuery = (searchParams.get("status") ?? "all") as StatusFilter;
45-
const categoryGroupQuery = searchParams.get("category_group_id") ?? "";
46-
const categoryQuery = searchParams.get("category_id") ?? "";
47-
const paidAfter = searchParams.get("first_paid_at_after") ?? "";
48-
const paidBefore = searchParams.get("first_paid_at_before") ?? "";
49-
const statusChangedAfter = searchParams.get("status_changed_at_after") ?? "";
50-
const statusChangedBefore = searchParams.get("status_changed_at_before") ?? "";
5182
const page = Number(searchParams.get("page") ?? 1);
5283
const pageSize = Number(searchParams.get("page_size") ?? DEFAULT_PAGE_SIZE);
5384

85+
// apiParams derives from the URL (the "applied" state); local filter inputs only update the URL on Apply.
5486
const apiParams: Record<string, string> = { page: String(page), page_size: String(pageSize) };
55-
if (nameQuery.trim()) apiParams.name = nameQuery.trim();
56-
if (emailQuery.trim()) apiParams.email = emailQuery.trim();
57-
if (impIdQuery.trim()) apiParams.imp_id = impIdQuery.trim();
58-
if (statusQuery !== "all") apiParams.status = statusQuery;
59-
if (categoryGroupQuery) apiParams.category_group_id = categoryGroupQuery;
60-
if (categoryQuery) apiParams.category_id = categoryQuery;
61-
if (paidAfter) apiParams.first_paid_at_after = paidAfter;
62-
if (paidBefore) apiParams.first_paid_at_before = paidBefore;
63-
if (statusChangedAfter) apiParams.status_changed_at_after = statusChangedAfter;
64-
if (statusChangedBefore) apiParams.status_changed_at_before = statusChangedBefore;
87+
for (const key of FILTER_KEYS) {
88+
const value = searchParams.get(key);
89+
if (!value) continue;
90+
if (key === "status" && value === "all") continue;
91+
if (key === "name" || key === "email" || key === "imp_id") {
92+
const trimmed = value.trim();
93+
if (trimmed) apiParams[key] = trimmed;
94+
} else {
95+
apiParams[key] = value;
96+
}
97+
}
98+
99+
const [filters, setFilters] = useState<FilterState>(() => readFilters(searchParams));
100+
101+
// Re-sync local form state when the URL changes externally (browser back/forward, pagination).
102+
useEffect(() => {
103+
setFilters(readFilters(searchParams));
104+
}, [searchParams]);
65105

66106
const ordersQuery = useListPaginatedQuery<OrderAdmin>(client, "shop", "orders", apiParams);
67107
const groupsQuery = useListQuery<CategoryGroupAdminWithCategories>(client, "shop", "category-groups", {});
68108
const { count = 0, results: orders = [] } = ordersQuery.data ?? {};
69109
const groups = useMemo(() => groupsQuery.data ?? [], [groupsQuery.data]);
70110

71-
const updateFilterParam = (key: string, value: string) => {
111+
const setFilter = <K extends keyof FilterState>(key: K, value: FilterState[K]) => {
112+
setFilters((prev) => ({ ...prev, [key]: value }));
113+
};
114+
115+
const setCategoryGroup = (value: string) => {
116+
setFilters((prev) => ({ ...prev, category_group_id: value, category_id: "" }));
117+
};
118+
119+
const handleApply = () => {
72120
const next = new URLSearchParams(searchParams);
73-
if (value) next.set(key, value);
74-
else next.delete(key);
121+
for (const key of FILTER_KEYS) {
122+
const value = filters[key];
123+
if (value && !(key === "status" && value === "all")) next.set(key, value);
124+
else next.delete(key);
125+
}
75126
next.delete("page");
76127
setSearchParams(next, { replace: true });
77128
};
78129

79-
const setCategoryGroup = (value: string) => {
130+
const handleReset = () => {
131+
setFilters(readFilters(new URLSearchParams()));
80132
const next = new URLSearchParams(searchParams);
81-
if (value) next.set("category_group_id", value);
82-
else next.delete("category_group_id");
83-
next.delete("category_id"); // 그룹 바꾸면 카테고리 선택 초기화
133+
for (const key of FILTER_KEYS) next.delete(key);
84134
next.delete("page");
85135
setSearchParams(next, { replace: true });
86136
};
@@ -109,17 +159,17 @@ const InnerOrderList: FC = ErrorBoundary.with(
109159
size="small"
110160
label="시작"
111161
type="datetime-local"
112-
value={paidAfter?.slice(0, 16) ?? ""}
113-
onChange={(e) => updateFilterParam("first_paid_at_after", e.target.value)}
162+
value={filters.first_paid_at_after.slice(0, 16)}
163+
onChange={(e) => setFilter("first_paid_at_after", e.target.value)}
114164
slotProps={{ inputLabel: { shrink: true } }}
115165
sx={{ minWidth: 220 }}
116166
/>
117167
<TextField
118168
size="small"
119169
label="종료"
120170
type="datetime-local"
121-
value={paidBefore?.slice(0, 16) ?? ""}
122-
onChange={(e) => updateFilterParam("first_paid_at_before", e.target.value)}
171+
value={filters.first_paid_at_before.slice(0, 16)}
172+
onChange={(e) => setFilter("first_paid_at_before", e.target.value)}
123173
slotProps={{ inputLabel: { shrink: true } }}
124174
sx={{ minWidth: 220 }}
125175
/>
@@ -130,36 +180,37 @@ const InnerOrderList: FC = ErrorBoundary.with(
130180
size="small"
131181
label="시작"
132182
type="datetime-local"
133-
value={statusChangedAfter?.slice(0, 16) ?? ""}
134-
onChange={(e) => updateFilterParam("status_changed_at_after", e.target.value)}
183+
value={filters.status_changed_at_after.slice(0, 16)}
184+
onChange={(e) => setFilter("status_changed_at_after", e.target.value)}
135185
slotProps={{ inputLabel: { shrink: true } }}
136186
sx={{ minWidth: 220 }}
137187
/>
138188
<TextField
139189
size="small"
140190
label="종료"
141191
type="datetime-local"
142-
value={statusChangedBefore?.slice(0, 16) ?? ""}
143-
onChange={(e) => updateFilterParam("status_changed_at_before", e.target.value)}
192+
value={filters.status_changed_at_before.slice(0, 16)}
193+
onChange={(e) => setFilter("status_changed_at_before", e.target.value)}
144194
slotProps={{ inputLabel: { shrink: true } }}
145195
sx={{ minWidth: 220 }}
146196
/>
147197
</AdminFilterFieldset>
148198

149199
<AdminFilterFieldset label="분류">
150-
<Select
151-
size="small"
152-
value={statusQuery}
153-
onChange={(e) => updateFilterParam("status", e.target.value === "all" ? "" : (e.target.value as string))}
154-
sx={{ minWidth: 140 }}
155-
>
200+
<Select size="small" value={filters.status} onChange={(e) => setFilter("status", e.target.value as StatusFilter)} sx={{ minWidth: 140 }}>
156201
<MenuItem value="all">전체 상태</MenuItem>
157202
<MenuItem value="pending">대기</MenuItem>
158203
<MenuItem value="completed">완료</MenuItem>
159204
<MenuItem value="partial_refunded">부분환불</MenuItem>
160205
<MenuItem value="refunded">환불</MenuItem>
161206
</Select>
162-
<Select size="small" value={categoryGroupQuery} onChange={(e) => setCategoryGroup(e.target.value)} displayEmpty sx={{ minWidth: 200 }}>
207+
<Select
208+
size="small"
209+
value={filters.category_group_id}
210+
onChange={(e) => setCategoryGroup(e.target.value)}
211+
displayEmpty
212+
sx={{ minWidth: 200 }}
213+
>
163214
<MenuItem value="">전체 카테고리 그룹</MenuItem>
164215
{groups.map((g) => (
165216
<MenuItem key={g.id} value={g.id}>
@@ -169,13 +220,13 @@ const InnerOrderList: FC = ErrorBoundary.with(
169220
</Select>
170221
<Select
171222
size="small"
172-
value={categoryQuery}
173-
onChange={(e) => updateFilterParam("category_id", e.target.value)}
223+
value={filters.category_id}
224+
onChange={(e) => setFilter("category_id", e.target.value)}
174225
displayEmpty
175226
sx={{ minWidth: 200 }}
176227
>
177228
<MenuItem value="">전체 카테고리</MenuItem>
178-
{(categoryGroupQuery ? groups.filter((g) => g.id === categoryGroupQuery) : groups).flatMap((group) => [
229+
{(filters.category_group_id ? groups.filter((g) => g.id === filters.category_group_id) : groups).flatMap((group) => [
179230
<MenuItem key={`group-${group.id}`} disabled sx={{ fontWeight: 600, opacity: "0.8 !important" }}>
180231
{group.name}
181232
</MenuItem>,
@@ -192,27 +243,39 @@ const InnerOrderList: FC = ErrorBoundary.with(
192243
<TextField
193244
size="small"
194245
label="이름 (사용자/고객)"
195-
value={nameQuery}
196-
onChange={(e) => updateFilterParam("name", e.target.value)}
246+
value={filters.name}
247+
onChange={(e) => setFilter("name", e.target.value)}
248+
onKeyDown={(e) => e.key === "Enter" && handleApply()}
197249
sx={{ minWidth: 220 }}
198250
/>
199251
<TextField
200252
size="small"
201253
label="이메일"
202-
value={emailQuery}
203-
onChange={(e) => updateFilterParam("email", e.target.value)}
254+
value={filters.email}
255+
onChange={(e) => setFilter("email", e.target.value)}
256+
onKeyDown={(e) => e.key === "Enter" && handleApply()}
204257
sx={{ minWidth: 220 }}
205258
/>
206259
<TextField
207260
size="small"
208261
label="PortOne imp_id"
209-
value={impIdQuery}
210-
onChange={(e) => updateFilterParam("imp_id", e.target.value)}
262+
value={filters.imp_id}
263+
onChange={(e) => setFilter("imp_id", e.target.value)}
264+
onKeyDown={(e) => e.key === "Enter" && handleApply()}
211265
sx={{ minWidth: 200 }}
212266
/>
213267
</AdminFilterFieldset>
214268
</Stack>
215269

270+
<Stack direction="row" spacing={1}>
271+
<Button variant="contained" onClick={handleApply} size="small">
272+
검색
273+
</Button>
274+
<Button variant="text" onClick={handleReset} size="small" startIcon={<RestartAlt />}>
275+
초기화
276+
</Button>
277+
</Stack>
278+
216279
<Table>
217280
<TableHead>
218281
<TableRow>

0 commit comments

Comments
 (0)