useSelection์ ๋ชฉ๋ก ํ์ด์ง์์ ์ฒดํฌ๋ฐ์ค๋ฅผ ํตํ ๋ค์ค ์ ํ์ ๊ด๋ฆฌํ๋ ํ
์
๋๋ค. Shift+ํด๋ฆญ ๋ฒ์ ์ ํ, ์ ์ฒด ์ ํ/ํด์ ๋ฑ์ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
ํต์ฌ ๊ธฐ๋ฅ
๋ค์ค ์ ํ
๊ฐ๋ณ/์ ์ฒด ์ ํ ๊ด๋ฆฌSet ๊ธฐ๋ฐ ํจ์จ์ ์ํ
๋ฒ์ ์ ํ
Shift+ํด๋ฆญ ์ง์์ฐ์๋ ํญ๋ชฉ ํ๋ฒ์ ์ ํ
์๋ ๊ฒ์ฆ
ํ์ด์ง ๋ณ๊ฒฝ ์ ์๋ ์ ๋ฆฌ์กด์ฌํ๋ ํญ๋ชฉ๋ง ์ ์ง
ํ์
์์
์ ๋ค๋ฆญ ํ์
์ง์any ํ์
ํค ์ฌ์ฉ ๊ฐ๋ฅ
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
Import ๋ฐ ์ด๊ธฐํ
import { useSelection } from "@sonamu-kit/react-components";
export function UserListPage() {
const { data } = UserService.useUsers("A", listParams);
const allIds = data?.rows.map(row => row.id) ?? [];
// useSelection ์ด๊ธฐํ
const {
selectedKeys, // ์ ํ๋ ํค ๋ฐฐ์ด
getSelected, // ํน์ ํค๊ฐ ์ ํ๋์๋์ง ํ์ธ
toggle, // ๊ฐ๋ณ ํญ๋ชฉ ์ ํ/ํด์ ํ ๊ธ
selectAll, // ์ ์ฒด ์ ํ
deselectAll, // ์ ์ฒด ํด์
isAllSelected, // ์ ์ฒด ์ ํ ์ฌ๋ถ
handleCheckboxClick, // Shift+ํด๋ฆญ ๋ฒ์ ์ ํ
} = useSelection(allIds, []); // allIds: ์ ์ฒด ํค, []: ์ด๊ธฐ ์ ํ
// ...
}
ํ๋ผ๋ฏธํฐ ์ค๋ช
:
allIds: ํ์ฌ ํ์ด์ง์ ๋ชจ๋ ํค ๋ฐฐ์ด (์: [1, 2, 3, 4, 5])
defaultSelectedKeys: ์ด๊ธฐ ์ ํ ์ํ (์ ํ ์ฌํญ, ๊ธฐ๋ณธ๊ฐ: [])
๊ฐ๋ณ ํญ๋ชฉ ์ ํ
import { Checkbox } from "@sonamu-kit/react-components/components";
const { getSelected, toggle } = useSelection(allIds, []);
<Checkbox
checked={getSelected(row.id)}
onValueChange={() => toggle(row.id)}
/>
๋์:
getSelected(key): ํด๋น ํค๊ฐ ์ ํ๋์์ผ๋ฉด true, ์๋๋ฉด false
toggle(key): ์ ํ ์ํ๋ฅผ ๋ฐ๋๋ก ๋ณ๊ฒฝ
์ ์ฒด ์ ํ/ํด์
const { isAllSelected, selectAll, deselectAll } = useSelection(allIds, []);
<Checkbox
checked={isAllSelected}
onValueChange={(checked) => {
if (checked) {
selectAll();
} else {
deselectAll();
}
}}
/>
๋์:
isAllSelected: ํ์ฌ ํ์ด์ง์ ๋ชจ๋ ํญ๋ชฉ์ด ์ ํ๋์๋์ง
selectAll(): ํ์ฌ ํ์ด์ง์ ๋ชจ๋ ํญ๋ชฉ ์ ํ
deselectAll(): ๋ชจ๋ ์ ํ ํด์
Shift+ํด๋ฆญ ๋ฒ์ ์ ํ
const { handleCheckboxClick } = useSelection(allIds, []);
{rows.map((row, index) => (
<tr key={row.id}>
<td onClick={(e) => handleCheckboxClick(e, index)}>
<Checkbox
checked={getSelected(row.id)}
onValueChange={() => toggle(row.id)}
/>
</td>
{/* ... */}
</tr>
))}
๋์:
- ์ผ๋ฐ ํด๋ฆญ: ๊ฐ๋ณ ํญ๋ชฉ ์ ํ/ํด์
- Shift+ํด๋ฆญ: ๋ง์ง๋ง ์ ํ ์์น๋ถํฐ ํ์ฌ ์์น๊น์ง ๋ชจ๋ ์ ํ
Shift+ํด๋ฆญ ๋์ ์๋ฆฌhandleCheckboxClick์ ๋ง์ง๋ง ํด๋ฆญ ์ธ๋ฑ์ค๋ฅผ ๊ธฐ์ตํ๊ณ , Shift+ํด๋ฆญ ์ ๊ทธ ์ฌ์ด์ ๋ชจ๋ ํญ๋ชฉ์ ์ ํํฉ๋๋ค.ํญ๋ชฉ: [A, B, C, D, E]
1. A ํด๋ฆญ โ A ์ ํ
2. D์ Shift+ํด๋ฆญ โ A, B, C, D ๋ชจ๋ ์ ํ
์ค์ ์์
์์ ํ ํ
์ด๋ธ ๊ตฌํ
import { useSelection } from "@sonamu-kit/react-components";
import { Checkbox, Button, Table } from "@sonamu-kit/react-components/components";
import { UserService } from "@/services/services.generated";
export function UserListPage() {
const { data } = UserService.useUsers("A", listParams);
const { rows } = data ?? {};
const allIds = rows?.map(row => row.id) ?? [];
const {
selectedKeys,
getSelected,
toggle,
selectAll,
deselectAll,
isAllSelected,
handleCheckboxClick,
} = useSelection(allIds, []);
// ๋ฒํฌ ์ญ์
const handleBulkDelete = async () => {
if (selectedKeys.length === 0) {
alert("์ญ์ ํ ํญ๋ชฉ์ ์ ํํ์ธ์");
return;
}
if (confirm(`${selectedKeys.length}๊ฐ ํญ๋ชฉ์ ์ญ์ ํ์๊ฒ ์ต๋๊น?`)) {
await UserService.del(selectedKeys);
deselectAll(); // ์ญ์ ํ ์ ํ ํด์
}
};
return (
<div>
{/* ๋ฒํฌ ์์
๋ฒํผ */}
{selectedKeys.length > 0 && (
<div className="mb-4 flex gap-2">
<span>{selectedKeys.length}๊ฐ ์ ํ๋จ</span>
<Button variant="red" onClick={handleBulkDelete}>
์ ํ ํญ๋ชฉ ์ญ์
</Button>
</div>
)}
<Table>
<thead>
<tr>
{/* ์ ์ฒด ์ ํ ์ฒดํฌ๋ฐ์ค */}
<th className="w-[40px]">
<Checkbox
checked={isAllSelected}
onValueChange={(checked) => {
if (checked) {
selectAll();
} else {
deselectAll();
}
}}
/>
</th>
<th>ID</th>
<th>์ด๋ฆ</th>
<th>์ด๋ฉ์ผ</th>
</tr>
</thead>
<tbody>
{rows?.map((row, index) => (
<tr key={row.id}>
{/* ๊ฐ๋ณ ์ ํ ์ฒดํฌ๋ฐ์ค (Shift+ํด๋ฆญ ์ง์) */}
<td onClick={(e) => handleCheckboxClick(e, index)}>
<Checkbox
checked={getSelected(row.id)}
onValueChange={() => toggle(row.id)}
/>
</td>
<td>{row.id}</td>
<td>{row.username}</td>
<td>{row.email}</td>
</tr>
))}
</tbody>
</Table>
</div>
);
}
ํ์ด์ง ๋ณ๊ฒฝ ์ ์๋ ๊ฒ์ฆ
useSelection์ allIds๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์๋์ผ๋ก ์ ํ ์ํ๋ฅผ ๊ฒ์ฆํฉ๋๋ค.
// 1ํ์ด์ง: rows = [1, 2, 3]
const selection1 = useSelection([1, 2, 3], []);
selection1.toggle(1); // 1 ์ ํ
selection1.toggle(2); // 2 ์ ํ
// selectedKeys = [1, 2]
// 2ํ์ด์ง๋ก ์ด๋: rows = [4, 5, 6]
const selection2 = useSelection([4, 5, 6], []);
// selectedKeys๋ ์๋์ผ๋ก []๋ก ์ ๋ฆฌ๋จ (1, 2๊ฐ allIds์ ์์)
๊ฒ์ฆ ๋ก์ง:
// useSelection ๋ด๋ถ (list-helpers.ts:84-91)
useEffect(() => {
const selectionKeys = Array.from(selection.keys());
const validKeys = intersection(allKeys, selectionKeys);
// ๋ณ๊ฒฝ์ด ํ์ํ ๊ฒฝ์ฐ์๋ง ์ํ ์
๋ฐ์ดํธ
if (validKeys.length !== selectionKeys.length) {
setSelection(new Map(validKeys.map(key => [key, true])));
}
}, [allKeys, selection]);
์ ์๋ ๊ฒ์ฆ์ด ํ์ํ๊ฐ์?ํ์ด์ง๋ฅผ ๋๊ธฐ๊ฑฐ๋ ํํฐ๋ฅผ ๋ณ๊ฒฝํ๋ฉด rows๊ฐ ๋ฐ๋๋๋ค. ์ด์ ํ์ด์ง์์ ์ ํํ ํญ๋ชฉ์ด ์ ํ์ด์ง์ ์์ ์ ์์ผ๋ฏ๋ก, ์๋์ผ๋ก ์ ํจํ์ง ์์ ์ ํ์ ์ ๊ฑฐํฉ๋๋ค.
์ ํ๋ ํญ๋ชฉ์ผ๋ก ์์
const { selectedKeys } = useSelection(allIds, []);
// ์ ํ๋ ํญ๋ชฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
const selectedRows = rows?.filter(row => selectedKeys.includes(row.id)) ?? [];
// ๋ฒํฌ ์์
const handleBulkUpdate = async () => {
await UserService.save({
params: selectedRows.map(row => ({
id: row.id,
status: "active",
}))
});
};
// ๋ฒํฌ ๋ด๋ณด๋ด๊ธฐ
const handleBulkExport = () => {
const csv = selectedRows.map(row =>
`${row.id},${row.username},${row.email}`
).join("\n");
// CSV ๋ค์ด๋ก๋
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "users.csv";
a.click();
};
๊ณ ๊ธ ์ฌ์ฉ
์ด๊ธฐ ์ ํ ์ํ
// URL ํ๋ผ๋ฏธํฐ์์ ์ ํ ์ํ ๋ณต์
const searchParams = new URLSearchParams(window.location.search);
const preselectedIds = searchParams.get("selected")?.split(",").map(Number) ?? [];
const { selectedKeys } = useSelection(allIds, preselectedIds);
์ ํ ์ํ ์ ์ฅ
const { selectedKeys } = useSelection(allIds, []);
// ์ ํ ์ํ๋ฅผ localStorage์ ์ ์ฅ
useEffect(() => {
localStorage.setItem("selectedUserIds", JSON.stringify(selectedKeys));
}, [selectedKeys]);
// ๋ค์ ๋ฐฉ๋ฌธ ์ ๋ณต์
const savedIds = JSON.parse(localStorage.getItem("selectedUserIds") ?? "[]");
const { selectedKeys } = useSelection(allIds, savedIds);
์กฐ๊ฑด๋ถ ์ ํ ์ ํ
const { toggle } = useSelection(allIds, []);
// ํน์ ์กฐ๊ฑด์ ํญ๋ชฉ๋ง ์ ํ ๊ฐ๋ฅ
const handleToggle = (row: UserRow) => {
if (row.role === "admin") {
alert("๊ด๋ฆฌ์๋ ์ ํํ ์ ์์ต๋๋ค");
return;
}
toggle(row.id);
};
<Checkbox
checked={getSelected(row.id)}
onValueChange={() => handleToggle(row)}
disabled={row.role === "admin"}
/>
๋ด๋ถ ๊ตฌ์กฐ
useSelection์ Map<T, boolean> ๊ตฌ์กฐ๋ก ์ ํ ์ํ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
// ๋ด๋ถ ์ํ (๋จ์ํ)
const [selection, setSelection] = useState<Map<number, boolean>>(
new Map([
[1, true], // ID 1 ์ ํ๋จ
[2, false], // ID 2 ์ ํ ์ ๋จ
[3, true], // ID 3 ์ ํ๋จ
])
);
// selectedKeys ์ถ์ถ
const selectedKeys = Array.from(selection)
.filter(([key, value]) => value === true)
.map(([key]) => key);
// ๊ฒฐ๊ณผ: [1, 3]
Map์ ์ฌ์ฉํ๋ ์ด์ :
- O(1) ์๊ฐ ๋ณต์ก๋๋ก ๋น ๋ฅธ ์กฐํ
- ํค์ ํ์
์ ์ฝ ์์ (number, string, object ๋ชจ๋ ๊ฐ๋ฅ)
- Set๋ณด๋ค ํ์ฅ์ฑ ๋์ (์ถ๊ฐ ๋ฉํ๋ฐ์ดํฐ ์ ์ฅ ๊ฐ๋ฅ)
ํ์
์์ ์ฑ
useSelection์ ์ ๋ค๋ฆญ์ผ๋ก ํค ํ์
์ ์ง์ ํ ์ ์์ต๋๋ค.
// number ํ์
const selection1 = useSelection<number>([1, 2, 3], []);
selection1.toggle(1); // โ
OK
selection1.toggle("1"); // โ ํ์
์๋ฌ
// string ํ์
const selection2 = useSelection<string>(["a", "b", "c"], []);
selection2.toggle("a"); // โ
OK
selection2.toggle(1); // โ ํ์
์๋ฌ
// ๋ณต์กํ ๊ฐ์ฒด ํ์
type UserKey = { id: number; email: string };
const keys: UserKey[] = [
{ id: 1, email: "[email protected]" },
{ id: 2, email: "[email protected]" },
];
const selection3 = useSelection<UserKey>(keys, []);
์ฃผ์์ฌํญ
1. allIds๋ ๋ฐ๋์ ์ ๊ณต
// โ ์๋ชป๋ ์ฌ์ฉ
const { selectedKeys } = useSelection([], []); // ๋น ๋ฐฐ์ด
// โ
์ฌ๋ฐ๋ฅธ ์ฌ์ฉ
const allIds = rows?.map(row => row.id) ?? [];
const { selectedKeys } = useSelection(allIds, []);
2. ํ์ด์ง ๋ณ๊ฒฝ ์ ์ ํ ์ ์งํ๊ณ ์ถ๋ค๋ฉด
๊ธฐ๋ณธ์ ์ผ๋ก ํ์ด์ง๋ฅผ ๋๊ธฐ๋ฉด ์ ํ์ด ์ด๊ธฐํ๋ฉ๋๋ค. ์ ์งํ๋ ค๋ฉด ๋ณ๋ ๊ด๋ฆฌ๊ฐ ํ์ํฉ๋๋ค.
// ํ์ด์ง ์ ์ฒด ์ ํ ์ํ (useState๋ก ๋ณ๋ ๊ด๋ฆฌ)
const [globalSelectedIds, setGlobalSelectedIds] = useState<Set<number>>(new Set());
// ํ์ฌ ํ์ด์ง ์ ํ๊ณผ ๋๊ธฐํ
useEffect(() => {
const currentPageSelected = allIds.filter(id => globalSelectedIds.has(id));
// useSelection์ ์ํ์ ๋๊ธฐํ
}, [allIds, globalSelectedIds]);
3. handleCheckboxClick ์ฌ์ฉ ์ ์ฃผ์
handleCheckboxClick์ <td>๋ ์ฒดํฌ๋ฐ์ค ์ปจํ
์ด๋์ ์ฐ๊ฒฐํด์ผ ํฉ๋๋ค.
// โ
์ฌ๋ฐ๋ฅธ ์ฌ์ฉ
<td onClick={(e) => handleCheckboxClick(e, index)}>
<Checkbox ... />
</td>
// โ ์๋ชป๋ ์ฌ์ฉ (ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ์ ํ๋์ง ์์)
<Checkbox
onClick={(e) => handleCheckboxClick(e, index)}
...
/>
๊ด๋ จ ๋ฌธ์