๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
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>
))}
๋™์ž‘:
  1. ์ผ๋ฐ˜ ํด๋ฆญ: ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์„ ํƒ/ํ•ด์ œ
  2. 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)}
  ...
/>

๊ด€๋ จ ๋ฌธ์„œ