feat: 가맹몰 상세 목록에 필터 초기화를 두고 행 선택을 모든 행에서 살린다
초기화는 카테고리·노출 상태·기간·업체명을 모두 비우고 첫 페이지로 돌아간다(초안과 URL을 함께 되돌린다). 행 선택 체크박스가 상세가 있는 행에만 그려져, 상세가 하나도 없는 목록에서는 기능이 통째로 사라졌다(사용자 지적). 모든 행에서 고를 수 있게 하고, 일괄 비활성·삭제는 그중 상세가 있는 행만 처리한다 — 없는 행은 백엔드가 NOT_FOUND로 거절하므로 보내기 전에 거른다. Co-Authored-By: Claude Opus 5
@f87bb1bdf80adc7fa0b12a8f4f02c0b9bcb9de92
--- app/(protected)/(basic)/system/merchant-details/_components/merchant-detail-list.tsx
+++ app/(protected)/(basic)/system/merchant-details/_components/merchant-detail-list.tsx
... | ... | @@ -25,6 +25,7 @@ |
| 25 | 25 |
} from '@/lib/domain/merchant-detail'; |
| 26 | 26 |
import {
|
| 27 | 27 |
FILTER_ALL, |
| 28 |
+ MERCHANT_DETAILS_PATH, |
|
| 28 | 29 |
MERCHANT_DETAIL_PAGE_SIZE_OPTIONS, |
| 29 | 30 |
buildMerchantDetailHref, |
| 30 | 31 |
clampDateRange, |
... | ... | @@ -79,9 +80,8 @@ |
| 79 | 80 |
endDate: query.endDate, |
| 80 | 81 |
}); |
| 81 | 82 |
|
| 82 |
- const selectableIds = items.filter((row) => row.hasDetail).map((row) => row.id); |
|
| 83 |
- const isAllSelected = |
|
| 84 |
- selectableIds.length > 0 && selectableIds.every((id) => selectedIds.has(id)); |
|
| 83 |
+ const allIds = items.map((row) => row.id); |
|
| 84 |
+ const isAllSelected = allIds.length > 0 && allIds.every((id) => selectedIds.has(id)); |
|
| 85 | 85 |
|
| 86 | 86 |
function go(patch: Partial<MerchantDetailQuery>) {
|
| 87 | 87 |
router.replace(buildMerchantDetailHref(query, { ...patch, page: 1 }));
|
... | ... | @@ -93,7 +93,13 @@ |
| 93 | 93 |
} |
| 94 | 94 |
|
| 95 | 95 |
function toggleAll(checked: boolean) {
|
| 96 |
- setSelectedIds(checked ? new Set(selectableIds) : new Set()); |
|
| 96 |
+ setSelectedIds(checked ? new Set(allIds) : new Set()); |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ /** 모든 필터를 비우고 첫 페이지로 — 초안과 URL을 함께 되돌린다. */ |
|
| 100 |
+ function resetFilters() {
|
|
| 101 |
+ setDraft({ category: FILTER_ALL, visibility: FILTER_ALL, startDate: '', endDate: '' });
|
|
| 102 |
+ router.replace(MERCHANT_DETAILS_PATH); |
|
| 97 | 103 |
} |
| 98 | 104 |
|
| 99 | 105 |
function toggleOne(id: string, checked: boolean) {
|
... | ... | @@ -110,7 +116,13 @@ |
| 110 | 116 |
successMessage: string |
| 111 | 117 |
) {
|
| 112 | 118 |
hideAlert(); |
| 113 |
- const ids = [...selectedIds]; |
|
| 119 |
+ const ids = items |
|
| 120 |
+ .filter((row) => selectedIds.has(row.id) && row.hasDetail) |
|
| 121 |
+ .map((row) => row.id); |
|
| 122 |
+ if (ids.length === 0) {
|
|
| 123 |
+ showToast({ variant: 'warning', message: '상세가 등록된 행만 처리할 수 있습니다.' });
|
|
| 124 |
+ return; |
|
| 125 |
+ } |
|
| 114 | 126 |
startAction(async () => {
|
| 115 | 127 |
const result = await run(ids); |
| 116 | 128 |
showToast( |
... | ... | @@ -122,11 +134,15 @@ |
| 122 | 134 |
}); |
| 123 | 135 |
} |
| 124 | 136 |
|
| 137 |
+ const selectedDetailCount = items.filter( |
|
| 138 |
+ (row) => selectedIds.has(row.id) && row.hasDetail |
|
| 139 |
+ ).length; |
|
| 140 |
+ |
|
| 125 | 141 |
function confirmBulkDelete() {
|
| 126 | 142 |
showAlert({
|
| 127 | 143 |
variant: 'danger', |
| 128 | 144 |
title: '선택한 상세를 삭제하시겠습니까?', |
| 129 |
- message: `${selectedIds.size}건의 상세 정보를 삭제합니다. 삭제 후에는 되돌릴 수 없습니다.`,
|
|
| 145 |
+ message: `${selectedDetailCount}건의 상세 정보를 삭제합니다. 삭제 후에는 되돌릴 수 없습니다.`,
|
|
| 130 | 146 |
actions: ( |
| 131 | 147 |
<FoxButtonGroup size="md"> |
| 132 | 148 |
<FoxButton type="default" size="md" label="취소" onAction={hideAlert} />
|
... | ... | @@ -150,22 +166,21 @@ |
| 150 | 166 |
label="전체 선택" |
| 151 | 167 |
hideLabel |
| 152 | 168 |
checked={isAllSelected}
|
| 153 |
- disabled={selectableIds.length === 0}
|
|
| 169 |
+ disabled={allIds.length === 0}
|
|
| 154 | 170 |
onChange={toggleAll}
|
| 155 | 171 |
/> |
| 156 | 172 |
), |
| 157 | 173 |
width: 56, |
| 158 | 174 |
align: 'center', |
| 159 |
- render: (row) => |
|
| 160 |
- row.hasDetail ? ( |
|
| 161 |
- <FoxCheckbox |
|
| 162 |
- size="md" |
|
| 163 |
- label={`${row.name} 선택`}
|
|
| 164 |
- hideLabel |
|
| 165 |
- checked={selectedIds.has(row.id)}
|
|
| 166 |
- onChange={(checked) => toggleOne(row.id, checked)}
|
|
| 167 |
- /> |
|
| 168 |
- ) : null, |
|
| 175 |
+ render: (row) => ( |
|
| 176 |
+ <FoxCheckbox |
|
| 177 |
+ size="md" |
|
| 178 |
+ label={`${row.name} 선택`}
|
|
| 179 |
+ hideLabel |
|
| 180 |
+ checked={selectedIds.has(row.id)}
|
|
| 181 |
+ onChange={(checked) => toggleOne(row.id, checked)}
|
|
| 182 |
+ /> |
|
| 183 |
+ ), |
|
| 169 | 184 |
}, |
| 170 | 185 |
{
|
| 171 | 186 |
key: 'no', |
... | ... | @@ -329,7 +344,10 @@ |
| 329 | 344 |
} |
| 330 | 345 |
/> |
| 331 | 346 |
</div> |
| 332 |
- <FoxButton type="secondary" size="md" label="적용" onAction={applyFilters} />
|
|
| 347 |
+ <FoxButtonGroup size="md"> |
|
| 348 |
+ <FoxButton type="secondary" size="md" label="적용" onAction={applyFilters} />
|
|
| 349 |
+ <FoxButton type="default" size="md" label="초기화" onAction={resetFilters} />
|
|
| 350 |
+ </FoxButtonGroup> |
|
| 333 | 351 |
</div> |
| 334 | 352 |
} |
| 335 | 353 |
searchFields={[]}
|
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?