@things-factory/figure-ui 10.1.5 → 10.1.7

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 (79) hide show
  1. package/client/graphql/index.ts +36 -3
  2. package/client/modeller/figure-animations.ts +712 -0
  3. package/client/modeller/figure-ask.ts +5 -5
  4. package/client/modeller/figure-canvas.ts +1 -1
  5. package/client/modeller/figure-capabilities.ts +372 -0
  6. package/client/modeller/figure-history-panel.ts +171 -18
  7. package/client/modeller/figure-inspector.ts +439 -76
  8. package/client/modeller/figure-parts.ts +15 -15
  9. package/client/modeller/figure-preview.ts +2 -2
  10. package/client/modeller/figure-report.ts +102 -37
  11. package/client/modeller/figure-settings.ts +20 -20
  12. package/client/modeller/figure-side.ts +91 -15
  13. package/client/modeller/figure-source.test.ts +15 -1
  14. package/client/modeller/figure-source.ts +31 -15
  15. package/client/modeller/figure-thumbnail.ts +49 -4
  16. package/client/modeller/maturity.ts +10 -10
  17. package/client/pages/figure-list-page.ts +204 -66
  18. package/client/pages/figure-modeller-page.ts +158 -42
  19. package/client/types.ts +31 -0
  20. package/client/viewparts/figure-thumb.ts +250 -0
  21. package/dist-client/graphql/index.d.ts +8 -1
  22. package/dist-client/graphql/index.js +33 -3
  23. package/dist-client/graphql/index.js.map +1 -1
  24. package/dist-client/modeller/figure-animations.d.ts +93 -0
  25. package/dist-client/modeller/figure-animations.js +655 -0
  26. package/dist-client/modeller/figure-animations.js.map +1 -0
  27. package/dist-client/modeller/figure-ask.js +5 -5
  28. package/dist-client/modeller/figure-ask.js.map +1 -1
  29. package/dist-client/modeller/figure-canvas.js +1 -1
  30. package/dist-client/modeller/figure-canvas.js.map +1 -1
  31. package/dist-client/modeller/figure-capabilities.d.ts +42 -0
  32. package/dist-client/modeller/figure-capabilities.js +344 -0
  33. package/dist-client/modeller/figure-capabilities.js.map +1 -0
  34. package/dist-client/modeller/figure-history-panel.d.ts +15 -1
  35. package/dist-client/modeller/figure-history-panel.js +165 -18
  36. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  37. package/dist-client/modeller/figure-inspector.d.ts +98 -0
  38. package/dist-client/modeller/figure-inspector.js +425 -75
  39. package/dist-client/modeller/figure-inspector.js.map +1 -1
  40. package/dist-client/modeller/figure-parts.js +15 -15
  41. package/dist-client/modeller/figure-parts.js.map +1 -1
  42. package/dist-client/modeller/figure-preview.js +2 -2
  43. package/dist-client/modeller/figure-preview.js.map +1 -1
  44. package/dist-client/modeller/figure-report.d.ts +18 -1
  45. package/dist-client/modeller/figure-report.js +105 -37
  46. package/dist-client/modeller/figure-report.js.map +1 -1
  47. package/dist-client/modeller/figure-settings.js +19 -19
  48. package/dist-client/modeller/figure-settings.js.map +1 -1
  49. package/dist-client/modeller/figure-side.d.ts +24 -0
  50. package/dist-client/modeller/figure-side.js +94 -14
  51. package/dist-client/modeller/figure-side.js.map +1 -1
  52. package/dist-client/modeller/figure-source.d.ts +15 -8
  53. package/dist-client/modeller/figure-source.js +16 -7
  54. package/dist-client/modeller/figure-source.js.map +1 -1
  55. package/dist-client/modeller/figure-thumbnail.js +40 -4
  56. package/dist-client/modeller/figure-thumbnail.js.map +1 -1
  57. package/dist-client/modeller/maturity.js +10 -10
  58. package/dist-client/modeller/maturity.js.map +1 -1
  59. package/dist-client/pages/figure-list-page.d.ts +11 -4
  60. package/dist-client/pages/figure-list-page.js +201 -63
  61. package/dist-client/pages/figure-list-page.js.map +1 -1
  62. package/dist-client/pages/figure-modeller-page.d.ts +33 -0
  63. package/dist-client/pages/figure-modeller-page.js +156 -42
  64. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  65. package/dist-client/tsconfig.tsbuildinfo +1 -1
  66. package/dist-client/types.d.ts +29 -0
  67. package/dist-client/types.js.map +1 -1
  68. package/dist-client/viewparts/figure-thumb.d.ts +29 -0
  69. package/dist-client/viewparts/figure-thumb.js +242 -0
  70. package/dist-client/viewparts/figure-thumb.js.map +1 -0
  71. package/dist-server/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +6 -5
  73. package/test/i18n-prefix-guard.test.ts +171 -0
  74. package/test/korean-register-guard.test.ts +83 -0
  75. package/translations/en.json +351 -264
  76. package/translations/ja.json +276 -264
  77. package/translations/ko.json +351 -264
  78. package/translations/ms.json +276 -264
  79. package/translations/zh.json +276 -264
@@ -68,29 +68,23 @@ function rememberPanes(panes: Panes): void {
68
68
  }
69
69
 
70
70
  /**
71
- * 새로 만든 Figure 의 시작 상태. **씬 모델로 만든다.**
71
+ * 새로 만든 Figure 의 시작 상태 **빈 상자 하나뿐이다.**
72
72
  *
73
- * 상태로 시작하면 검증이 곧바로 오류를 낸다 사용자가 아무것도 하지 않았는데 빨간
74
- * 화면을 보게 된다. 그래서 부품 하나를 놓아 준다. **처음 열었을 바로 무언가 보이는
75
- * 것**이 이 도구를 익히는 첫 단계다.
73
+ * 전에는 부품(300 상자)을 하나 놓아 주었다. 형식이 부품 0개를 오류로 보기 때문이다
74
+ * (`missing-parts` parts 없다). 그래서 아무것도 했는데 붉은 판정이 뜨는 것을 피하려고
75
+ * 상자를 깔았다.
76
+ *
77
+ * **그런데 「무조건 상자」는 대개 첫 동작이 그것을 지우는 일이 된다** — 컨베이어를 만들 사람도,
78
+ * 시그널타워를 만들 사람도 상자부터 치운다. 그래서 비운다. 형식은 그대로 두고(부품 없는 것은
79
+ * 발행할 수 없는 것이 맞다) **화면이 「아직 저작 안 함」과 「형식을 어김」을 가른다.**
80
+ *
81
+ * 빈 화면이 가르치는 자리다: 부품 목록이 「위에서 도형을 골라 시작하세요」를 이미 말하고 팔레트가
82
+ * 바로 그 위에 있다(`figure-parts`).
76
83
  */
77
84
  function startingModel(type: string): { board: BoardModel; parts: PartModel[] } {
78
85
  return {
79
86
  board: { width: 300, height: 300, depth: 300, figureType: type, detailLevel: 'M' },
80
- parts: [
81
- {
82
- type: 'figure-part',
83
- name: 'body',
84
- primitive: 'cube',
85
- left: 0,
86
- top: 0,
87
- width: 300,
88
- height: 300,
89
- zPos: 0,
90
- depth: 300,
91
- token: 'palette.primary'
92
- }
93
- ]
87
+ parts: []
94
88
  }
95
89
  }
96
90
 
@@ -251,6 +245,23 @@ export class FigureModellerPage extends FigureModellerPageBase {
251
245
  cursor: default;
252
246
  }
253
247
 
248
+ /*
249
+ 저장이 거절된 사유. 단추 **바로 왼쪽**에 둔다 — 누른 자리에서 답을 봐야 한다. 길면 줄이지만
250
+ 전문은 title 로 남긴다(서버 문장이 길 수 있다).
251
+ */
252
+ span[save-failed] {
253
+ max-width: 280px;
254
+ overflow: hidden;
255
+ text-overflow: ellipsis;
256
+ white-space: nowrap;
257
+ padding: 4px 8px;
258
+ font: var(--label-font, inherit);
259
+ font-size: 0.72rem;
260
+ color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));
261
+ background-color: var(--md-sys-color-error-container);
262
+ border-radius: 6px;
263
+ }
264
+
254
265
  /*
255
266
  칸을 벡터 저작도구의 관례대로 놓는다 — 왼쪽에 무엇이 있나(레이어), 오른쪽에
256
267
  고른 것의 속성. 쓰는 사람이 이미 아는 자리라 배울 것이 하나 줄어든다.
@@ -510,6 +521,16 @@ export class FigureModellerPage extends FigureModellerPageBase {
510
521
  @state() private dirty = false
511
522
  @state() private saving = false
512
523
  @state() private violations: FigureFinding[] = []
524
+ /**
525
+ * 저장이 왜 안 됐나.
526
+ *
527
+ * **`save()` 에 `catch` 가 없었다.** `try … finally` 뿐이라 서버가 거절하면(형식 위반 · 타입 이름
528
+ * 중복 · 권한 없음 · 네트워크) 단추만 멈추고 화면은 **아무 말도 하지 않았다.** 저작자는 눌렀는데
529
+ * 아무 일도 안 난 것으로 보고, 저장됐는지 아닌지도 알 수 없었다.
530
+ *
531
+ * 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
532
+ */
533
+ @state() private saveFailure = ''
513
534
  /** 저작 보조가 낸 후보. 받기 전에는 원본 데이터가 아니다. */
514
535
  @state() private proposal?: FigureSource
515
536
  /** 그 후보의 셈. 무엇을 받는 것인지 숫자로도 보여 준다. */
@@ -533,7 +554,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
533
554
 
534
555
  get context() {
535
556
  return {
536
- title: this.figure?.name ?? i18next.t('title.new-figure'),
557
+ title: this.figure?.name ?? i18next.t('figure.title.new-figure'),
537
558
  /*
538
559
  **제목 줄은 끈다.** 이름은 그대로 둔다.
539
560
 
@@ -558,7 +579,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
558
579
  하네스 시절의 자리였다 — 앱에서는 셸이 페이지 제목으로 같은 이름을 그리므로 글자가 두 번
559
580
  나오고, 캔버스가 주인공인 화면에서 그만큼 높이를 잃었다.
560
581
  -->
561
- ${this.dirty ? html`<span dirty>${i18next.t('text.unsaved-changes')}</span>` : ''}
582
+ ${this.dirty ? html`<span dirty>${i18next.t('figure.text.unsaved-changes')}</span>` : ''}
562
583
  <!--
563
584
  말로 시키는 줄이 머리줄의 남는 폭을 쓴다. 따로 두면 화면 위가 두 줄이 되고, 그 위 줄은
564
585
  이름을 페이지 제목과 겹쳐 적는다.
@@ -568,8 +589,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
568
589
  .type=${source?.type ?? ''}
569
590
  @proposed=${(e: CustomEvent) => this.receive(e.detail.proposal)}
570
591
  ></figure-ask>
592
+ ${this.saveFailure ? html`<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
571
593
  <button save ?disabled=${!this.canSave} @click=${() => this.save()}>
572
- ${this.saving ? i18next.t('text.saving') : i18next.t('button.save')}
594
+ ${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
573
595
  </button>
574
596
  </div>
575
597
 
@@ -578,6 +600,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
578
600
  ?collapse-left=${this.collapsed.left}
579
601
  ?collapse-right=${this.collapsed.right}
580
602
  @figure-rename=${(e: CustomEvent) => this.rename(e.detail.name)}
603
+ @figure-catalog=${(e: CustomEvent) => this.catalog(e.detail)}
581
604
  @figure-released=${(e: CustomEvent) => this.released(e.detail.figure)}
582
605
  @figure-reverted=${() => this.figure?.id && this.load(this.figure.id)}
583
606
  @add-part=${(e: CustomEvent) => this.addPart(e.detail.primitive)}
@@ -591,6 +614,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
591
614
  @toggle-part-hidden=${(e: CustomEvent) => this.toggleHidden(e.detail.index)}
592
615
  @set-all-parts-hidden=${(e: CustomEvent) => this.setAllHidden(e.detail.hidden)}
593
616
  @change-detail-level=${(e: CustomEvent) => this.changeDetailLevel(e.detail.level)}
617
+ @update-board=${(e: CustomEvent) => this.updateBoard(e.detail.board)}
594
618
  @part-tool=${(e: CustomEvent) => this.runTool(e.detail.index, e.detail.tool, e.detail.axis)}
595
619
  @pick-part=${(e: CustomEvent) => this.pickPart(e.detail.name)}
596
620
  >
@@ -607,7 +631,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
607
631
  ? html`
608
632
  <div lanes>
609
633
  <div lane>
610
- <h4>${i18next.t('label.now')}</h4>
634
+ <h4>${i18next.t('figure.label.now')}</h4>
611
635
  <figure-canvas
612
636
  .board=${this.board}
613
637
  .parts=${this.parts}
@@ -617,11 +641,11 @@ export class FigureModellerPage extends FigureModellerPageBase {
617
641
  </div>
618
642
  <div lane candidate>
619
643
  <h4>
620
- ${i18next.t('label.proposed')}
644
+ ${i18next.t('figure.label.proposed')}
621
645
  ${this.proposalOf
622
646
  ? html`<small>
623
647
  ${this.proposalOf.grade} ·
624
- ${i18next.t('text.n-triangles', { count: this.proposalOf.triangles })}
648
+ ${i18next.t('figure.text.n-triangles', { count: this.proposalOf.triangles })}
625
649
  </small>`
626
650
  : ''}
627
651
  </h4>
@@ -640,6 +664,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
640
664
 
641
665
  <figure-side
642
666
  .figureName=${this.figure?.name ?? ''}
667
+ .figureCategory=${this.figure?.category}
668
+ .figureTags=${this.figure?.tags}
643
669
  .figureId=${this.figure?.id ?? ''}
644
670
  .figureState=${this.figure?.state}
645
671
  .figureVersion=${this.figure?.version ?? 0}
@@ -677,10 +703,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
677
703
  <button
678
704
  mode
679
705
  ?on=${this.mode === mode}
680
- title=${i18next.t('text.mode-' + mode + '-explained')}
706
+ title=${i18next.t('figure.text.mode-' + mode + '-explained')}
681
707
  @click=${() => (this.mode = mode)}
682
708
  >
683
- ${i18next.t('label.mode-' + mode)}
709
+ ${i18next.t('figure.label.mode-' + mode)}
684
710
  </button>
685
711
  `
686
712
 
@@ -706,7 +732,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
706
732
  return html`
707
733
  <button
708
734
  collapse
709
- title=${i18next.t(`text.${gone ? 'expand' : 'collapse'}-${pane}-pane`)}
735
+ title=${i18next.t(`figure.text.${gone ? 'expand' : 'collapse'}-${pane}-pane`)}
710
736
  @click=${() => this.toggleCollapse(side)}
711
737
  >
712
738
  <md-icon>${gone ? back : shut}</md-icon>
@@ -755,11 +781,11 @@ export class FigureModellerPage extends FigureModellerPageBase {
755
781
 
756
782
  return html`
757
783
  <div decide>
758
- <span>${i18next.t('text.assistant-proposed-a-figure')}</span>
784
+ <span>${i18next.t('figure.text.assistant-proposed-a-figure')}</span>
759
785
  <div spacer></div>
760
- <button drop @click=${() => this.discard()}>${i18next.t('button.discard')}</button>
786
+ <button drop @click=${() => this.discard()}>${i18next.t('figure.button.discard')}</button>
761
787
  <button take ?disabled=${errors.length > 0 || this.picked.size === 0} @click=${() => this.take()}>
762
- ${i18next.t('button.take-n-changes', { n: this.picked.size })}
788
+ ${i18next.t('figure.button.take-n-changes', { n: this.picked.size })}
763
789
  </button>
764
790
  </div>
765
791
 
@@ -771,7 +797,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
771
797
  `
772
798
  : ''}
773
799
  ${errors.length > 0
774
- ? html`<p blocked>${i18next.t('text.picked-changes-do-not-validate')} — ${errors[0].message}</p>`
800
+ ? html`<p blocked>${i18next.t('figure.text.picked-changes-do-not-validate')} — ${errors[0].message}</p>`
775
801
  : violations.length > 0
776
802
  ? html`<p blocked warn>${violations[0].message}</p>`
777
803
  : ''}
@@ -796,7 +822,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
796
822
  <button
797
823
  change
798
824
  ?on=${on}
799
- title=${i18next.t('text.change-' + change.kind)}
825
+ title=${i18next.t('figure.text.change-' + change.kind)}
800
826
  @click=${() => this.toggle(change.key)}
801
827
  >
802
828
  <md-icon>${icon}</md-icon>
@@ -866,9 +892,18 @@ export class FigureModellerPage extends FigureModellerPageBase {
866
892
  *
867
893
  * 형식을 어긴 원본 데이터로는 저장을 누를 수 없게 한다. 눌렀다가 서버가 받아주지 않는 것보다,
868
894
  * 누를 수 없는 이유가 판단 패널에 떠 있는 것이 낫다.
895
+ *
896
+ * ## 바뀐 것이 없으면 누를 수 없다
897
+ *
898
+ * **`dirty` 를 안 보고 있었다.** 저장한 직후에도 단추가 살아 있어 같은 정본을 몇 번이고 다시 쓸
899
+ * 수 있었고, 저장마다 썸네일을 새로 찍어 20KB 를 다시 넣고 `thumbnailUpdatedAt` 을 올렸다 —
900
+ * 그림 주소의 판이 바뀌므로 브라우저 캐시도 함께 버려진다. 바뀐 것이 없는데 일어날 일이 아니다.
901
+ *
902
+ * 새로 만드는 중인 것은 `startNew` 가 `dirty` 로 시작하므로 첫 저장이 막히지 않는다.
903
+ * 저장이 거절되면 `dirty` 를 그대로 두므로 다시 누를 수 있다.
869
904
  */
870
905
  private get canSave(): boolean {
871
- if (this.saving || !this.folded || !this.figure?.name) {
906
+ if (this.saving || !this.dirty || !this.folded || !this.figure?.name) {
872
907
  return false
873
908
  }
874
909
  return validate(this.folded).errors.length === 0
@@ -880,9 +915,41 @@ export class FigureModellerPage extends FigureModellerPageBase {
880
915
  }
881
916
 
882
917
  const id = lifecycle?.resourceId
883
- if (id && id !== this.figure?.id) {
884
- await this.load(id)
885
- } else if (!id && !this.figure) {
918
+
919
+ if (id) {
920
+ if (id !== this.figure?.id) {
921
+ await this.load(id)
922
+ }
923
+ return
924
+ }
925
+
926
+ /*
927
+ ── id 없이 들어온 것은 「새로 만든다」는 뜻이다 ─────────────────────────────
928
+ 전에는 `!this.figure` 일 때만 새로 시작했다. 셸은 페이지를 살려 두므로(MDI 탭) 카탈로그에서
929
+ 하나 열어 본 뒤에는 이 페이지가 그 도형을 계속 들고 있고, 그래서 「새 figure」가 **아무 일도
930
+ 하지 않았다.**
931
+
932
+ 그것보다 나쁜 것이 있었다 — 화면에는 옛 도형이 그대로 있으니 사람은 새 것인 줄 알고 고치고
933
+ 저장한다. 그러면 **열려 있던 도형에 덮어쓴다.** 「안 열린다」로 보이던 것이 실은 그 문이었다.
934
+
935
+ 들고 있는 것이 **저장된 도형이면**(id 가 있으면) 새로 시작한다. 아직 저장 안 한 새 도형을
936
+ 들고 있으면 그대로 둔다 — 페이지를 다시 들렀다고 저작 중인 것을 버리지 않는다.
937
+ */
938
+ if (!this.figure) {
939
+ this.startNew()
940
+ return
941
+ }
942
+
943
+ if (this.figure.id) {
944
+ /*
945
+ 고치던 것이 있으면 묻는다. 저작한 것을 말없이 버리지 않는다 — 되돌릴 수 없다.
946
+ 그만두면 보고 있던 도형의 주소로 되돌려 화면과 주소가 어긋나지 않게 한다.
947
+ */
948
+ if (this.dirty && !confirm(i18next.t('figure.text.discard-changes-and-start-new'))) {
949
+ navigate(`figure-modeller/${this.figure.id}`, true)
950
+ return
951
+ }
952
+
886
953
  this.startNew()
887
954
  }
888
955
  }
@@ -892,8 +959,14 @@ export class FigureModellerPage extends FigureModellerPageBase {
892
959
 
893
960
  this.figure = figure
894
961
 
895
- /* 그림이 없으면 이 자리에서 채운다(아래 `fillThumbnail`). 기다리지 않는다 — 화면이 먼저다. */
896
- if (!figure.thumbnail && figure.id) {
962
+ /*
963
+ 그림이 없으면 이 자리에서 채운다(아래 `fillThumbnail`). 기다리지 않는다 — 화면이 먼저다.
964
+
965
+ **`thumbnail` 이 아니라 `thumbnailUpdatedAt` 으로 판정한다.** 그림 자체는 목록·낱개 질의가
966
+ 가져오지 않는다(base64 라서 무겁고 캐시가 안 된다 — 주소로 부른다). 없는 칸으로 판정하면
967
+ 늘 「그림이 없다」가 되어 저작면을 열 때마다 다시 찍어 저장하게 된다.
968
+ */
969
+ if (!figure.thumbnailUpdatedAt && figure.id) {
897
970
  void this.fillThumbnail(figure.id)
898
971
  }
899
972
 
@@ -927,7 +1000,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
927
1000
  this.figure = { id: '', type: draftType, name: '' } as Figure
928
1001
  this.board = board
929
1002
  this.parts = parts
930
- this.selected = 0
1003
+ /* 고른 것이 없다 — 부품이 없으니 속성 패널도 고를 것을 기다린다. */
1004
+ this.selected = -1
931
1005
  this.dirty = true
932
1006
  this.violations = []
933
1007
  }
@@ -939,6 +1013,19 @@ export class FigureModellerPage extends FigureModellerPageBase {
939
1013
  this.dirty = true
940
1014
  }
941
1015
 
1016
+ /**
1017
+ * 분류와 태그를 고친다.
1018
+ *
1019
+ * 정본(`source`)이 아니라 **카탈로그의 값**이다 — 도형이 무엇으로 생겼는지가 아니라 어디에
1020
+ * 걸리는지다. 그래서 점수도 발행도 흔들리지 않는다.
1021
+ */
1022
+ private catalog(patch: { category?: string; tags?: string[] }) {
1023
+ if (!this.figure) return
1024
+
1025
+ this.figure = { ...this.figure, ...patch }
1026
+ this.dirty = true
1027
+ }
1028
+
942
1029
  /**
943
1030
  * 부품 목록을 갈아 끼운다.
944
1031
  *
@@ -973,6 +1060,17 @@ export class FigureModellerPage extends FigureModellerPageBase {
973
1060
  this.put(edits.updatePart(this.parts, index, part))
974
1061
  }
975
1062
 
1063
+ /**
1064
+ * 판 자체의 값을 갈아 끼운다 — 능력처럼 부품보다 위에 있는 것들.
1065
+ *
1066
+ * 부품 목록은 건드리지 않는다. `put` 은 부품을 함께 받으므로 여기서 쓰면 능력 하나를
1067
+ * 고칠 때마다 부품 목록이 새 배열이 되고, 그리는 쪽이 전부 다시 그린다.
1068
+ */
1069
+ private updateBoard(board: BoardModel) {
1070
+ this.board = board
1071
+ this.dirty = true
1072
+ }
1073
+
976
1074
  /**
977
1075
  * 디테일 등급을 바꾼다.
978
1076
  *
@@ -1101,8 +1199,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
1101
1199
  }
1102
1200
 
1103
1201
  try {
1104
- await updateFigure(id, { thumbnail })
1105
- this.figure = { ...this.figure, thumbnail }
1202
+ const { figure } = await updateFigure(id, { thumbnail })
1203
+ /* 서버가 찍은 시각을 그대로 받는다 — 이 값이 그림을 부르는 주소의 판 번호가 된다. */
1204
+ this.figure = { ...this.figure, thumbnailUpdatedAt: figure?.thumbnailUpdatedAt }
1106
1205
  } catch (e) {
1107
1206
  /* 볼 권한만 있는 사람일 수 있다. 화면은 그대로 두고 조용히 넘어간다. */
1108
1207
  console.warn(`[thumbnail] ${id} 를 채우지 못했습니다 — ${(e as Error).message}`)
@@ -1115,6 +1214,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1115
1214
  }
1116
1215
 
1117
1216
  this.saving = true
1217
+ this.saveFailure = ''
1118
1218
  try {
1119
1219
  const sourceJson = JSON.stringify(this.folded)
1120
1220
  /*
@@ -1130,6 +1230,12 @@ export class FigureModellerPage extends FigureModellerPageBase {
1130
1230
  if (this.figure.id) {
1131
1231
  const result = await updateFigure(this.figure.id, {
1132
1232
  name: this.figure.name,
1233
+ /*
1234
+ 카탈로그의 값도 함께 보낸다. 화면에서 고쳐 놓고 저장에 안 실으면 저장 뒤에 조용히
1235
+ 사라진다 — 저작자는 저장했다고 보고 다시 열면 없다.
1236
+ */
1237
+ category: this.figure.category,
1238
+ tags: this.figure.tags,
1133
1239
  source: sourceJson,
1134
1240
  thumbnail
1135
1241
  })
@@ -1140,12 +1246,14 @@ export class FigureModellerPage extends FigureModellerPageBase {
1140
1246
  // 만든 뒤에는 고칠 수 없으므로 여기서 걸러야 한다.
1141
1247
  const taken = await fetchFigureTypeNames()
1142
1248
  if (taken.includes(this.folded.type)) {
1143
- throw new Error(i18next.t('text.type-name-already-taken', { type: this.folded.type }))
1249
+ throw new Error(i18next.t('figure.text.type-name-already-taken', { type: this.folded.type }))
1144
1250
  }
1145
1251
 
1146
1252
  const result = await createFigure({
1147
1253
  type: this.folded.type,
1148
1254
  name: this.figure.name,
1255
+ category: this.figure.category,
1256
+ tags: this.figure.tags,
1149
1257
  source: sourceJson,
1150
1258
  thumbnail
1151
1259
  })
@@ -1155,6 +1263,14 @@ export class FigureModellerPage extends FigureModellerPageBase {
1155
1263
  }
1156
1264
 
1157
1265
  this.dirty = false
1266
+ } catch (e) {
1267
+ /*
1268
+ **서버가 말한 사유를 그대로 올린다.** 여기서 문장을 다시 지으면 무엇이 걸렸는지 알 수 없다 —
1269
+ 타입 이름이 이미 쓰였다는 것과 권한이 없다는 것은 저작자가 할 일이 다르다.
1270
+
1271
+ `dirty` 는 건드리지 않는다. 저장이 안 됐으므로 미저장인 것이 사실이다.
1272
+ */
1273
+ this.saveFailure = (e as Error).message
1158
1274
  } finally {
1159
1275
  this.saving = false
1160
1276
  }
package/client/types.ts CHANGED
@@ -14,7 +14,10 @@ export interface Figure {
14
14
  type: string
15
15
  name: string
16
16
  description?: string
17
+ /** 분류 하나 — 팔레트에서 묶어 보이는 기준. */
17
18
  category?: string
19
+ /** 태그 여럿 — 분류로는 못 하는 찾기. */
20
+ tags?: string[]
18
21
  state?: 'draft' | 'released'
19
22
  /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */
20
23
  score?: number
@@ -63,6 +66,7 @@ export interface NewFigure {
63
66
  name: string
64
67
  description?: string
65
68
  category?: string
69
+ tags?: string[]
66
70
  source: string
67
71
  properties?: string
68
72
  state?: 'draft' | 'released'
@@ -79,6 +83,8 @@ export interface FigurePatch {
79
83
  name?: string
80
84
  description?: string
81
85
  category?: string
86
+ /** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */
87
+ tags?: string[]
82
88
  source?: string
83
89
  properties?: string
84
90
  state?: 'draft' | 'released'
@@ -111,3 +117,28 @@ export interface FigureProposal {
111
117
  /** 몇 번 만에 됐나. 진단용이다. */
112
118
  attempts: number
113
119
  }
120
+
121
+ /**
122
+ * 발행 판정 한 줄.
123
+ *
124
+ * `FigureFinding` 과 달리 **`blocking` 이 있다.** 저장 쪽 발견은 정의상 막지 않는 것들이고,
125
+ * 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고
126
+ * 화면이 다시 분류하면 규칙이 두 벌이 된다.
127
+ */
128
+ export interface FigureGateFinding {
129
+ code: string
130
+ message: string
131
+ /** 왜 그것이 문제인가 — 사람이 무엇을 보게 되나. */
132
+ why?: string
133
+ /** 무엇을 바꾸면 되나 — 부품 이름과 값으로. */
134
+ how?: string
135
+ /** 갈라진 부품 이름들, 또는 정본 안의 경로. */
136
+ at?: string
137
+ blocking: boolean
138
+ }
139
+
140
+ /** 발행해도 되나. `blocked` 면 서버가 거절한다. */
141
+ export interface FigureInspection {
142
+ blocked: boolean
143
+ findings: FigureGateFinding[]
144
+ }