@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
- ## [Unreleased]
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
- /* Readonly mode */
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
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/data-components",
3
3
  "description": "iyulab data visualization components",
4
- "version": "0.1.6",
4
+ "version": "0.1.8",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-components",