11import { useBackendAdminClient , useListPaginatedQuery , useListQuery } from "@frontend/common/hooks/useAdminAPI" ;
2+ import { RestartAlt } from "@mui/icons-material" ;
23import {
4+ Button ,
35 Chip ,
46 CircularProgress ,
57 MenuItem ,
@@ -14,7 +16,7 @@ import {
1416 Typography ,
1517} from "@mui/material" ;
1618import { ErrorBoundary , Suspense } from "@suspensive/react" ;
17- import { FC , useMemo } from "react" ;
19+ import { FC , useEffect , useMemo , useState } from "react" ;
1820import { Link , useSearchParams } from "react-router-dom" ;
1921
2022import { AdminFilterFieldset } from "@apps/pyconkr-admin/components/elements/admin_filter_fieldset" ;
@@ -32,55 +34,103 @@ const DEFAULT_PAGE_SIZE = 50;
3234
3335type 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+
3576const 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