@wipco/sdui 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,904 @@
1
+ import { useSduiDispatch, useSduiCan, SduiDialog } from './chunk-QU7YVMFA.js';
2
+ import { useSduiAdapter, hostFetchOf, defaultAdapter } from './chunk-DVOPZFQS.js';
3
+ import { Card, Input, Button, Icon, Badge, IconButton } from './chunk-WIDDHR7L.js';
4
+ import { boxed } from './chunk-SAN4IX6N.js';
5
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
+
8
+ var PAGE_LIMIT = 500;
9
+ var MAX_PAGES = 10;
10
+ function toJobPosition(row) {
11
+ if (typeof row !== "object" || row === null) return [];
12
+ const record = row;
13
+ const { id, name } = record;
14
+ if (typeof id !== "string" || typeof name !== "string") return [];
15
+ const num = (a, b) => typeof a === "number" ? a : typeof b === "number" ? b : 0;
16
+ const active = record.is_active ?? record.isActive;
17
+ return [
18
+ {
19
+ id,
20
+ name,
21
+ rankOrder: num(record.rank_order, record.rankOrder),
22
+ sortOrder: num(record.sort_order, record.sortOrder),
23
+ isActive: typeof active === "boolean" ? active : true
24
+ }
25
+ ];
26
+ }
27
+ async function fetchAllPositions(api, endpoint) {
28
+ const all = [];
29
+ for (let page = 0; page < MAX_PAGES; page += 1) {
30
+ const body = await api(`${endpoint}?limit=${PAGE_LIMIT}&offset=${page * PAGE_LIMIT}`, {
31
+ method: "GET"
32
+ });
33
+ const rows = Array.isArray(body?.data) ? body.data : [];
34
+ all.push(...rows.flatMap(toJobPosition));
35
+ const total = typeof body?.total === "number" ? body.total : null;
36
+ if (rows.length === 0 || total === null || all.length >= total) break;
37
+ }
38
+ return all;
39
+ }
40
+ function groupByRank(positions) {
41
+ const map = /* @__PURE__ */ new Map();
42
+ for (const position of positions) {
43
+ const group = map.get(position.rankOrder);
44
+ if (group) group.push(position);
45
+ else map.set(position.rankOrder, [position]);
46
+ }
47
+ return [...map.entries()].sort(([left], [right]) => left - right).map(
48
+ ([, group]) => [...group].sort(
49
+ (left, right) => left.sortOrder - right.sortOrder || left.name.localeCompare(right.name, "ko")
50
+ )
51
+ );
52
+ }
53
+ function rankSignature(ranks) {
54
+ return JSON.stringify(ranks.map((rank) => rank.map((p) => p.id)));
55
+ }
56
+ var chipStyle = (inactive, dragEnabled, dragging) => ({
57
+ position: "relative",
58
+ display: "inline-flex",
59
+ alignItems: "center",
60
+ gap: "var(--wp-space-1)",
61
+ borderRadius: "var(--wp-radius-pill)",
62
+ border: inactive ? "var(--wp-border-w) dashed var(--wp-border-default)" : "var(--wp-border-w) solid var(--wp-border-default)",
63
+ background: "var(--wp-surface-card)",
64
+ padding: "2px 6px 2px 12px",
65
+ color: inactive ? "var(--wp-text-muted)" : "var(--wp-text-default)",
66
+ cursor: dragEnabled ? "grab" : void 0,
67
+ opacity: dragging ? 0.5 : 1
68
+ });
69
+ var menuItemStyle = (disabled, danger = false) => ({
70
+ display: "flex",
71
+ alignItems: "center",
72
+ gap: "var(--wp-space-2)",
73
+ width: "100%",
74
+ padding: "var(--wp-space-1) var(--wp-space-2)",
75
+ border: "none",
76
+ borderRadius: "var(--wp-radius-sm)",
77
+ background: "transparent",
78
+ textAlign: "left",
79
+ cursor: disabled ? "not-allowed" : "pointer",
80
+ color: disabled ? "var(--wp-text-disabled)" : danger ? "var(--wp-text-danger)" : "var(--wp-text-default)"
81
+ });
82
+ function MenuItem({
83
+ icon,
84
+ label,
85
+ disabled,
86
+ danger,
87
+ onSelect
88
+ }) {
89
+ return /* @__PURE__ */ jsxs(
90
+ "button",
91
+ {
92
+ type: "button",
93
+ className: "wp-body-sm",
94
+ style: menuItemStyle(!!disabled, !!danger),
95
+ disabled,
96
+ onClick: onSelect,
97
+ onMouseEnter: (e) => {
98
+ if (!disabled) e.currentTarget.style.background = "var(--wp-surface-hover)";
99
+ },
100
+ onMouseLeave: (e) => {
101
+ e.currentTarget.style.background = "transparent";
102
+ },
103
+ children: [
104
+ /* @__PURE__ */ jsx(Icon, { name: icon, size: 14 }),
105
+ label
106
+ ]
107
+ }
108
+ );
109
+ }
110
+ function RankBoardImpl({
111
+ endpoint = "/job-positions",
112
+ entity = "job_position",
113
+ canCreate: canCreateProp,
114
+ canUpdate: canUpdateProp,
115
+ canDelete: canDeleteProp
116
+ }) {
117
+ const dispatch = useSduiDispatch();
118
+ const can = useSduiCan();
119
+ const adapter = useSduiAdapter();
120
+ const api = hostFetchOf(adapter) ?? defaultAdapter.fetch;
121
+ const canCreate = canCreateProp ?? can({ entity, action: "create" });
122
+ const canUpdate = canUpdateProp ?? can({ entity, action: "update" });
123
+ const canDelete = canDeleteProp ?? can({ entity, action: "delete" });
124
+ const [positions, setPositions] = useState([]);
125
+ const [isLoading, setIsLoading] = useState(true);
126
+ const [error, setError] = useState(null);
127
+ const [ranks, setRanks] = useState([]);
128
+ const [saving, setSaving] = useState(false);
129
+ const [drag, setDrag] = useState(null);
130
+ const [dragOver, setDragOver] = useState(null);
131
+ const [newName, setNewName] = useState("");
132
+ const [renameTarget, setRenameTarget] = useState(null);
133
+ const [renameValue, setRenameValue] = useState("");
134
+ const [addTarget, setAddTarget] = useState(null);
135
+ const [addName, setAddName] = useState("");
136
+ const [menuFor, setMenuFor] = useState(null);
137
+ useEffect(() => {
138
+ setRanks(groupByRank(positions));
139
+ }, [positions]);
140
+ const reload = useCallback(async () => {
141
+ try {
142
+ const next = await fetchAllPositions(api, endpoint);
143
+ setPositions(next);
144
+ setError(null);
145
+ } catch (caught) {
146
+ setError(caught instanceof Error ? caught.message : "\uC9C1\uC704 \uBAA9\uB85D\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
147
+ }
148
+ }, [endpoint]);
149
+ useEffect(() => {
150
+ let cancelled = false;
151
+ setIsLoading(true);
152
+ setError(null);
153
+ fetchAllPositions(api, endpoint).then((next) => {
154
+ if (cancelled) return;
155
+ setPositions(next);
156
+ setError(null);
157
+ }).catch((caught) => {
158
+ if (cancelled) return;
159
+ setError(
160
+ caught instanceof Error ? caught.message : "\uC9C1\uC704 \uBAA9\uB85D\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."
161
+ );
162
+ }).finally(() => {
163
+ if (!cancelled) setIsLoading(false);
164
+ });
165
+ return () => {
166
+ cancelled = true;
167
+ };
168
+ }, [endpoint]);
169
+ useEffect(() => {
170
+ if (menuFor === null) return;
171
+ const onPointerDown = (event) => {
172
+ const target = event.target;
173
+ if (!target?.closest?.("[data-rb-menu]")) setMenuFor(null);
174
+ };
175
+ const onKeyDown = (event) => {
176
+ if (event.key === "Escape") setMenuFor(null);
177
+ };
178
+ document.addEventListener("pointerdown", onPointerDown);
179
+ document.addEventListener("keydown", onKeyDown);
180
+ return () => {
181
+ document.removeEventListener("pointerdown", onPointerDown);
182
+ document.removeEventListener("keydown", onKeyDown);
183
+ };
184
+ }, [menuFor]);
185
+ const allPositions = useMemo(() => ranks.flat(), [ranks]);
186
+ const toast = useCallback(
187
+ (tone, message, title) => {
188
+ dispatch({ action: "toast", tone, title, message });
189
+ },
190
+ [dispatch]
191
+ );
192
+ const runMutation = useCallback(
193
+ async (mutate, failTitle) => {
194
+ setSaving(true);
195
+ let ok = true;
196
+ try {
197
+ await mutate();
198
+ } catch (caught) {
199
+ ok = false;
200
+ toast(
201
+ "danger",
202
+ caught instanceof Error ? caught.message : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.",
203
+ failTitle
204
+ );
205
+ }
206
+ dispatch({ action: "invalidate", prefix: endpoint });
207
+ await reload();
208
+ setSaving(false);
209
+ return ok;
210
+ },
211
+ [dispatch, endpoint, reload, toast]
212
+ );
213
+ const persistOrder = useCallback(
214
+ (next) => {
215
+ if (saving) return;
216
+ const cleaned = next.filter((rank) => rank.length > 0);
217
+ if (rankSignature(cleaned) === rankSignature(ranks)) return;
218
+ setRanks(cleaned);
219
+ void runMutation(
220
+ () => api(`${endpoint}/order`, {
221
+ method: "PUT",
222
+ body: { ranks: cleaned.map((rank) => rank.map((p) => p.id)) }
223
+ }).then(() => void 0),
224
+ "\uC21C\uC11C \uC800\uC7A5 \uC2E4\uD328"
225
+ );
226
+ },
227
+ [endpoint, ranks, runMutation, saving]
228
+ );
229
+ const moveRank = (index, delta) => {
230
+ const target = index + delta;
231
+ if (target < 0 || target >= ranks.length) return;
232
+ const copy = ranks.map((rank) => [...rank]);
233
+ [copy[index], copy[target]] = [copy[target], copy[index]];
234
+ persistOrder(copy);
235
+ };
236
+ const moveRankToBoundary = (from, boundary) => {
237
+ if (from < 0 || from >= ranks.length) return;
238
+ const copy = ranks.map((rank) => [...rank]);
239
+ const [row] = copy.splice(from, 1);
240
+ const target = boundary > from ? boundary - 1 : boundary;
241
+ copy.splice(target, 0, row);
242
+ persistOrder(copy);
243
+ };
244
+ const moveChipToRank = (id, targetIndex) => {
245
+ const chip = allPositions.find((position) => position.id === id);
246
+ const target = ranks[targetIndex];
247
+ if (!chip || !target) return;
248
+ if (target.some((position) => position.id === id)) return;
249
+ const copy = ranks.map((rank) => rank.filter((position) => position.id !== id));
250
+ copy[targetIndex] = [...copy[targetIndex], chip];
251
+ persistOrder(copy);
252
+ };
253
+ const moveChipToNewRank = (id, boundary) => {
254
+ const chip = allPositions.find((position) => position.id === id);
255
+ if (!chip) return;
256
+ const copy = ranks.map((rank) => rank.filter((position) => position.id !== id));
257
+ copy.splice(boundary, 0, [chip]);
258
+ persistOrder(copy);
259
+ };
260
+ const endDrag = useCallback(() => {
261
+ setDrag(null);
262
+ setDragOver(null);
263
+ }, []);
264
+ const updateDragOver = useCallback((next) => {
265
+ setDragOver(
266
+ (prev) => prev && prev.kind === next.kind && prev.index === next.index ? prev : next
267
+ );
268
+ }, []);
269
+ const handleRowDrop = (event, index) => {
270
+ event.preventDefault();
271
+ setDragOver(null);
272
+ if (!drag) return;
273
+ setDrag(null);
274
+ if (saving) return;
275
+ if (drag.kind === "chip") {
276
+ moveChipToRank(drag.id, index);
277
+ return;
278
+ }
279
+ if (drag.index === index || drag.index >= ranks.length) return;
280
+ const copy = ranks.map((rank) => [...rank]);
281
+ const [row] = copy.splice(drag.index, 1);
282
+ copy.splice(index, 0, row);
283
+ persistOrder(copy);
284
+ };
285
+ const handleGapDrop = (event, boundary) => {
286
+ event.preventDefault();
287
+ setDragOver(null);
288
+ if (!drag) return;
289
+ setDrag(null);
290
+ if (saving) return;
291
+ if (drag.kind === "chip") moveChipToNewRank(drag.id, boundary);
292
+ else moveRankToBoundary(drag.index, boundary);
293
+ };
294
+ const duplicateName = (name, exceptId) => allPositions.some((position) => position.id !== exceptId && position.name === name);
295
+ const createPosition = (input) => api(endpoint, { method: "POST", body: input }).then(() => void 0);
296
+ const handleCreate = async () => {
297
+ const name = newName.trim();
298
+ if (!name) {
299
+ toast("danger", "\uC9C1\uC704 \uC774\uB984\uC744 \uC785\uB825\uD558\uC138\uC694.", "\uC785\uB825 \uC624\uB958");
300
+ return;
301
+ }
302
+ if (duplicateName(name)) {
303
+ toast("danger", "\uC774\uBBF8 \uB4F1\uB85D\uB41C \uC9C1\uC704\uC785\uB2C8\uB2E4.", "\uC785\uB825 \uC624\uB958");
304
+ return;
305
+ }
306
+ const nextRank = allPositions.length > 0 ? Math.max(...allPositions.map((p) => p.rankOrder)) + 1 : 1;
307
+ const ok = await runMutation(
308
+ () => createPosition({ name, rankOrder: nextRank, sortOrder: 1 }),
309
+ "\uC9C1\uC704 \uB4F1\uB85D \uC2E4\uD328"
310
+ );
311
+ if (ok) {
312
+ setNewName("");
313
+ toast("success", `'${name}' \uC9C1\uC704\uB97C \uB9E8 \uC544\uB798 \uC0C8 \uAE09\uC73C\uB85C \uB4F1\uB85D\uD588\uC2B5\uB2C8\uB2E4.`);
314
+ }
315
+ };
316
+ const handleAddToRank = async () => {
317
+ if (addTarget === null) return;
318
+ const rank = ranks[addTarget];
319
+ if (!rank || rank.length === 0) return;
320
+ const name = addName.trim();
321
+ if (!name) {
322
+ toast("danger", "\uC9C1\uC704 \uC774\uB984\uC744 \uC785\uB825\uD558\uC138\uC694.", "\uC785\uB825 \uC624\uB958");
323
+ return;
324
+ }
325
+ if (duplicateName(name)) {
326
+ toast("danger", "\uC774\uBBF8 \uB4F1\uB85D\uB41C \uC9C1\uC704\uC785\uB2C8\uB2E4.", "\uC785\uB825 \uC624\uB958");
327
+ return;
328
+ }
329
+ const ok = await runMutation(
330
+ () => createPosition({
331
+ name,
332
+ rankOrder: rank[0].rankOrder,
333
+ sortOrder: Math.max(...rank.map((p) => p.sortOrder)) + 1
334
+ }),
335
+ "\uC9C1\uC704 \uB4F1\uB85D \uC2E4\uD328"
336
+ );
337
+ if (ok) {
338
+ setAddTarget(null);
339
+ setAddName("");
340
+ toast("success", `'${name}' \uC9C1\uC704\uB97C \uAC19\uC740 \uAE09\uC5D0 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`);
341
+ }
342
+ };
343
+ const handleRename = async () => {
344
+ if (!renameTarget) return;
345
+ const name = renameValue.trim();
346
+ if (!name) {
347
+ toast("danger", "\uC9C1\uC704 \uC774\uB984\uC744 \uC785\uB825\uD558\uC138\uC694.", "\uC785\uB825 \uC624\uB958");
348
+ return;
349
+ }
350
+ if (name === renameTarget.name) {
351
+ setRenameTarget(null);
352
+ return;
353
+ }
354
+ if (duplicateName(name, renameTarget.id)) {
355
+ toast("danger", "\uC774\uBBF8 \uB4F1\uB85D\uB41C \uC9C1\uC704\uC785\uB2C8\uB2E4.", "\uC785\uB825 \uC624\uB958");
356
+ return;
357
+ }
358
+ const ok = await runMutation(
359
+ () => api(`${endpoint}/${encodeURIComponent(renameTarget.id)}`, {
360
+ method: "PATCH",
361
+ body: { name }
362
+ }).then(() => void 0),
363
+ "\uC774\uB984 \uBCC0\uACBD \uC2E4\uD328"
364
+ );
365
+ if (ok) {
366
+ setRenameTarget(null);
367
+ toast("success", `\uC9C1\uC704 \uC774\uB984\uC744 '${name}'(\uC73C)\uB85C \uBCC0\uACBD\uD588\uC2B5\uB2C8\uB2E4.`);
368
+ }
369
+ };
370
+ const toggleActive = (position) => {
371
+ void runMutation(
372
+ () => api(`${endpoint}/${encodeURIComponent(position.id)}`, {
373
+ method: "PATCH",
374
+ body: { isActive: !position.isActive }
375
+ }).then(() => void 0),
376
+ "\uC0AC\uC6A9 \uC5EC\uBD80 \uBCC0\uACBD \uC2E4\uD328"
377
+ );
378
+ };
379
+ const handleDelete = (position) => {
380
+ if (!window.confirm(
381
+ `'${position.name}' \uC9C1\uC704\uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? \uC0AD\uC81C\uB41C \uC9C1\uC704\uB294 \uC124\uC815 > \uD734\uC9C0\uD1B5\uC5D0\uC11C \uBCF5\uAD6C\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
382
+ ))
383
+ return;
384
+ void runMutation(
385
+ () => api(`${endpoint}/${encodeURIComponent(position.id)}`, {
386
+ method: "DELETE"
387
+ }).then(() => void 0),
388
+ "\uC9C1\uC704 \uC0AD\uC81C \uC2E4\uD328"
389
+ );
390
+ };
391
+ const dragEnabled = canUpdate && !saving;
392
+ const gap = (boundary) => {
393
+ const hot = drag && dragOver?.kind === "gap" && dragOver.index === boundary;
394
+ return /* @__PURE__ */ jsx(
395
+ "div",
396
+ {
397
+ style: {
398
+ height: hot ? 40 : drag ? 20 : 8,
399
+ borderRadius: "var(--wp-radius-md)",
400
+ border: hot ? "var(--wp-border-w) dashed var(--wp-border-brand)" : "var(--wp-border-w) dashed transparent",
401
+ background: hot ? "var(--wp-accent-soft)" : "transparent",
402
+ transition: "height .12s ease, background .12s ease"
403
+ },
404
+ onDragOver: (event) => {
405
+ event.preventDefault();
406
+ event.dataTransfer.dropEffect = "move";
407
+ updateDragOver({ kind: "gap", index: boundary });
408
+ },
409
+ onDragLeave: (event) => {
410
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragOver(null);
411
+ },
412
+ onDrop: (event) => handleGapDrop(event, boundary)
413
+ },
414
+ `gap-${boundary}`
415
+ );
416
+ };
417
+ return /* @__PURE__ */ jsxs("div", { className: "wp-col", style: { gap: "var(--wp-space-4)" }, children: [
418
+ canCreate ? /* @__PURE__ */ jsx(Card, { padding: "tight", children: /* @__PURE__ */ jsxs(
419
+ "form",
420
+ {
421
+ style: {
422
+ display: "flex",
423
+ flexWrap: "wrap",
424
+ alignItems: "center",
425
+ gap: "var(--wp-space-2)"
426
+ },
427
+ onSubmit: (event) => {
428
+ event.preventDefault();
429
+ void handleCreate();
430
+ },
431
+ children: [
432
+ /* @__PURE__ */ jsx(
433
+ Input,
434
+ {
435
+ style: { width: 256 },
436
+ value: newName,
437
+ disabled: saving,
438
+ placeholder: "\uC0C8 \uC9C1\uC704 \uC774\uB984 (\uC608: \uC120\uC784\uC5F0\uAD6C\uC6D0)",
439
+ "aria-label": "\uC0C8 \uC9C1\uC704 \uC774\uB984",
440
+ onChange: (event) => setNewName(event.target.value)
441
+ }
442
+ ),
443
+ /* @__PURE__ */ jsx(Button, { type: "submit", variant: "primary", icon: "plus", disabled: saving || !newName.trim(), children: "\uB4F1\uB85D" }),
444
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", style: { color: "var(--wp-text-muted)" }, children: "\uB9E8 \uC544\uB798 \uC0C8 \uAE09\uC73C\uB85C \uB4F1\uB85D\uB429\uB2C8\uB2E4. \uAE30\uC874 \uAE09\uC5D0 \uB123\uC73C\uB824\uBA74 \uAC01 \uC904\uC758 '\uC774 \uAE09\uC5D0 \uCD94\uAC00'\uB97C \uC4F0\uAC70\uB098 \uCE69\uC744 \uB4DC\uB798\uADF8\uD558\uC138\uC694." })
445
+ ]
446
+ }
447
+ ) }) : null,
448
+ error ? /* @__PURE__ */ jsxs(
449
+ "div",
450
+ {
451
+ className: "wp-body-sm",
452
+ style: {
453
+ display: "flex",
454
+ flexWrap: "wrap",
455
+ alignItems: "center",
456
+ gap: "var(--wp-space-3)",
457
+ border: "var(--wp-border-w) solid var(--wp-status-danger-border)",
458
+ background: "var(--wp-status-danger-bg)",
459
+ color: "var(--wp-status-danger-fg)",
460
+ borderRadius: "var(--wp-radius-md)",
461
+ padding: "var(--wp-space-3)"
462
+ },
463
+ children: [
464
+ /* @__PURE__ */ jsx("span", { children: error }),
465
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => void reload(), children: "\uB2E4\uC2DC \uC2DC\uB3C4" })
466
+ ]
467
+ }
468
+ ) : null,
469
+ isLoading && ranks.length === 0 ? /* @__PURE__ */ jsx(
470
+ "p",
471
+ {
472
+ className: "wp-body-sm",
473
+ style: {
474
+ padding: "var(--wp-space-8) 0",
475
+ textAlign: "center",
476
+ color: "var(--wp-text-muted)",
477
+ margin: 0
478
+ },
479
+ children: "\uC9C1\uC704 \uBAA9\uB85D\uC744 \uBD88\uB7EC\uC624\uB294 \uC911..."
480
+ }
481
+ ) : ranks.length === 0 ? !error && /* @__PURE__ */ jsx(
482
+ "div",
483
+ {
484
+ className: "wp-body-sm",
485
+ style: {
486
+ border: "2px dashed var(--wp-border-default)",
487
+ borderRadius: "var(--wp-radius-lg)",
488
+ padding: "var(--wp-space-10) 0",
489
+ textAlign: "center",
490
+ color: "var(--wp-text-muted)"
491
+ },
492
+ children: "\uB4F1\uB85D\uB41C \uC9C1\uC704\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uC704\uC5D0\uC11C \uCCAB \uC9C1\uC704\uB97C \uB4F1\uB85D\uD558\uC138\uC694."
493
+ }
494
+ ) : /* @__PURE__ */ jsxs("div", { className: "wp-col", style: { gap: 0 }, children: [
495
+ ranks.map((rank, rowIndex) => {
496
+ const rowDragging = drag?.kind === "rank" && drag.index === rowIndex;
497
+ const rowHot = drag && !rowDragging && dragOver?.kind === "row" && dragOver.index === rowIndex;
498
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
499
+ gap(rowIndex),
500
+ /* @__PURE__ */ jsxs(
501
+ "div",
502
+ {
503
+ style: {
504
+ display: "flex",
505
+ alignItems: "center",
506
+ gap: "var(--wp-space-3)",
507
+ border: rowHot ? "var(--wp-border-w) solid var(--wp-border-brand)" : "var(--wp-border-w) solid var(--wp-border-default)",
508
+ background: rowHot ? "var(--wp-accent-soft)" : "var(--wp-surface-card)",
509
+ borderRadius: "var(--wp-radius-lg)",
510
+ padding: "var(--wp-space-3)",
511
+ opacity: rowDragging ? 0.5 : 1,
512
+ transition: "background .12s ease, border-color .12s ease"
513
+ },
514
+ onDragOver: (event) => {
515
+ event.preventDefault();
516
+ event.dataTransfer.dropEffect = "move";
517
+ updateDragOver({ kind: "row", index: rowIndex });
518
+ },
519
+ onDragLeave: (event) => {
520
+ if (!event.currentTarget.contains(event.relatedTarget))
521
+ setDragOver(null);
522
+ },
523
+ onDrop: (event) => handleRowDrop(event, rowIndex),
524
+ children: [
525
+ canUpdate ? /* @__PURE__ */ jsx(
526
+ "div",
527
+ {
528
+ style: {
529
+ flex: "none",
530
+ cursor: "grab",
531
+ color: "var(--wp-text-muted)",
532
+ pointerEvents: saving ? "none" : void 0,
533
+ opacity: saving ? 0.5 : 1,
534
+ display: "inline-flex"
535
+ },
536
+ draggable: dragEnabled,
537
+ onDragStart: (event) => {
538
+ event.dataTransfer.effectAllowed = "move";
539
+ event.dataTransfer.setData("text/plain", `rank-${rowIndex}`);
540
+ setDrag({ kind: "rank", index: rowIndex });
541
+ },
542
+ onDragEnd: endDrag,
543
+ title: "\uB4DC\uB798\uADF8\uD574\uC11C \uAE09 \uC21C\uC11C \uBCC0\uACBD",
544
+ children: /* @__PURE__ */ jsx(Icon, { name: "move", size: 16 })
545
+ }
546
+ ) : null,
547
+ /* @__PURE__ */ jsx(
548
+ "div",
549
+ {
550
+ className: "wp-label",
551
+ style: {
552
+ flex: "none",
553
+ width: 32,
554
+ height: 32,
555
+ display: "inline-flex",
556
+ alignItems: "center",
557
+ justifyContent: "center",
558
+ borderRadius: "var(--wp-radius-pill)",
559
+ background: "var(--wp-accent-soft)",
560
+ color: "var(--wp-text-brand)"
561
+ },
562
+ children: rowIndex + 1
563
+ }
564
+ ),
565
+ /* @__PURE__ */ jsxs(
566
+ "div",
567
+ {
568
+ style: {
569
+ display: "flex",
570
+ flex: 1,
571
+ flexWrap: "wrap",
572
+ alignItems: "center",
573
+ gap: "var(--wp-space-2)"
574
+ },
575
+ children: [
576
+ rank.map((position) => /* @__PURE__ */ jsxs(
577
+ "div",
578
+ {
579
+ className: "wp-body-sm",
580
+ draggable: dragEnabled,
581
+ onDragStart: (event) => {
582
+ event.dataTransfer.effectAllowed = "move";
583
+ event.dataTransfer.setData("text/plain", position.id);
584
+ setDrag({ kind: "chip", id: position.id });
585
+ },
586
+ onDragEnd: endDrag,
587
+ style: chipStyle(
588
+ !position.isActive,
589
+ dragEnabled,
590
+ drag?.kind === "chip" && drag.id === position.id
591
+ ),
592
+ children: [
593
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 600 }, children: position.name }),
594
+ !position.isActive ? /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "\uBBF8\uC0AC\uC6A9" }) : null,
595
+ canUpdate || canDelete ? /* @__PURE__ */ jsxs("span", { "data-rb-menu": true, style: { position: "relative", display: "inline-flex" }, children: [
596
+ /* @__PURE__ */ jsx(
597
+ IconButton,
598
+ {
599
+ icon: "ellipsis-vertical",
600
+ size: "xs",
601
+ label: `${position.name} \uC9C1\uC704 \uBA54\uB274`,
602
+ disabled: saving,
603
+ onClick: () => setMenuFor((cur) => cur === position.id ? null : position.id)
604
+ }
605
+ ),
606
+ menuFor === position.id ? /* @__PURE__ */ jsxs(
607
+ "div",
608
+ {
609
+ className: "wp-col",
610
+ style: {
611
+ position: "absolute",
612
+ top: "calc(100% + 4px)",
613
+ left: 0,
614
+ zIndex: "var(--wp-z-dropdown)",
615
+ minWidth: 172,
616
+ gap: 2,
617
+ padding: "var(--wp-space-1)",
618
+ border: "var(--wp-border-w) solid var(--wp-border-default)",
619
+ borderRadius: "var(--wp-radius-md)",
620
+ background: "var(--wp-surface-card)",
621
+ boxShadow: "var(--wp-shadow-md)"
622
+ },
623
+ children: [
624
+ canUpdate ? /* @__PURE__ */ jsxs(Fragment, { children: [
625
+ /* @__PURE__ */ jsx(
626
+ MenuItem,
627
+ {
628
+ icon: "chevron-up",
629
+ label: "\uC704 \uAE09\uC73C\uB85C \uC774\uB3D9",
630
+ disabled: rowIndex === 0,
631
+ onSelect: () => {
632
+ setMenuFor(null);
633
+ moveChipToRank(position.id, rowIndex - 1);
634
+ }
635
+ }
636
+ ),
637
+ /* @__PURE__ */ jsx(
638
+ MenuItem,
639
+ {
640
+ icon: "chevron-down",
641
+ label: "\uC544\uB798 \uAE09\uC73C\uB85C \uC774\uB3D9",
642
+ disabled: rowIndex === ranks.length - 1,
643
+ onSelect: () => {
644
+ setMenuFor(null);
645
+ moveChipToRank(position.id, rowIndex + 1);
646
+ }
647
+ }
648
+ ),
649
+ /* @__PURE__ */ jsx(
650
+ MenuItem,
651
+ {
652
+ icon: "split",
653
+ label: "\uC0C8 \uAE09\uC73C\uB85C \uBD84\uB9AC",
654
+ disabled: rank.length < 2,
655
+ onSelect: () => {
656
+ setMenuFor(null);
657
+ moveChipToNewRank(position.id, rowIndex + 1);
658
+ }
659
+ }
660
+ ),
661
+ /* @__PURE__ */ jsx(
662
+ "div",
663
+ {
664
+ style: {
665
+ height: 1,
666
+ background: "var(--wp-divider)",
667
+ margin: "2px 0"
668
+ }
669
+ }
670
+ ),
671
+ /* @__PURE__ */ jsx(
672
+ MenuItem,
673
+ {
674
+ icon: "pencil",
675
+ label: "\uC774\uB984 \uBCC0\uACBD",
676
+ onSelect: () => {
677
+ setMenuFor(null);
678
+ setRenameTarget(position);
679
+ setRenameValue(position.name);
680
+ }
681
+ }
682
+ ),
683
+ /* @__PURE__ */ jsx(
684
+ MenuItem,
685
+ {
686
+ icon: position.isActive ? "eye-off" : "eye",
687
+ label: position.isActive ? "\uBBF8\uC0AC\uC6A9\uC73C\uB85C \uC804\uD658" : "\uC0AC\uC6A9\uC73C\uB85C \uC804\uD658",
688
+ onSelect: () => {
689
+ setMenuFor(null);
690
+ toggleActive(position);
691
+ }
692
+ }
693
+ )
694
+ ] }) : null,
695
+ canDelete ? /* @__PURE__ */ jsxs(Fragment, { children: [
696
+ canUpdate ? /* @__PURE__ */ jsx(
697
+ "div",
698
+ {
699
+ style: {
700
+ height: 1,
701
+ background: "var(--wp-divider)",
702
+ margin: "2px 0"
703
+ }
704
+ }
705
+ ) : null,
706
+ /* @__PURE__ */ jsx(
707
+ MenuItem,
708
+ {
709
+ icon: "trash-2",
710
+ label: "\uC0AD\uC81C",
711
+ danger: true,
712
+ onSelect: () => {
713
+ setMenuFor(null);
714
+ handleDelete(position);
715
+ }
716
+ }
717
+ )
718
+ ] }) : null
719
+ ]
720
+ }
721
+ ) : null
722
+ ] }) : null
723
+ ]
724
+ },
725
+ position.id
726
+ )),
727
+ canCreate ? /* @__PURE__ */ jsx(
728
+ Button,
729
+ {
730
+ type: "button",
731
+ variant: "ghost",
732
+ size: "sm",
733
+ icon: "plus",
734
+ style: { color: "var(--wp-text-muted)" },
735
+ disabled: saving,
736
+ onClick: () => {
737
+ setAddTarget(rowIndex);
738
+ setAddName("");
739
+ },
740
+ children: "\uC774 \uAE09\uC5D0 \uCD94\uAC00"
741
+ }
742
+ ) : null
743
+ ]
744
+ }
745
+ ),
746
+ canUpdate ? /* @__PURE__ */ jsxs(
747
+ "div",
748
+ {
749
+ style: {
750
+ marginLeft: "auto",
751
+ flex: "none",
752
+ display: "flex",
753
+ alignItems: "center",
754
+ gap: 2
755
+ },
756
+ children: [
757
+ /* @__PURE__ */ jsx(
758
+ IconButton,
759
+ {
760
+ icon: "chevron-up",
761
+ size: "sm",
762
+ label: "\uAE09 \uC704\uB85C \uC774\uB3D9",
763
+ disabled: saving || rowIndex === 0,
764
+ onClick: () => moveRank(rowIndex, -1)
765
+ }
766
+ ),
767
+ /* @__PURE__ */ jsx(
768
+ IconButton,
769
+ {
770
+ icon: "chevron-down",
771
+ size: "sm",
772
+ label: "\uAE09 \uC544\uB798\uB85C \uC774\uB3D9",
773
+ disabled: saving || rowIndex === ranks.length - 1,
774
+ onClick: () => moveRank(rowIndex, 1)
775
+ }
776
+ )
777
+ ]
778
+ }
779
+ ) : null
780
+ ]
781
+ }
782
+ )
783
+ ] }, rank[0]?.id ?? `rank-${rowIndex}`);
784
+ }),
785
+ gap(ranks.length),
786
+ /* @__PURE__ */ jsx(
787
+ "p",
788
+ {
789
+ className: "wp-caption",
790
+ style: { marginTop: "var(--wp-space-2)", color: "var(--wp-text-muted)" },
791
+ children: "\uC904\uC744 \uB4DC\uB798\uADF8\uD558\uAC70\uB098 \u25B2\u25BC\uB85C \uAE09 \uC11C\uC5F4\uC744 \uBC14\uAFB8\uACE0, \uC9C1\uC704 \uCE69\uC744 \uB2E4\uB978 \uC904\uC5D0 \uB4DC\uB798\uADF8\uD558\uBA74 \uADF8 \uAE09\uC73C\uB85C \uC774\uB3D9\uD569\uB2C8\uB2E4. \uC904 \uC0AC\uC774\uC5D0 \uCE69\uC744 \uB193\uC73C\uBA74 \uC0C8 \uAE09\uC73C\uB85C \uB07C\uC6CC\uB123\uC2B5\uB2C8\uB2E4."
792
+ }
793
+ )
794
+ ] }),
795
+ renameTarget ? /* @__PURE__ */ jsx(
796
+ SduiDialog,
797
+ {
798
+ title: "\uC9C1\uC704 \uC774\uB984 \uBCC0\uACBD",
799
+ subtitle: "\uC774\uB984 \uBCC0\uACBD\uC740 \uC9C1\uC704 \uB9C8\uC2A4\uD130\uC5D0\uB9CC \uC801\uC6A9\uB429\uB2C8\uB2E4. \uC0AC\uC6D0\uC5D0\uAC8C \uC774\uBBF8 \uC800\uC7A5\uB41C \uC9C1\uC704 \uBB38\uC790\uC5F4\uC740 \uBC14\uB00C\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
800
+ size: "sm",
801
+ onClose: () => {
802
+ if (!saving) setRenameTarget(null);
803
+ },
804
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
805
+ /* @__PURE__ */ jsx(
806
+ Button,
807
+ {
808
+ type: "button",
809
+ variant: "secondary",
810
+ disabled: saving,
811
+ onClick: () => setRenameTarget(null),
812
+ children: "\uCDE8\uC18C"
813
+ }
814
+ ),
815
+ /* @__PURE__ */ jsx(
816
+ Button,
817
+ {
818
+ type: "button",
819
+ variant: "primary",
820
+ disabled: saving || !renameValue.trim(),
821
+ onClick: () => void handleRename(),
822
+ children: "\uC800\uC7A5"
823
+ }
824
+ )
825
+ ] }),
826
+ children: /* @__PURE__ */ jsx(
827
+ "form",
828
+ {
829
+ onSubmit: (event) => {
830
+ event.preventDefault();
831
+ void handleRename();
832
+ },
833
+ children: /* @__PURE__ */ jsx(
834
+ Input,
835
+ {
836
+ value: renameValue,
837
+ disabled: saving,
838
+ "aria-label": "\uC9C1\uC704 \uC774\uB984",
839
+ autoFocus: true,
840
+ onChange: (event) => setRenameValue(event.target.value)
841
+ }
842
+ )
843
+ }
844
+ )
845
+ }
846
+ ) : null,
847
+ addTarget !== null ? /* @__PURE__ */ jsx(
848
+ SduiDialog,
849
+ {
850
+ title: `\uC11C\uC5F4 ${addTarget + 1}\uAE09\uC5D0 \uC9C1\uC704 \uCD94\uAC00`,
851
+ subtitle: ranks[addTarget] ? `${ranks[addTarget].map((p) => p.name).join("\xB7")}\uC640(\uACFC) \uAC19\uC740 \uAE09\uC73C\uB85C \uB4F1\uB85D\uB429\uB2C8\uB2E4.` : "",
852
+ size: "sm",
853
+ onClose: () => {
854
+ if (!saving) setAddTarget(null);
855
+ },
856
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
857
+ /* @__PURE__ */ jsx(
858
+ Button,
859
+ {
860
+ type: "button",
861
+ variant: "secondary",
862
+ disabled: saving,
863
+ onClick: () => setAddTarget(null),
864
+ children: "\uCDE8\uC18C"
865
+ }
866
+ ),
867
+ /* @__PURE__ */ jsx(
868
+ Button,
869
+ {
870
+ type: "button",
871
+ variant: "primary",
872
+ disabled: saving || !addName.trim(),
873
+ onClick: () => void handleAddToRank(),
874
+ children: "\uCD94\uAC00"
875
+ }
876
+ )
877
+ ] }),
878
+ children: /* @__PURE__ */ jsx(
879
+ "form",
880
+ {
881
+ onSubmit: (event) => {
882
+ event.preventDefault();
883
+ void handleAddToRank();
884
+ },
885
+ children: /* @__PURE__ */ jsx(
886
+ Input,
887
+ {
888
+ value: addName,
889
+ disabled: saving,
890
+ placeholder: "\uC9C1\uC704 \uC774\uB984",
891
+ "aria-label": "\uC9C1\uC704 \uC774\uB984",
892
+ autoFocus: true,
893
+ onChange: (event) => setAddName(event.target.value)
894
+ }
895
+ )
896
+ }
897
+ )
898
+ }
899
+ ) : null
900
+ ] });
901
+ }
902
+ var RankBoard = boxed(RankBoardImpl, "RankBoard");
903
+
904
+ export { RankBoard };