@iyulab/data-components 0.1.6 → 0.1.8
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.
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.1.8] - 2026-03-08
|
|
4
|
+
### Added
|
|
5
|
+
- USimpleSheet: `format` 속성 — Intl.NumberFormatOptions(통화, 퍼센트 등) 또는 콜백 함수로 표시 포맷 지정
|
|
6
|
+
- USimpleSheet: 셀 값 기반 자동 텍스트 정렬 (숫자 우측, 문자 좌측)
|
|
7
|
+
|
|
8
|
+
## [0.1.7] - 2026-03-08
|
|
9
|
+
### Added
|
|
10
|
+
- USimpleSheet: `compute` 콜백을 통한 열 단위 자동 계산 기능
|
|
11
|
+
- 가로(같은 행) 및 세로(행 간) 계산 모두 지원
|
|
12
|
+
- compute 열 자동 readonly 및 시각적 구분 (이탤릭 + 파란 배경)
|
|
13
|
+
- 에러 발생 시 빈 문자열 표시
|
|
14
|
+
- 붙여넣기/Fill 시 compute 열 건너뛰기
|
|
15
|
+
|
|
16
|
+
## [0.1.6]
|
|
4
17
|
- Initial library version release
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**[Live Demo](https://iyulab.github.io/node-data-components/)**
|
|
6
6
|
|
|
7
|
-
- **USimpleSheet** — 엑셀 호환 스프레드시트 입력 컴포넌트 (Lit)
|
|
7
|
+
- **USimpleSheet** — 엑셀 호환 스프레드시트 입력 컴포넌트 (Lit, compute 자동 계산 지원)
|
|
8
8
|
- **UDataView** — Grid / List / Table 뷰 전환 컴포넌트 (Lit)
|
|
9
9
|
- **UDataGrid** — OData 기반 서버 사이드 데이터 그리드 (React + DevExtreme)
|
|
10
10
|
|
|
@@ -44,6 +44,25 @@ npm install @iyulab/data-components
|
|
|
44
44
|
></u-simple-sheet>
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
자동 계산 열 (compute):
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<u-simple-sheet
|
|
51
|
+
.columns=${[
|
|
52
|
+
{ key: 'item', label: '품목', width: 150 },
|
|
53
|
+
{ key: 'qty', label: '수량', width: 80 },
|
|
54
|
+
{ key: 'price', label: '단가', width: 100 },
|
|
55
|
+
{ key: 'total', label: '합계', width: 100,
|
|
56
|
+
compute: (r, data) => {
|
|
57
|
+
const qty = Number(data[r][1]) || 0;
|
|
58
|
+
const price = Number(data[r][2]) || 0;
|
|
59
|
+
return String(qty * price);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
]}
|
|
63
|
+
></u-simple-sheet>
|
|
64
|
+
```
|
|
65
|
+
|
|
47
66
|
### UDataView
|
|
48
67
|
|
|
49
68
|
```html
|
|
@@ -13,6 +13,10 @@ export interface SheetColumn {
|
|
|
13
13
|
options?: string[] | ((row: number, col: number) => string[]);
|
|
14
14
|
/** true이면 목록에 있는 값만 입력 가능 (기본: false = 자유 입력 허용) */
|
|
15
15
|
strict?: boolean;
|
|
16
|
+
/** 자동 계산 함수. 설정 시 해당 열은 자동 readonly. 열 순서(좌→우), 행 순서(위→아래)로 계산. */
|
|
17
|
+
compute?: (rowIndex: number, data: string[][]) => string;
|
|
18
|
+
/** 표시 포맷. Intl.NumberFormatOptions(숫자) 또는 커스텀 콜백. 원본 데이터는 유지. */
|
|
19
|
+
format?: Intl.NumberFormatOptions | ((value: string, rowIndex: number) => string);
|
|
16
20
|
}
|
|
17
21
|
/**
|
|
18
22
|
* USimpleSheet - 심플 스프레드시트 컴포넌트
|
|
@@ -120,6 +124,12 @@ export declare class USimpleSheet extends UElement {
|
|
|
120
124
|
private _scrollCellIntoView;
|
|
121
125
|
private _getCellFromEvent;
|
|
122
126
|
private _isColReadonly;
|
|
127
|
+
private _isColComputed;
|
|
128
|
+
private _isNumeric;
|
|
129
|
+
/** 열의 format 설정에 따라 표시값을 반환. format 미설정 시 원본 반환. */
|
|
130
|
+
private _formatValue;
|
|
131
|
+
/** compute 열을 재계산 (열 순서 좌→우, 행 순서 위→아래) */
|
|
132
|
+
private _recompute;
|
|
123
133
|
private _getColOptions;
|
|
124
134
|
private _filterOptions;
|
|
125
135
|
private _emitChange;
|
|
@@ -477,6 +477,7 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
477
477
|
newData.push(row);
|
|
478
478
|
}
|
|
479
479
|
this._data = newData;
|
|
480
|
+
this._recompute();
|
|
480
481
|
this._history = [this._data.map((r) => [...r])];
|
|
481
482
|
this._historyIndex = 0;
|
|
482
483
|
const prevWidths = this._colWidths;
|
|
@@ -510,6 +511,7 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
510
511
|
if (this._historyIndex <= 0) return;
|
|
511
512
|
this._historyIndex--;
|
|
512
513
|
this._data = this._history[this._historyIndex].map((r) => [...r]);
|
|
514
|
+
this._recompute();
|
|
513
515
|
this._emitChange();
|
|
514
516
|
this.requestUpdate();
|
|
515
517
|
}
|
|
@@ -517,6 +519,7 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
517
519
|
if (this._historyIndex >= this._history.length - 1) return;
|
|
518
520
|
this._historyIndex++;
|
|
519
521
|
this._data = this._history[this._historyIndex].map((r) => [...r]);
|
|
522
|
+
this._recompute();
|
|
520
523
|
this._emitChange();
|
|
521
524
|
this.requestUpdate();
|
|
522
525
|
}
|
|
@@ -619,11 +622,18 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
619
622
|
const isSelected = this._inSel(r, c);
|
|
620
623
|
const isAnchor = this._isAnchor(r, c);
|
|
621
624
|
const value = this._data[r]?.[c] ?? "";
|
|
625
|
+
const isColReadonly = this._isColReadonly(c);
|
|
626
|
+
const isComputed = this._isColComputed(c);
|
|
627
|
+
const hasIntlFormat = this.columns?.[c]?.format && typeof this.columns[c].format !== "function";
|
|
628
|
+
const isNumeric = hasIntlFormat || this._isNumeric(value);
|
|
622
629
|
const classes = [
|
|
623
630
|
"cell",
|
|
624
631
|
isSelected ? "selected" : "",
|
|
625
632
|
isAnchor ? "anchor" : "",
|
|
626
|
-
isEditing ? "editing" : ""
|
|
633
|
+
isEditing ? "editing" : "",
|
|
634
|
+
isColReadonly ? "cell-readonly" : "",
|
|
635
|
+
isComputed ? "cell-computed" : "",
|
|
636
|
+
isNumeric ? "cell-numeric" : ""
|
|
627
637
|
].filter(Boolean).join(" ");
|
|
628
638
|
const hasOptions = isEditing && this._getColOptions(r, c) !== null;
|
|
629
639
|
const showDropdown = isEditing && this._dropdownItems.length > 0;
|
|
@@ -657,7 +667,7 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
657
667
|
<div class="dropdown-empty">일치하는 항목 없음</div>
|
|
658
668
|
</div>
|
|
659
669
|
` : ""}
|
|
660
|
-
` : value}
|
|
670
|
+
` : this._formatValue(value, c, r)}
|
|
661
671
|
</td>
|
|
662
672
|
`;
|
|
663
673
|
}
|
|
@@ -694,12 +704,13 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
694
704
|
for (let ci = 0; ci < pasteRows[ri].length; ci++) {
|
|
695
705
|
const r = anchor.row + ri;
|
|
696
706
|
const c = anchor.col + ci;
|
|
697
|
-
if (r < newData.length && c < (newData[r]?.length ?? 0)) {
|
|
707
|
+
if (r < newData.length && c < (newData[r]?.length ?? 0) && !this._isColComputed(c)) {
|
|
698
708
|
newData[r][c] = pasteRows[ri][ci];
|
|
699
709
|
}
|
|
700
710
|
}
|
|
701
711
|
}
|
|
702
712
|
this._data = newData;
|
|
713
|
+
this._recompute();
|
|
703
714
|
this._pushHistory();
|
|
704
715
|
this._emitChange();
|
|
705
716
|
this.requestUpdate();
|
|
@@ -722,6 +733,7 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
722
733
|
}
|
|
723
734
|
}
|
|
724
735
|
this._data = newData;
|
|
736
|
+
this._recompute();
|
|
725
737
|
this._pushHistory();
|
|
726
738
|
this._emitChange();
|
|
727
739
|
this.requestUpdate();
|
|
@@ -741,6 +753,7 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
741
753
|
}
|
|
742
754
|
}
|
|
743
755
|
this._data = newData;
|
|
756
|
+
this._recompute();
|
|
744
757
|
this._pushHistory();
|
|
745
758
|
this._emitChange();
|
|
746
759
|
this.requestUpdate();
|
|
@@ -802,6 +815,7 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
802
815
|
this._dropdownItems = [];
|
|
803
816
|
this._dropdownIndex = -1;
|
|
804
817
|
if (this._editVal !== prevVal) {
|
|
818
|
+
this._recompute();
|
|
805
819
|
this._pushHistory();
|
|
806
820
|
this._emitChange();
|
|
807
821
|
}
|
|
@@ -825,6 +839,7 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
825
839
|
}
|
|
826
840
|
}
|
|
827
841
|
this._data = newData;
|
|
842
|
+
this._recompute();
|
|
828
843
|
this._pushHistory();
|
|
829
844
|
this._emitChange();
|
|
830
845
|
this.requestUpdate();
|
|
@@ -854,7 +869,44 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
854
869
|
return { row, col };
|
|
855
870
|
}
|
|
856
871
|
_isColReadonly(col) {
|
|
857
|
-
return this.columns?.[col]?.readonly ?? false;
|
|
872
|
+
return (this.columns?.[col]?.readonly ?? false) || this._isColComputed(col);
|
|
873
|
+
}
|
|
874
|
+
_isColComputed(col) {
|
|
875
|
+
return typeof this.columns?.[col]?.compute === "function";
|
|
876
|
+
}
|
|
877
|
+
_isNumeric(value) {
|
|
878
|
+
if (!value || !value.trim()) return false;
|
|
879
|
+
return !isNaN(Number(value.replace(/,/g, "")));
|
|
880
|
+
}
|
|
881
|
+
/** 열의 format 설정에 따라 표시값을 반환. format 미설정 시 원본 반환. */
|
|
882
|
+
_formatValue(value, col, row) {
|
|
883
|
+
const fmt = this.columns?.[col]?.format;
|
|
884
|
+
if (!fmt || !value) return value;
|
|
885
|
+
try {
|
|
886
|
+
if (typeof fmt === "function") {
|
|
887
|
+
return fmt(value, row);
|
|
888
|
+
}
|
|
889
|
+
const num = Number(value.replace(/,/g, ""));
|
|
890
|
+
if (isNaN(num)) return value;
|
|
891
|
+
return new Intl.NumberFormat("ko-KR", fmt).format(num);
|
|
892
|
+
} catch {
|
|
893
|
+
return value;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/** compute 열을 재계산 (열 순서 좌→우, 행 순서 위→아래) */
|
|
897
|
+
_recompute() {
|
|
898
|
+
if (!this.columns?.some((c) => c.compute)) return;
|
|
899
|
+
for (let c = 0; c < this._colCount; c++) {
|
|
900
|
+
const fn = this.columns?.[c]?.compute;
|
|
901
|
+
if (!fn) continue;
|
|
902
|
+
for (let r = 0; r < this._rowCount; r++) {
|
|
903
|
+
try {
|
|
904
|
+
this._data[r][c] = fn(r, this._data);
|
|
905
|
+
} catch {
|
|
906
|
+
this._data[r][c] = "";
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
858
910
|
}
|
|
859
911
|
_getColOptions(row, col) {
|
|
860
912
|
const colDef = this.columns?.[col];
|
|
@@ -904,6 +956,7 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
904
956
|
const newData = this._data.map((r) => [...r]);
|
|
905
957
|
newData[row][col] = value;
|
|
906
958
|
this._data = newData;
|
|
959
|
+
this._recompute();
|
|
907
960
|
this._pushHistory();
|
|
908
961
|
this._emitChange();
|
|
909
962
|
this.requestUpdate();
|
|
@@ -240,7 +240,43 @@ const styles = css`
|
|
|
240
240
|
font-style: italic;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
/*
|
|
243
|
+
/* Numeric cell (right-aligned like Excel) */
|
|
244
|
+
.cell.cell-numeric {
|
|
245
|
+
text-align: right;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* Readonly column/cell */
|
|
249
|
+
.cell.cell-readonly {
|
|
250
|
+
background: var(--u-neutral-50, #f8fafc);
|
|
251
|
+
color: var(--u-txt-color-weak, #64748b);
|
|
252
|
+
cursor: default;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.cell.cell-readonly.selected {
|
|
256
|
+
background: var(--u-neutral-100, #f1f5f9);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.cell.cell-readonly.anchor {
|
|
260
|
+
background: var(--u-neutral-50, #f8fafc);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* Computed column/cell */
|
|
264
|
+
.cell.cell-computed {
|
|
265
|
+
background: var(--u-blue-50, #eff6ff);
|
|
266
|
+
color: var(--u-txt-color, #0f172a);
|
|
267
|
+
font-style: italic;
|
|
268
|
+
cursor: default;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.cell.cell-computed.selected {
|
|
272
|
+
background: var(--u-blue-100, #dbeafe);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.cell.cell-computed.anchor {
|
|
276
|
+
background: var(--u-blue-50, #eff6ff);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* Readonly mode (whole sheet) */
|
|
244
280
|
:host([readonly]) .cell {
|
|
245
281
|
cursor: default;
|
|
246
282
|
}
|
|
@@ -297,6 +333,32 @@ const styles = css`
|
|
|
297
333
|
border-bottom-color: var(--u-border-color-weak, #2A2A2A);
|
|
298
334
|
}
|
|
299
335
|
|
|
336
|
+
:host-context([theme="dark"]) .cell.cell-readonly {
|
|
337
|
+
background: var(--u-neutral-100, #1E1E1E);
|
|
338
|
+
color: var(--u-txt-color-weak, #8A8A8A);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
:host-context([theme="dark"]) .cell.cell-readonly.selected {
|
|
342
|
+
background: var(--u-neutral-200, #2A2A2A);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
:host-context([theme="dark"]) .cell.cell-readonly.anchor {
|
|
346
|
+
background: var(--u-neutral-100, #1E1E1E);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
:host-context([theme="dark"]) .cell.cell-computed {
|
|
350
|
+
background: var(--u-blue-100, #1e3a5f);
|
|
351
|
+
color: var(--u-txt-color, #D4D4D4);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
:host-context([theme="dark"]) .cell.cell-computed.selected {
|
|
355
|
+
background: var(--u-blue-200, #2a4a6f);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
:host-context([theme="dark"]) .cell.cell-computed.anchor {
|
|
359
|
+
background: var(--u-blue-100, #1e3a5f);
|
|
360
|
+
}
|
|
361
|
+
|
|
300
362
|
:host-context([theme="dark"]) .cell.selected {
|
|
301
363
|
background: var(--u-blue-0, #0a1e3d);
|
|
302
364
|
}
|