@simplysm/angular 14.0.86 → 14.0.88

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.
Files changed (109) hide show
  1. package/dist/controls/pagination/sd-pagination.d.ts.map +1 -1
  2. package/dist/controls/pagination/sd-pagination.js +2 -0
  3. package/dist/controls/select/sd-select-item.js +1 -1
  4. package/dist/controls/select/sd-select.js +1 -1
  5. package/dist/core/app-structure/sd-app-structure.provider.d.ts +1 -0
  6. package/dist/core/app-structure/sd-app-structure.provider.d.ts.map +1 -1
  7. package/dist/core/app-structure/sd-app-structure.provider.js +3 -0
  8. package/dist/core/app-structure/sd-app-structure.utils.d.ts +2 -0
  9. package/dist/core/app-structure/sd-app-structure.utils.d.ts.map +1 -1
  10. package/dist/core/app-structure/sd-app-structure.utils.js +9 -1
  11. package/dist/core/busy/sd-busy-container.d.ts +1 -1
  12. package/dist/core/busy/sd-busy-container.d.ts.map +1 -1
  13. package/dist/core/busy/sd-busy-container.js +11 -11
  14. package/dist/core/busy/sd-busy.provider.d.ts.map +1 -1
  15. package/dist/core/busy/sd-busy.provider.js +3 -0
  16. package/dist/core/config/sd-system-config.provider.d.ts +4 -4
  17. package/dist/core/config/sd-system-config.provider.d.ts.map +1 -1
  18. package/dist/core/config/sd-system-config.provider.js +3 -0
  19. package/dist/core/error-handler/sd-global-error-handler.plugin.d.ts +1 -1
  20. package/dist/core/error-handler/sd-global-error-handler.plugin.d.ts.map +1 -1
  21. package/dist/core/file-dialog/sd-file-dialog.provider.d.ts.map +1 -1
  22. package/dist/core/file-dialog/sd-file-dialog.provider.js +4 -0
  23. package/dist/core/modal/sd-activated-modal.provider.d.ts +2 -1
  24. package/dist/core/modal/sd-activated-modal.provider.d.ts.map +1 -1
  25. package/dist/core/modal/sd-modal.d.ts.map +1 -1
  26. package/dist/core/modal/sd-modal.js +3 -3
  27. package/dist/core/modal/sd-modal.provider.d.ts.map +1 -1
  28. package/dist/core/modal/sd-modal.provider.js +8 -0
  29. package/dist/core/modal/sd-prompt-modal.d.ts +2 -2
  30. package/dist/core/modal/sd-prompt-modal.d.ts.map +1 -1
  31. package/dist/core/modal/sd-prompt-modal.js +32 -37
  32. package/dist/core/print/sd-print.provider.d.ts.map +1 -1
  33. package/dist/core/print/sd-print.provider.js +19 -6
  34. package/dist/core/provideSdAngular.d.ts.map +1 -1
  35. package/dist/core/provideSdAngular.js +4 -1
  36. package/dist/core/routing/injectViewTitleSignal.d.ts.map +1 -1
  37. package/dist/core/routing/injectViewTitleSignal.js +1 -6
  38. package/dist/core/shared-data/sd-shared-data.provider.d.ts +2 -1
  39. package/dist/core/shared-data/sd-shared-data.provider.d.ts.map +1 -1
  40. package/dist/core/shared-data/sd-shared-data.provider.js +1 -1
  41. package/dist/core/toast/sd-toast.provider.d.ts +1 -0
  42. package/dist/core/toast/sd-toast.provider.d.ts.map +1 -1
  43. package/dist/core/toast/sd-toast.provider.js +18 -4
  44. package/dist/data/kanban/sd-kanban-lane.d.ts.map +1 -1
  45. package/dist/data/shared-data/sd-shared-data-select-button.d.ts +1 -2
  46. package/dist/data/shared-data/sd-shared-data-select-button.d.ts.map +1 -1
  47. package/dist/data/shared-data/sd-shared-data-select-button.js +18 -21
  48. package/dist/data/shared-data/sd-shared-data-select.d.ts +3 -2
  49. package/dist/data/shared-data/sd-shared-data-select.d.ts.map +1 -1
  50. package/dist/data/shared-data/sd-shared-data-select.js +10 -10
  51. package/dist/data/sheet/sd-sheet-config.modal.d.ts +3 -3
  52. package/dist/data/sheet/sd-sheet-config.modal.d.ts.map +1 -1
  53. package/dist/data/sheet/sd-sheet-config.modal.js +37 -34
  54. package/dist/data/sheet/sd-sheet.d.ts.map +1 -1
  55. package/dist/data/sheet/sd-sheet.js +5 -1
  56. package/dist/data/sheet/useSheetCellStyling.d.ts.map +1 -1
  57. package/dist/data/sheet/useSheetCellStyling.js +4 -22
  58. package/dist/data/state-preset/sd-state-preset.d.ts +10 -10
  59. package/dist/data/state-preset/sd-state-preset.d.ts.map +1 -1
  60. package/dist/data/state-preset/sd-state-preset.js +4 -4
  61. package/dist/features/address/sd-address-search.modal.js +1 -1
  62. package/dist/features/editor/sd-tiptap-editor.d.ts.map +1 -1
  63. package/dist/features/editor/sd-tiptap-editor.js +3 -2
  64. package/dist/features/theme/sd-theme-selector.d.ts +6 -8
  65. package/dist/features/theme/sd-theme-selector.d.ts.map +1 -1
  66. package/dist/features/theme/sd-theme-selector.js +54 -51
  67. package/dist/features/visual/sd-barcode.d.ts.map +1 -1
  68. package/dist/features/visual/sd-barcode.js +5 -10
  69. package/dist/layout/sidebar/sd-sidebar-user.d.ts.map +1 -1
  70. package/dist/layout/sidebar/sd-sidebar-user.js +26 -36
  71. package/package.json +5 -5
  72. package/src/controls/button/sd-modal-select-button.ts +1 -1
  73. package/src/controls/form/sd-form.ts +1 -1
  74. package/src/controls/input/sd-textfield-type-handlers.ts +2 -2
  75. package/src/controls/pagination/sd-pagination.ts +1 -0
  76. package/src/controls/select/sd-select-item.ts +1 -1
  77. package/src/controls/select/sd-select.ts +1 -1
  78. package/src/core/app-structure/sd-app-structure.provider.ts +4 -0
  79. package/src/core/app-structure/sd-app-structure.utils.ts +14 -2
  80. package/src/core/busy/sd-busy-container.ts +6 -13
  81. package/src/core/busy/sd-busy.provider.ts +4 -0
  82. package/src/core/config/injectSdSystemConfigResource.ts +1 -1
  83. package/src/core/config/sd-system-config.provider.ts +5 -3
  84. package/src/core/error-handler/sd-global-error-handler.plugin.ts +1 -1
  85. package/src/core/file-dialog/sd-file-dialog.provider.ts +5 -1
  86. package/src/core/modal/sd-activated-modal.provider.ts +2 -1
  87. package/src/core/modal/sd-modal.provider.ts +8 -0
  88. package/src/core/modal/sd-modal.ts +6 -3
  89. package/src/core/modal/sd-prompt-modal.ts +13 -23
  90. package/src/core/print/sd-print.provider.ts +21 -6
  91. package/src/core/provideSdAngular.ts +5 -1
  92. package/src/core/routing/injectViewTitleSignal.ts +1 -5
  93. package/src/core/shared-data/sd-shared-data.provider.ts +2 -2
  94. package/src/core/toast/sd-toast.provider.ts +35 -12
  95. package/src/data/kanban/sd-kanban-lane.ts +2 -2
  96. package/src/data/shared-data/sd-shared-data-select-button.ts +15 -20
  97. package/src/data/shared-data/sd-shared-data-select-list.ts +4 -4
  98. package/src/data/shared-data/sd-shared-data-select.ts +10 -12
  99. package/src/data/sheet/sd-sheet-column.ts +1 -1
  100. package/src/data/sheet/sd-sheet-config.modal.ts +39 -35
  101. package/src/data/sheet/sd-sheet.ts +6 -1
  102. package/src/data/sheet/useSheetCellStyling.ts +3 -20
  103. package/src/data/state-preset/sd-state-preset.ts +11 -11
  104. package/src/features/address/sd-address-search.modal.ts +1 -1
  105. package/src/features/editor/sd-tiptap-editor.ts +3 -2
  106. package/src/features/theme/sd-theme-selector.ts +77 -75
  107. package/src/features/visual/sd-barcode.ts +5 -9
  108. package/src/features/visual/sd-calendar.ts +1 -1
  109. package/src/layout/sidebar/sd-sidebar-user.ts +3 -5
@@ -9,10 +9,22 @@ import type { SdMenu, SdFlatMenu, SdPermission } from "./sd-app-structure.types"
9
9
  export abstract class SdAppStructureUtils {
10
10
  //---------- Info
11
11
 
12
- static getTitleByFullCode<TModule>(items: AppStructureItem<TModule>[], fullCode: string) {
12
+ static getTitleByFullCode<TModule>(items: AppStructureItem<TModule>[], fullCode: string): string {
13
+ const title = this.findTitleByFullCode(items, fullCode);
14
+ if (title == null) {
15
+ throw new Error(`Item not found for fullCode: ${fullCode}`);
16
+ }
17
+ return title;
18
+ }
19
+
20
+ /** 항목을 못 찾으면 throw 대신 undefined 반환 (결측 보존). */
21
+ static findTitleByFullCode<TModule>(
22
+ items: AppStructureItem<TModule>[],
23
+ fullCode: string,
24
+ ): string | undefined {
13
25
  const itemChain = this.getItemChainByFullCode(items, fullCode);
14
26
  if (itemChain.length === 0) {
15
- throw new Error(`Item not found for fullCode: ${fullCode}`);
27
+ return undefined;
16
28
  }
17
29
  const parent = itemChain
18
30
  .slice(0, -1)
@@ -3,7 +3,6 @@ import {
3
3
  ChangeDetectionStrategy,
4
4
  Component,
5
5
  computed,
6
- ElementRef,
7
6
  inject,
8
7
  input,
9
8
  ViewEncapsulation,
@@ -18,6 +17,7 @@ import { SdBusyProvider, type SdBusyType } from "./sd-busy.provider";
18
17
  host: {
19
18
  "[attr.data-sd-busy]": "busy() || undefined",
20
19
  "[attr.data-sd-type]": "currType()",
20
+ "(keydown.capture)": "onKeydownCapture($event)",
21
21
  },
22
22
  template: `
23
23
  <div class="_screen">
@@ -339,17 +339,10 @@ export class SdBusyContainer {
339
339
 
340
340
  currType = computed(() => this.type() ?? this._sdBusy.type());
341
341
 
342
- constructor() {
343
- const el = inject(ElementRef).nativeElement as HTMLElement;
344
- el.addEventListener(
345
- "keydown",
346
- (event) => {
347
- if (this.busy()) {
348
- event.preventDefault();
349
- event.stopPropagation();
350
- }
351
- },
352
- true,
353
- );
342
+ onKeydownCapture(event: Event): void {
343
+ if (this.busy()) {
344
+ event.preventDefault();
345
+ event.stopPropagation();
346
+ }
354
347
  }
355
348
  }
@@ -44,6 +44,10 @@ export class SdBusyProvider {
44
44
  constructor() {
45
45
  effect(() => {
46
46
  const busy = this.globalBusyCount() > 0;
47
+
48
+ // busy 가 처음 true 가 되기 전엔 컨테이너를 생성하지 않음 (lazy)
49
+ if (!busy && this._containerRef == null) return;
50
+
47
51
  const ref = this.containerRef;
48
52
  const el = ref.location.nativeElement as HTMLElement;
49
53
 
@@ -20,7 +20,7 @@ export function injectSdSystemConfigResource<T>(options: { key: Signal<string |
20
20
  const key = options.key();
21
21
  if (key == null) return;
22
22
  queueMicrotask(() => {
23
- sdSystemConfig.setAsync(`${elTag}.${key}`, value as T).catch((err) => {
23
+ sdSystemConfig.setAsync(`${elTag}.${key}`, value).catch((err) => {
24
24
  errorHandler.handleError(err);
25
25
  });
26
26
  });
@@ -6,13 +6,15 @@ export class SdSystemConfigProvider<T> {
6
6
  private readonly _sdLocalStorage = inject<SdLocalStorageProvider<T>>(SdLocalStorageProvider);
7
7
 
8
8
  fn?: {
9
- set<K extends keyof T & string>(key: K, data: T[K]): Promise<void> | void;
10
- get(key: keyof T & string): PromiseLike<any>;
9
+ set<K extends keyof T & string>(key: K, data: T[K] | undefined): Promise<void> | void;
10
+ get(key: keyof T & string): PromiseLike<unknown>;
11
11
  };
12
12
 
13
- async setAsync<K extends keyof T & string>(key: K, data: T[K]) {
13
+ async setAsync<K extends keyof T & string>(key: K, data: T[K] | undefined) {
14
14
  if (this.fn) {
15
15
  await this.fn.set(key, data);
16
+ } else if (data == null) {
17
+ this._sdLocalStorage.remove(key);
16
18
  } else {
17
19
  this._sdLocalStorage.set(key, data);
18
20
  }
@@ -17,7 +17,7 @@ export class SdGlobalErrorHandlerPlugin implements ErrorHandler {
17
17
  private readonly _sdSystemLog = inject(SdSystemLogProvider);
18
18
  private _hasDisplayedError = false;
19
19
 
20
- handleError(event: any) {
20
+ handleError(event: unknown) {
21
21
  try {
22
22
  if (event instanceof PromiseRejectionEvent) {
23
23
  const reason = event.reason;
@@ -23,7 +23,11 @@ export class SdFileDialogProvider {
23
23
  }
24
24
  inputEl.onchange = (event: Event): void => {
25
25
  cleanup();
26
- const files = (event.target as HTMLInputElement).files!;
26
+ const files = (event.target as HTMLInputElement).files;
27
+ if (files == null) {
28
+ resolve(undefined);
29
+ return;
30
+ }
27
31
  resolve(multiple ? Array.from(files) : files[0]);
28
32
  };
29
33
 
@@ -1,12 +1,13 @@
1
1
  import { Injectable, signal } from "@angular/core";
2
2
  import type { SdModalContentDef } from "./sd-modal.provider";
3
+ import type { SdModal } from "./sd-modal";
3
4
 
4
5
  /**
5
6
  * 모달 내부에서 inject하여 사용하는 프로바이더
6
7
  */
7
8
  @Injectable()
8
9
  export class SdActivatedModalProvider<T extends SdModalContentDef<any> = SdModalContentDef<any>> {
9
- modalComponent = signal<any>(undefined);
10
+ modalComponent = signal<SdModal | undefined>(undefined);
10
11
  contentComponent = signal<T | undefined>(undefined);
11
12
  canDeactivateFn: () => boolean = () => true;
12
13
  }
@@ -195,7 +195,12 @@ export class SdModalProvider {
195
195
 
196
196
  const modalEl = modalRef.location.nativeElement as HTMLElement;
197
197
 
198
+ let destroyed = false;
198
199
  const doDestroy = () => {
200
+ // transitionend 와 setTimeout 폴백 중 먼저 도달한 쪽에서 1회만 실행
201
+ if (destroyed) return;
202
+ destroyed = true;
203
+
199
204
  // DOM에서 제거
200
205
  if (modalEl.parentNode != null) {
201
206
  modalEl.parentNode.removeChild(modalEl);
@@ -221,7 +226,10 @@ export class SdModalProvider {
221
226
  // transition duration 확인 후 대기 또는 즉시 destroy
222
227
  const duration = parseFloat(getComputedStyle(modalEl).transitionDuration || "0");
223
228
  if (duration > 0) {
229
+ // transitionend 가 정상 경로. 단 백그라운드 탭·transition 인터럽트 시 미발화하므로
230
+ // duration(초→ms) + 여유 100ms 후 발화하는 setTimeout 폴백을 함께 걸어 destroy 누락을 방지.
224
231
  modalEl.addEventListener("transitionend", doDestroy, { once: true });
232
+ setTimeout(doDestroy, duration * 1000 + 100);
225
233
  } else {
226
234
  doDestroy();
227
235
  }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ afterNextRender,
2
3
  ChangeDetectionStrategy,
3
4
  Component,
4
5
  effect,
@@ -323,7 +324,9 @@ import "@simplysm/core-browser";
323
324
  export class SdModal {
324
325
  private readonly _elRef = inject(ElementRef<HTMLElement>);
325
326
  private readonly _sdActivatedModal = inject(SdActivatedModalProvider, { optional: true });
326
- private readonly _sdSystemConfig = inject(SdSystemConfigProvider, { optional: true });
327
+ private readonly _sdSystemConfig = inject<
328
+ SdSystemConfigProvider<Record<string, Record<string, string>>>
329
+ >(SdSystemConfigProvider, { optional: true });
327
330
  private readonly _errorHandler = inject(ErrorHandler);
328
331
  private readonly _focusTrap = injectFocusTrap();
329
332
 
@@ -359,7 +362,7 @@ export class SdModal {
359
362
 
360
363
  constructor() {
361
364
  // data-sd-init: 첫 렌더 후 설정하여 CSS transition 트리거 허용
362
- effect(() => {
365
+ afterNextRender(() => {
363
366
  this._elRef.nativeElement.setAttribute("data-sd-init", "");
364
367
  });
365
368
 
@@ -508,7 +511,7 @@ export class SdModal {
508
511
  if (dialogEl.style.left !== "") config["left"] = dialogEl.style.left;
509
512
  if (dialogEl.style.top !== "") config["top"] = dialogEl.style.top;
510
513
 
511
- await this._sdSystemConfig.setAsync(`sd-modal.${k}` as any, config as any);
514
+ await this._sdSystemConfig.setAsync(`sd-modal.${k}`, config);
512
515
  }
513
516
 
514
517
  private async _restoreConfig(k: string): Promise<void> {
@@ -9,35 +9,33 @@ import {
9
9
  import type { SdModalContentDef } from "./sd-modal.provider";
10
10
  import { SdButton } from "../../controls/button/sd-button";
11
11
  import { SdTextfield } from "../../controls/input/sd-textfield";
12
+ import { SdForm } from "../../controls/form/sd-form";
12
13
 
13
14
  /**
14
15
  * 범용 프롬프트 모달.
15
16
  * SdModalContentDef<string> 구현. message를 표시하고, 텍스트 입력 후 확인/취소.
16
17
  * 확인 시 입력값 emit, 취소 시 undefined emit.
18
+ * 입력값은 필수(required)이며, 빈 값으로 확인 시 sd-form 네이티브 검증으로 차단된다.
17
19
  */
18
20
  @Component({
19
21
  selector: "sd-prompt-modal",
20
22
  changeDetection: ChangeDetectionStrategy.OnPush,
21
23
  encapsulation: ViewEncapsulation.None,
22
24
  standalone: true,
23
- imports: [SdButton, SdTextfield],
25
+ imports: [SdForm, SdButton, SdTextfield],
24
26
  host: {
25
27
  style: "display: block",
26
28
  class: "p-default",
27
29
  },
28
30
  template: `
29
- <p class="mb-default" [innerHTML]="message()"></p>
30
- <sd-textfield
31
- [type]="'text'"
32
- [value]="_inputValue()"
33
- (input)="onInputChange($event)"
34
- (keydown.enter)="onConfirm()"
35
- class="mb-default"
36
- />
37
- <div class="flex-row main-align-end gap-sm">
38
- <sd-button [theme]="'primary'" (click)="onConfirm()">확인</sd-button>
39
- <sd-button (click)="onCancel()">취소</sd-button>
40
- </div>
31
+ <sd-form (formSubmit)="onConfirm()">
32
+ <p class="mb-default" [innerHTML]="message()"></p>
33
+ <sd-textfield [type]="'text'" [(value)]="_inputValue" [required]="true" class="mb-default" />
34
+ <div class="flex-row main-align-end gap-sm">
35
+ <sd-button [type]="'submit'" [theme]="'primary'">확인</sd-button>
36
+ <sd-button (click)="onCancel()">취소</sd-button>
37
+ </div>
38
+ </sd-form>
41
39
  `,
42
40
  })
43
41
  export class SdPromptModal implements SdModalContentDef<string> {
@@ -46,18 +44,10 @@ export class SdPromptModal implements SdModalContentDef<string> {
46
44
 
47
45
  message = input.required<string>();
48
46
 
49
- _inputValue = signal("");
50
-
51
- onInputChange(event: Event): void {
52
- const inputEl = event.target as HTMLInputElement;
53
- this._inputValue.set(inputEl.value);
54
- }
47
+ _inputValue = signal<string | undefined>(undefined);
55
48
 
56
49
  onConfirm(): void {
57
- const val = this._inputValue();
58
- if (val !== "") {
59
- this.close.emit(val);
60
- }
50
+ this.close.emit(this._inputValue());
61
51
  }
62
52
 
63
53
  onCancel(): void {
@@ -9,10 +9,12 @@ import {
9
9
  } from "@angular/core";
10
10
  import type { DirectiveInputSignals, WithOptional } from "../directive-input-signals";
11
11
  import { SdBusyProvider } from "../busy/sd-busy.provider";
12
- import { wait } from "@simplysm/core-common";
12
+ import { createLogger, wait } from "@simplysm/core-common";
13
13
  import { jsPDF } from "jspdf";
14
14
  import * as htmlToImage from "html-to-image";
15
15
 
16
+ const logger = createLogger("angular:print");
17
+
16
18
  export interface SdPrint {
17
19
  initialized: Signal<boolean>;
18
20
  readonly _optionalPrintInputs?: string;
@@ -175,6 +177,7 @@ export class SdPrintProvider {
175
177
 
176
178
  private async _waitForAllImagesLoadedAsync(container: HTMLElement): Promise<void> {
177
179
  const imgs = Array.from(container.querySelectorAll("img"));
180
+ const failedSrcs: string[] = [];
178
181
 
179
182
  await Promise.all(
180
183
  imgs.map((img) => {
@@ -182,16 +185,28 @@ export class SdPrintProvider {
182
185
  if (img.complete && img.naturalWidth !== 0) {
183
186
  resolve();
184
187
  } else {
185
- const onDone = () => {
186
- img.removeEventListener("load", onDone);
187
- img.removeEventListener("error", onDone);
188
+ const cleanup = () => {
189
+ img.removeEventListener("load", onLoad);
190
+ img.removeEventListener("error", onError);
191
+ };
192
+ const onLoad = () => {
193
+ cleanup();
194
+ resolve();
195
+ };
196
+ const onError = () => {
197
+ failedSrcs.push(img.src);
198
+ cleanup();
188
199
  resolve();
189
200
  };
190
- img.addEventListener("load", onDone);
191
- img.addEventListener("error", onDone);
201
+ img.addEventListener("load", onLoad);
202
+ img.addEventListener("error", onError);
192
203
  }
193
204
  });
194
205
  }),
195
206
  );
207
+
208
+ if (failedSrcs.length > 0) {
209
+ logger.error(`인쇄/PDF: 이미지 ${failedSrcs.length}건 로드 실패`, failedSrcs);
210
+ }
196
211
  }
197
212
  }
@@ -28,6 +28,9 @@ import { SdThemeProvider } from "../features/theme/sd-theme-provider";
28
28
  import { SdLocalStorageProvider } from "./config/sd-local-storage.provider";
29
29
  import { SdGlobalErrorHandlerPlugin } from "./error-handler/sd-global-error-handler.plugin";
30
30
  import { SdOptionEventPlugin } from "./events/sd-option-event.plugin";
31
+ import { createLogger } from "@simplysm/core-common";
32
+
33
+ const logger = createLogger("angular:sw-update");
31
34
 
32
35
  export function provideSdAngular(opt: { clientName: string }): EnvironmentProviders {
33
36
  return makeEnvironmentProviders([
@@ -131,8 +134,9 @@ export function provideSdAngular(opt: { clientName: string }): EnvironmentProvid
131
134
  }
132
135
  }
133
136
  failCount = 0;
134
- } catch {
137
+ } catch (err) {
135
138
  failCount++;
139
+ logger.error(err);
136
140
  } finally {
137
141
  const interval = Math.min(
138
142
  BASE_INTERVAL * Math.pow(2, Math.max(0, failCount - 1)),
@@ -16,10 +16,6 @@ export function injectViewTitleSignal(): Signal<string> {
16
16
  return sdActivatedModal.modalComponent()?.title() ?? "";
17
17
  }
18
18
 
19
- try {
20
- return sdAppStructure.getTitleByFullCode(currPageCode?.() ?? fullPageCode());
21
- } catch {
22
- return "";
23
- }
19
+ return sdAppStructure.findTitleByFullCode(currPageCode?.() ?? fullPageCode()) ?? "";
24
20
  });
25
21
  }
@@ -1,6 +1,6 @@
1
1
  import { computed, ErrorHandler, inject, Injectable, signal, type Signal, type WritableSignal } from "@angular/core";
2
2
  import { defineEvent } from "@simplysm/service-common";
3
- import { obj, wait as waitUtil } from "@simplysm/core-common";
3
+ import { DateOnly, DateTime, obj, Time, wait as waitUtil } from "@simplysm/core-common";
4
4
  import { SdServiceClientFactoryProvider } from "../service-client/sd-service-client-factory.provider";
5
5
  import "@simplysm/core-common";
6
6
 
@@ -15,7 +15,7 @@ export interface SharedDataInfo<T extends SharedDataBase<string | number>> {
15
15
  serviceKey: string;
16
16
  getter: (changeKeys?: (string | number)[]) => Promise<T[]>;
17
17
  filter?: unknown;
18
- orderBy?: (item: T) => any;
18
+ orderBy?: (item: T) => string | number | DateOnly | DateTime | Time | undefined;
19
19
  }
20
20
 
21
21
  export interface SharedDataHandle<T extends SharedDataBase<string | number>> {
@@ -45,6 +45,7 @@ export class SdToastProvider {
45
45
  private _containerRef: ComponentRef<SdToastContainer> | undefined;
46
46
  private readonly _toastRefs: ComponentRef<SdToast>[] = [];
47
47
  private readonly _contentRefs = new Map<ComponentRef<SdToast>, ComponentRef<any>>();
48
+ private readonly _autoDismissCleanups = new Map<ComponentRef<SdToast>, () => void>();
48
49
 
49
50
  constructor() {
50
51
  effect(() => {
@@ -234,19 +235,30 @@ export class SdToastProvider {
234
235
  onDismiss?: () => void,
235
236
  ): void {
236
237
  const el = toastRef.location.nativeElement as HTMLElement;
238
+ const ac = new AbortController();
239
+ let timerId: ReturnType<typeof setTimeout> | undefined;
237
240
  let isHovering = false;
238
241
  let dismissPending = false;
239
242
  let dismissed = false;
240
243
 
244
+ // 타이머·리스너 정리. 외부 파괴 경로(_dismissToast/_destroyToast)에서도 호출되어
245
+ // 죽은 toastRef 에 대해 타이머가 깨어나는 race 와 리스너 누수를 막는다.
246
+ const cleanup = () => {
247
+ dismissed = true;
248
+ if (timerId != null) clearTimeout(timerId);
249
+ ac.abort();
250
+ this._autoDismissCleanups.delete(toastRef);
251
+ };
252
+ this._autoDismissCleanups.set(toastRef, cleanup);
253
+
241
254
  const dismiss = () => {
242
255
  if (dismissed) return;
243
- dismissed = true;
244
256
  onDismiss?.();
245
257
  this._dismissToast(toastRef);
246
258
  };
247
259
 
248
260
  const dismissAfterDelay = (ms: number) => {
249
- setTimeout(() => {
261
+ timerId = setTimeout(() => {
250
262
  if (dismissed) return;
251
263
  if (isHovering) {
252
264
  dismissPending = true;
@@ -256,21 +268,30 @@ export class SdToastProvider {
256
268
  }, ms);
257
269
  };
258
270
 
259
- el.addEventListener("mouseenter", () => {
260
- isHovering = true;
261
- });
262
- el.addEventListener("mouseleave", () => {
263
- isHovering = false;
264
- if (dismissPending) {
265
- dismissPending = false;
266
- dismissAfterDelay(1000);
267
- }
268
- });
271
+ el.addEventListener(
272
+ "mouseenter",
273
+ () => {
274
+ isHovering = true;
275
+ },
276
+ { signal: ac.signal },
277
+ );
278
+ el.addEventListener(
279
+ "mouseleave",
280
+ () => {
281
+ isHovering = false;
282
+ if (dismissPending) {
283
+ dismissPending = false;
284
+ dismissAfterDelay(1000);
285
+ }
286
+ },
287
+ { signal: ac.signal },
288
+ );
269
289
 
270
290
  dismissAfterDelay(delayMs);
271
291
  }
272
292
 
273
293
  private _dismissToast(toastRef: ComponentRef<SdToast>): void {
294
+ this._autoDismissCleanups.get(toastRef)?.();
274
295
  toastRef.instance.open.set(false);
275
296
 
276
297
  const el = toastRef.location.nativeElement as HTMLElement;
@@ -292,6 +313,8 @@ export class SdToastProvider {
292
313
  }
293
314
 
294
315
  private _destroyToast(toastRef: ComponentRef<SdToast>): void {
316
+ this._autoDismissCleanups.get(toastRef)?.();
317
+
295
318
  const idx = this._toastRefs.indexOf(toastRef);
296
319
  if (idx === -1) return; // 이미 파괴됨
297
320
 
@@ -133,8 +133,8 @@ export class SdKanbanLane<L, T> implements SdKanbanDropTarget<L, T> {
133
133
 
134
134
  kanbanControls = contentChildren<SdKanban<L, T>>(SdKanban, { descendants: true });
135
135
 
136
- toolTplRef = contentChild<any, TemplateRef<void>>("toolTpl", { read: TemplateRef });
137
- titleTplRef = contentChild<any, TemplateRef<void>>("titleTpl", { read: TemplateRef });
136
+ toolTplRef = contentChild<unknown, TemplateRef<void>>("toolTpl", { read: TemplateRef });
137
+ titleTplRef = contentChild<unknown, TemplateRef<void>>("titleTpl", { read: TemplateRef });
138
138
 
139
139
  isAllSelected = computed(() => {
140
140
  const selectableControls = this.kanbanControls().filter((ctrl) => ctrl.selectable());
@@ -3,11 +3,10 @@ import {
3
3
  booleanAttribute,
4
4
  ChangeDetectionStrategy,
5
5
  Component,
6
+ computed,
6
7
  contentChild,
7
- effect,
8
8
  input,
9
9
  model,
10
- signal,
11
10
  TemplateRef,
12
11
  ViewEncapsulation,
13
12
  } from "@angular/core";
@@ -70,27 +69,23 @@ export class SdSharedDataSelectButton<
70
69
  inset = input(false, { transform: booleanAttribute });
71
70
  size = input<"sm" | "lg">();
72
71
 
73
- itemTplRef = contentChild.required<any, TemplateRef<SdItemOfTemplateContext<TItem>>>(
72
+ itemTplRef = contentChild.required<SdItemOfTemplate<TItem>, TemplateRef<SdItemOfTemplateContext<TItem>>>(
74
73
  SdItemOfTemplate,
75
74
  { read: TemplateRef },
76
75
  );
77
76
 
78
- protected readonly _selectedItems = signal<TItem[]>([]);
77
+ _selectedItems = computed<TItem[]>(() => {
78
+ const v = this.value();
79
+ const items = this.items();
80
+ const mode = this.selectMode();
79
81
 
80
- constructor() {
81
- effect(() => {
82
- const v = this.value();
83
- const items = this.items();
84
- const mode = this.selectMode();
85
-
86
- if (mode === "multi" && Array.isArray(v) && v.filterExists().length > 0) {
87
- const keys = v.filterExists() as (string | number)[];
88
- this._selectedItems.set(items.filter((it) => keys.includes(it.__valueKey)));
89
- } else if (mode === "single" && !Array.isArray(v) && v != null) {
90
- this._selectedItems.set(items.filter((it) => it.__valueKey === v));
91
- } else {
92
- this._selectedItems.set([]);
93
- }
94
- });
95
- }
82
+ if (mode === "multi" && Array.isArray(v) && v.filterExists().length > 0) {
83
+ const keys = v.filterExists() as (string | number)[];
84
+ return items.filter((it) => keys.includes(it.__valueKey));
85
+ } else if (mode === "single" && !Array.isArray(v) && v != null) {
86
+ return items.filter((it) => it.__valueKey === v);
87
+ } else {
88
+ return [];
89
+ }
90
+ });
96
91
  }
@@ -146,17 +146,17 @@ export class SdSharedDataSelectList<
146
146
 
147
147
  header = input<string>();
148
148
 
149
- headerTplRef = contentChild<any, TemplateRef<void>>("headerTpl", {
149
+ headerTplRef = contentChild<unknown, TemplateRef<void>>("headerTpl", {
150
150
  read: TemplateRef,
151
151
  });
152
- filterTplRef = contentChild<any, TemplateRef<void>>("filterTpl", {
152
+ filterTplRef = contentChild<unknown, TemplateRef<void>>("filterTpl", {
153
153
  read: TemplateRef,
154
154
  });
155
- itemTplRef = contentChild<any, TemplateRef<SdItemOfTemplateContext<TItem>>>(
155
+ itemTplRef = contentChild<SdItemOfTemplate<TItem>, TemplateRef<SdItemOfTemplateContext<TItem>>>(
156
156
  SdItemOfTemplate,
157
157
  { read: TemplateRef },
158
158
  );
159
- undefinedTplRef = contentChild<any, TemplateRef<void>>("undefinedTpl", {
159
+ undefinedTplRef = contentChild<unknown, TemplateRef<void>>("undefinedTpl", {
160
160
  read: TemplateRef,
161
161
  });
162
162
 
@@ -1,4 +1,5 @@
1
1
  import { NgTemplateOutlet } from "@angular/common";
2
+ import type { DateOnly, DateTime, Time } from "@simplysm/core-common";
2
3
  import {
3
4
  booleanAttribute,
4
5
  ChangeDetectionStrategy,
@@ -170,13 +171,13 @@ export class SdSharedDataSelect<
170
171
  getSearchTextFn = input<(item: TItem, index: number) => string>(
171
172
  (item) => item.__searchText,
172
173
  );
173
- displayOrderKeyProp = input<string>();
174
+ displayOrderByFn = input<(item: TItem) => string | number | DateOnly | DateTime | Time | undefined>();
174
175
 
175
- itemTplRef = contentChild<any, TemplateRef<SdItemOfTemplateContext<TItem>>>(
176
+ itemTplRef = contentChild<SdItemOfTemplate<TItem>, TemplateRef<SdItemOfTemplateContext<TItem>>>(
176
177
  SdItemOfTemplate,
177
178
  { read: TemplateRef },
178
179
  );
179
- undefinedTplRef = contentChild<any, TemplateRef<void>>("undefinedTpl", {
180
+ undefinedTplRef = contentChild<unknown, TemplateRef<void>>("undefinedTpl", {
180
181
  read: TemplateRef,
181
182
  });
182
183
 
@@ -219,9 +220,9 @@ export class SdSharedDataSelect<
219
220
  return true;
220
221
  });
221
222
 
222
- const orderProp = this.displayOrderKeyProp();
223
- if (orderProp != null) {
224
- result = result.orderBy((item) => (item as any)[orderProp]);
223
+ const orderBy = this.displayOrderByFn();
224
+ if (orderBy != null) {
225
+ result = result.orderBy(orderBy);
225
226
  }
226
227
 
227
228
  return result;
@@ -316,15 +317,12 @@ export class SdSharedDataSelect<
316
317
  const parentMap = this.itemByParentKeyMap();
317
318
  if (parentMap == null) return undefined;
318
319
 
319
- const orderProp = this.displayOrderKeyProp();
320
- if (orderProp == null) return parentMap;
320
+ const orderBy = this.displayOrderByFn();
321
+ if (orderBy == null) return parentMap;
321
322
 
322
323
  const sorted = new Map<TItem["__valueKey"] | undefined, TItem[]>();
323
324
  for (const [key, children] of parentMap) {
324
- sorted.set(
325
- key,
326
- children.orderBy((item) => (item as any)[orderProp]),
327
- );
325
+ sorted.set(key, children.orderBy(orderBy));
328
326
  }
329
327
  return sorted;
330
328
  });
@@ -32,7 +32,7 @@ export class SdSheetColumn<T = unknown> {
32
32
  disableResizing = input(false, { transform: booleanAttribute });
33
33
  ordering = input(0);
34
34
 
35
- cellTplRef = contentChild.required<any, TemplateRef<SdSheetCellContext<T>>>(
35
+ cellTplRef = contentChild.required<SdSheetColumnCellTemplate<T>, TemplateRef<SdSheetCellContext<T>>>(
36
36
  SdSheetColumnCellTemplate,
37
37
  { read: TemplateRef },
38
38
  );