@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
@@ -54,6 +54,16 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
54
54
  private dirty;
55
55
  private saving;
56
56
  private violations;
57
+ /**
58
+ * 저장이 왜 안 됐나.
59
+ *
60
+ * **`save()` 에 `catch` 가 없었다.** `try … finally` 뿐이라 서버가 거절하면(형식 위반 · 타입 이름
61
+ * 중복 · 권한 없음 · 네트워크) 단추만 멈추고 화면은 **아무 말도 하지 않았다.** 저작자는 눌렀는데
62
+ * 아무 일도 안 난 것으로 보고, 저장됐는지 아닌지도 알 수 없었다.
63
+ *
64
+ * 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
65
+ */
66
+ private saveFailure;
57
67
  /** 저작 보조가 낸 후보. 받기 전에는 원본 데이터가 아니다. */
58
68
  private proposal?;
59
69
  /** 그 후보의 셈. 무엇을 받는 것인지 숫자로도 보여 준다. */
@@ -144,6 +154,15 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
144
154
  *
145
155
  * 형식을 어긴 원본 데이터로는 저장을 누를 수 없게 한다. 눌렀다가 서버가 받아주지 않는 것보다,
146
156
  * 누를 수 없는 이유가 판단 패널에 떠 있는 것이 낫다.
157
+ *
158
+ * ## 바뀐 것이 없으면 누를 수 없다
159
+ *
160
+ * **`dirty` 를 안 보고 있었다.** 저장한 직후에도 단추가 살아 있어 같은 정본을 몇 번이고 다시 쓸
161
+ * 수 있었고, 저장마다 썸네일을 새로 찍어 20KB 를 다시 넣고 `thumbnailUpdatedAt` 을 올렸다 —
162
+ * 그림 주소의 판이 바뀌므로 브라우저 캐시도 함께 버려진다. 바뀐 것이 없는데 일어날 일이 아니다.
163
+ *
164
+ * 새로 만드는 중인 것은 `startNew` 가 `dirty` 로 시작하므로 첫 저장이 막히지 않는다.
165
+ * 저장이 거절되면 `dirty` 를 그대로 두므로 다시 누를 수 있다.
147
166
  */
148
167
  private get canSave();
149
168
  pageUpdated(changes: Record<string, unknown>, lifecycle: {
@@ -152,6 +171,13 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
152
171
  private load;
153
172
  private startNew;
154
173
  private rename;
174
+ /**
175
+ * 분류와 태그를 고친다.
176
+ *
177
+ * 정본(`source`)이 아니라 **카탈로그의 값**이다 — 도형이 무엇으로 생겼는지가 아니라 어디에
178
+ * 걸리는지다. 그래서 점수도 발행도 흔들리지 않는다.
179
+ */
180
+ private catalog;
155
181
  /**
156
182
  * 부품 목록을 갈아 끼운다.
157
183
  *
@@ -164,6 +190,13 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
164
190
  private duplicatePart;
165
191
  private movePart;
166
192
  private updatePart;
193
+ /**
194
+ * 판 자체의 값을 갈아 끼운다 — 능력처럼 부품보다 위에 있는 것들.
195
+ *
196
+ * 부품 목록은 건드리지 않는다. `put` 은 부품을 함께 받으므로 여기서 쓰면 능력 하나를
197
+ * 고칠 때마다 부품 목록이 새 배열이 되고, 그리는 쪽이 전부 다시 그린다.
198
+ */
199
+ private updateBoard;
167
200
  /**
168
201
  * 디테일 등급을 바꾼다.
169
202
  *
@@ -47,29 +47,23 @@ function rememberPanes(panes) {
47
47
  }
48
48
  }
49
49
  /**
50
- * 새로 만든 Figure 의 시작 상태. **씬 모델로 만든다.**
50
+ * 새로 만든 Figure 의 시작 상태 **빈 상자 하나뿐이다.**
51
51
  *
52
- * 상태로 시작하면 검증이 곧바로 오류를 낸다 사용자가 아무것도 하지 않았는데 빨간
53
- * 화면을 보게 된다. 그래서 부품 하나를 놓아 준다. **처음 열었을 바로 무언가 보이는
54
- * 것**이 이 도구를 익히는 첫 단계다.
52
+ * 전에는 부품(300 상자)을 하나 놓아 주었다. 형식이 부품 0개를 오류로 보기 때문이다
53
+ * (`missing-parts` parts 없다). 그래서 아무것도 했는데 붉은 판정이 뜨는 것을 피하려고
54
+ * 상자를 깔았다.
55
+ *
56
+ * **그런데 「무조건 상자」는 대개 첫 동작이 그것을 지우는 일이 된다** — 컨베이어를 만들 사람도,
57
+ * 시그널타워를 만들 사람도 상자부터 치운다. 그래서 비운다. 형식은 그대로 두고(부품 없는 것은
58
+ * 발행할 수 없는 것이 맞다) **화면이 「아직 저작 안 함」과 「형식을 어김」을 가른다.**
59
+ *
60
+ * 빈 화면이 가르치는 자리다: 부품 목록이 「위에서 도형을 골라 시작하세요」를 이미 말하고 팔레트가
61
+ * 바로 그 위에 있다(`figure-parts`).
55
62
  */
56
63
  function startingModel(type) {
57
64
  return {
58
65
  board: { width: 300, height: 300, depth: 300, figureType: type, detailLevel: 'M' },
59
- parts: [
60
- {
61
- type: 'figure-part',
62
- name: 'body',
63
- primitive: 'cube',
64
- left: 0,
65
- top: 0,
66
- width: 300,
67
- height: 300,
68
- zPos: 0,
69
- depth: 300,
70
- token: 'palette.primary'
71
- }
72
- ]
66
+ parts: []
73
67
  };
74
68
  }
75
69
  /**
@@ -122,6 +116,16 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
122
116
  this.dirty = false;
123
117
  this.saving = false;
124
118
  this.violations = [];
119
+ /**
120
+ * 저장이 왜 안 됐나.
121
+ *
122
+ * **`save()` 에 `catch` 가 없었다.** `try … finally` 뿐이라 서버가 거절하면(형식 위반 · 타입 이름
123
+ * 중복 · 권한 없음 · 네트워크) 단추만 멈추고 화면은 **아무 말도 하지 않았다.** 저작자는 눌렀는데
124
+ * 아무 일도 안 난 것으로 보고, 저장됐는지 아닌지도 알 수 없었다.
125
+ *
126
+ * 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
127
+ */
128
+ this.saveFailure = '';
125
129
  /** 받기로 고른 변경들. 처음에는 전부 골라 둔다. */
126
130
  this.picked = new Set();
127
131
  }
@@ -258,6 +262,23 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
258
262
  cursor: default;
259
263
  }
260
264
 
265
+ /*
266
+ 저장이 거절된 사유. 단추 **바로 왼쪽**에 둔다 — 누른 자리에서 답을 봐야 한다. 길면 줄이지만
267
+ 전문은 title 로 남긴다(서버 문장이 길 수 있다).
268
+ */
269
+ span[save-failed] {
270
+ max-width: 280px;
271
+ overflow: hidden;
272
+ text-overflow: ellipsis;
273
+ white-space: nowrap;
274
+ padding: 4px 8px;
275
+ font: var(--label-font, inherit);
276
+ font-size: 0.72rem;
277
+ color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));
278
+ background-color: var(--md-sys-color-error-container);
279
+ border-radius: 6px;
280
+ }
281
+
261
282
  /*
262
283
  칸을 벡터 저작도구의 관례대로 놓는다 — 왼쪽에 무엇이 있나(레이어), 오른쪽에
263
284
  고른 것의 속성. 쓰는 사람이 이미 아는 자리라 배울 것이 하나 줄어든다.
@@ -494,7 +515,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
494
515
  }
495
516
  get context() {
496
517
  return {
497
- title: this.figure?.name ?? i18next.t('title.new-figure'),
518
+ title: this.figure?.name ?? i18next.t('figure.title.new-figure'),
498
519
  /*
499
520
  **제목 줄은 끈다.** 이름은 그대로 둔다.
500
521
 
@@ -517,7 +538,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
517
538
  하네스 시절의 자리였다 — 앱에서는 셸이 페이지 제목으로 같은 이름을 그리므로 글자가 두 번
518
539
  나오고, 캔버스가 주인공인 화면에서 그만큼 높이를 잃었다.
519
540
  -->
520
- ${this.dirty ? html `<span dirty>${i18next.t('text.unsaved-changes')}</span>` : ''}
541
+ ${this.dirty ? html `<span dirty>${i18next.t('figure.text.unsaved-changes')}</span>` : ''}
521
542
  <!--
522
543
  말로 시키는 줄이 머리줄의 남는 폭을 쓴다. 따로 두면 화면 위가 두 줄이 되고, 그 위 줄은
523
544
  이름을 페이지 제목과 겹쳐 적는다.
@@ -527,8 +548,9 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
527
548
  .type=${source?.type ?? ''}
528
549
  @proposed=${(e) => this.receive(e.detail.proposal)}
529
550
  ></figure-ask>
551
+ ${this.saveFailure ? html `<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
530
552
  <button save ?disabled=${!this.canSave} @click=${() => this.save()}>
531
- ${this.saving ? i18next.t('text.saving') : i18next.t('button.save')}
553
+ ${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
532
554
  </button>
533
555
  </div>
534
556
 
@@ -537,6 +559,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
537
559
  ?collapse-left=${this.collapsed.left}
538
560
  ?collapse-right=${this.collapsed.right}
539
561
  @figure-rename=${(e) => this.rename(e.detail.name)}
562
+ @figure-catalog=${(e) => this.catalog(e.detail)}
540
563
  @figure-released=${(e) => this.released(e.detail.figure)}
541
564
  @figure-reverted=${() => this.figure?.id && this.load(this.figure.id)}
542
565
  @add-part=${(e) => this.addPart(e.detail.primitive)}
@@ -550,6 +573,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
550
573
  @toggle-part-hidden=${(e) => this.toggleHidden(e.detail.index)}
551
574
  @set-all-parts-hidden=${(e) => this.setAllHidden(e.detail.hidden)}
552
575
  @change-detail-level=${(e) => this.changeDetailLevel(e.detail.level)}
576
+ @update-board=${(e) => this.updateBoard(e.detail.board)}
553
577
  @part-tool=${(e) => this.runTool(e.detail.index, e.detail.tool, e.detail.axis)}
554
578
  @pick-part=${(e) => this.pickPart(e.detail.name)}
555
579
  >
@@ -566,7 +590,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
566
590
  ? html `
567
591
  <div lanes>
568
592
  <div lane>
569
- <h4>${i18next.t('label.now')}</h4>
593
+ <h4>${i18next.t('figure.label.now')}</h4>
570
594
  <figure-canvas
571
595
  .board=${this.board}
572
596
  .parts=${this.parts}
@@ -576,11 +600,11 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
576
600
  </div>
577
601
  <div lane candidate>
578
602
  <h4>
579
- ${i18next.t('label.proposed')}
603
+ ${i18next.t('figure.label.proposed')}
580
604
  ${this.proposalOf
581
605
  ? html `<small>
582
606
  ${this.proposalOf.grade} ·
583
- ${i18next.t('text.n-triangles', { count: this.proposalOf.triangles })}
607
+ ${i18next.t('figure.text.n-triangles', { count: this.proposalOf.triangles })}
584
608
  </small>`
585
609
  : ''}
586
610
  </h4>
@@ -599,6 +623,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
599
623
 
600
624
  <figure-side
601
625
  .figureName=${this.figure?.name ?? ''}
626
+ .figureCategory=${this.figure?.category}
627
+ .figureTags=${this.figure?.tags}
602
628
  .figureId=${this.figure?.id ?? ''}
603
629
  .figureState=${this.figure?.state}
604
630
  .figureVersion=${this.figure?.version ?? 0}
@@ -635,10 +661,10 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
635
661
  <button
636
662
  mode
637
663
  ?on=${this.mode === mode}
638
- title=${i18next.t('text.mode-' + mode + '-explained')}
664
+ title=${i18next.t('figure.text.mode-' + mode + '-explained')}
639
665
  @click=${() => (this.mode = mode)}
640
666
  >
641
- ${i18next.t('label.mode-' + mode)}
667
+ ${i18next.t('figure.label.mode-' + mode)}
642
668
  </button>
643
669
  `;
644
670
  return html `<div modes>
@@ -661,7 +687,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
661
687
  return html `
662
688
  <button
663
689
  collapse
664
- title=${i18next.t(`text.${gone ? 'expand' : 'collapse'}-${pane}-pane`)}
690
+ title=${i18next.t(`figure.text.${gone ? 'expand' : 'collapse'}-${pane}-pane`)}
665
691
  @click=${() => this.toggleCollapse(side)}
666
692
  >
667
693
  <md-icon>${gone ? back : shut}</md-icon>
@@ -706,11 +732,11 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
706
732
  const { errors, violations } = validate(picked);
707
733
  return html `
708
734
  <div decide>
709
- <span>${i18next.t('text.assistant-proposed-a-figure')}</span>
735
+ <span>${i18next.t('figure.text.assistant-proposed-a-figure')}</span>
710
736
  <div spacer></div>
711
- <button drop @click=${() => this.discard()}>${i18next.t('button.discard')}</button>
737
+ <button drop @click=${() => this.discard()}>${i18next.t('figure.button.discard')}</button>
712
738
  <button take ?disabled=${errors.length > 0 || this.picked.size === 0} @click=${() => this.take()}>
713
- ${i18next.t('button.take-n-changes', { n: this.picked.size })}
739
+ ${i18next.t('figure.button.take-n-changes', { n: this.picked.size })}
714
740
  </button>
715
741
  </div>
716
742
 
@@ -722,7 +748,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
722
748
  `
723
749
  : ''}
724
750
  ${errors.length > 0
725
- ? html `<p blocked>${i18next.t('text.picked-changes-do-not-validate')} — ${errors[0].message}</p>`
751
+ ? html `<p blocked>${i18next.t('figure.text.picked-changes-do-not-validate')} — ${errors[0].message}</p>`
726
752
  : violations.length > 0
727
753
  ? html `<p blocked warn>${violations[0].message}</p>`
728
754
  : ''}
@@ -744,7 +770,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
744
770
  <button
745
771
  change
746
772
  ?on=${on}
747
- title=${i18next.t('text.change-' + change.kind)}
773
+ title=${i18next.t('figure.text.change-' + change.kind)}
748
774
  @click=${() => this.toggle(change.key)}
749
775
  >
750
776
  <md-icon>${icon}</md-icon>
@@ -804,9 +830,18 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
804
830
  *
805
831
  * 형식을 어긴 원본 데이터로는 저장을 누를 수 없게 한다. 눌렀다가 서버가 받아주지 않는 것보다,
806
832
  * 누를 수 없는 이유가 판단 패널에 떠 있는 것이 낫다.
833
+ *
834
+ * ## 바뀐 것이 없으면 누를 수 없다
835
+ *
836
+ * **`dirty` 를 안 보고 있었다.** 저장한 직후에도 단추가 살아 있어 같은 정본을 몇 번이고 다시 쓸
837
+ * 수 있었고, 저장마다 썸네일을 새로 찍어 20KB 를 다시 넣고 `thumbnailUpdatedAt` 을 올렸다 —
838
+ * 그림 주소의 판이 바뀌므로 브라우저 캐시도 함께 버려진다. 바뀐 것이 없는데 일어날 일이 아니다.
839
+ *
840
+ * 새로 만드는 중인 것은 `startNew` 가 `dirty` 로 시작하므로 첫 저장이 막히지 않는다.
841
+ * 저장이 거절되면 `dirty` 를 그대로 두므로 다시 누를 수 있다.
807
842
  */
808
843
  get canSave() {
809
- if (this.saving || !this.folded || !this.figure?.name) {
844
+ if (this.saving || !this.dirty || !this.folded || !this.figure?.name) {
810
845
  return false;
811
846
  }
812
847
  return validate(this.folded).errors.length === 0;
@@ -816,18 +851,51 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
816
851
  return;
817
852
  }
818
853
  const id = lifecycle?.resourceId;
819
- if (id && id !== this.figure?.id) {
820
- await this.load(id);
854
+ if (id) {
855
+ if (id !== this.figure?.id) {
856
+ await this.load(id);
857
+ }
858
+ return;
859
+ }
860
+ /*
861
+ ── id 없이 들어온 것은 「새로 만든다」는 뜻이다 ─────────────────────────────
862
+ 전에는 `!this.figure` 일 때만 새로 시작했다. 셸은 페이지를 살려 두므로(MDI 탭) 카탈로그에서
863
+ 하나 열어 본 뒤에는 이 페이지가 그 도형을 계속 들고 있고, 그래서 「새 figure」가 **아무 일도
864
+ 하지 않았다.**
865
+
866
+ 그것보다 나쁜 것이 있었다 — 화면에는 옛 도형이 그대로 있으니 사람은 새 것인 줄 알고 고치고
867
+ 저장한다. 그러면 **열려 있던 도형에 덮어쓴다.** 「안 열린다」로 보이던 것이 실은 그 문이었다.
868
+
869
+ 들고 있는 것이 **저장된 도형이면**(id 가 있으면) 새로 시작한다. 아직 저장 안 한 새 도형을
870
+ 들고 있으면 그대로 둔다 — 페이지를 다시 들렀다고 저작 중인 것을 버리지 않는다.
871
+ */
872
+ if (!this.figure) {
873
+ this.startNew();
874
+ return;
821
875
  }
822
- else if (!id && !this.figure) {
876
+ if (this.figure.id) {
877
+ /*
878
+ 고치던 것이 있으면 묻는다. 저작한 것을 말없이 버리지 않는다 — 되돌릴 수 없다.
879
+ 그만두면 보고 있던 도형의 주소로 되돌려 화면과 주소가 어긋나지 않게 한다.
880
+ */
881
+ if (this.dirty && !confirm(i18next.t('figure.text.discard-changes-and-start-new'))) {
882
+ navigate(`figure-modeller/${this.figure.id}`, true);
883
+ return;
884
+ }
823
885
  this.startNew();
824
886
  }
825
887
  }
826
888
  async load(id) {
827
889
  const figure = await fetchFigure(id);
828
890
  this.figure = figure;
829
- /* 그림이 없으면 이 자리에서 채운다(아래 `fillThumbnail`). 기다리지 않는다 — 화면이 먼저다. */
830
- if (!figure.thumbnail && figure.id) {
891
+ /*
892
+ 그림이 없으면 이 자리에서 채운다(아래 `fillThumbnail`). 기다리지 않는다 — 화면이 먼저다.
893
+
894
+ **`thumbnail` 이 아니라 `thumbnailUpdatedAt` 으로 판정한다.** 그림 자체는 목록·낱개 질의가
895
+ 가져오지 않는다(base64 라서 무겁고 캐시가 안 된다 — 주소로 부른다). 없는 칸으로 판정하면
896
+ 늘 「그림이 없다」가 되어 저작면을 열 때마다 다시 찍어 저장하게 된다.
897
+ */
898
+ if (!figure.thumbnailUpdatedAt && figure.id) {
831
899
  void this.fillThumbnail(figure.id);
832
900
  }
833
901
  // 서버가 준 것을 그대로 쓴다. 읽지 못하면 오류보고없이 빈 상태로 바꾸지 않는다.
@@ -857,7 +925,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
857
925
  this.figure = { id: '', type: draftType, name: '' };
858
926
  this.board = board;
859
927
  this.parts = parts;
860
- this.selected = 0;
928
+ /* 고른 것이 없다 — 부품이 없으니 속성 패널도 고를 것을 기다린다. */
929
+ this.selected = -1;
861
930
  this.dirty = true;
862
931
  this.violations = [];
863
932
  }
@@ -867,6 +936,18 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
867
936
  this.figure = { ...this.figure, name };
868
937
  this.dirty = true;
869
938
  }
939
+ /**
940
+ * 분류와 태그를 고친다.
941
+ *
942
+ * 정본(`source`)이 아니라 **카탈로그의 값**이다 — 도형이 무엇으로 생겼는지가 아니라 어디에
943
+ * 걸리는지다. 그래서 점수도 발행도 흔들리지 않는다.
944
+ */
945
+ catalog(patch) {
946
+ if (!this.figure)
947
+ return;
948
+ this.figure = { ...this.figure, ...patch };
949
+ this.dirty = true;
950
+ }
870
951
  /**
871
952
  * 부품 목록을 갈아 끼운다.
872
953
  *
@@ -896,6 +977,16 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
896
977
  updatePart(index, part) {
897
978
  this.put(edits.updatePart(this.parts, index, part));
898
979
  }
980
+ /**
981
+ * 판 자체의 값을 갈아 끼운다 — 능력처럼 부품보다 위에 있는 것들.
982
+ *
983
+ * 부품 목록은 건드리지 않는다. `put` 은 부품을 함께 받으므로 여기서 쓰면 능력 하나를
984
+ * 고칠 때마다 부품 목록이 새 배열이 되고, 그리는 쪽이 전부 다시 그린다.
985
+ */
986
+ updateBoard(board) {
987
+ this.board = board;
988
+ this.dirty = true;
989
+ }
899
990
  /**
900
991
  * 디테일 등급을 바꾼다.
901
992
  *
@@ -1019,8 +1110,9 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1019
1110
  return;
1020
1111
  }
1021
1112
  try {
1022
- await updateFigure(id, { thumbnail });
1023
- this.figure = { ...this.figure, thumbnail };
1113
+ const { figure } = await updateFigure(id, { thumbnail });
1114
+ /* 서버가 찍은 시각을 그대로 받는다 — 이 값이 그림을 부르는 주소의 판 번호가 된다. */
1115
+ this.figure = { ...this.figure, thumbnailUpdatedAt: figure?.thumbnailUpdatedAt };
1024
1116
  }
1025
1117
  catch (e) {
1026
1118
  /* 볼 권한만 있는 사람일 수 있다. 화면은 그대로 두고 조용히 넘어간다. */
@@ -1032,6 +1124,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1032
1124
  return;
1033
1125
  }
1034
1126
  this.saving = true;
1127
+ this.saveFailure = '';
1035
1128
  try {
1036
1129
  const sourceJson = JSON.stringify(this.folded);
1037
1130
  /*
@@ -1046,6 +1139,12 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1046
1139
  if (this.figure.id) {
1047
1140
  const result = await updateFigure(this.figure.id, {
1048
1141
  name: this.figure.name,
1142
+ /*
1143
+ 카탈로그의 값도 함께 보낸다. 화면에서 고쳐 놓고 저장에 안 실으면 저장 뒤에 조용히
1144
+ 사라진다 — 저작자는 저장했다고 보고 다시 열면 없다.
1145
+ */
1146
+ category: this.figure.category,
1147
+ tags: this.figure.tags,
1049
1148
  source: sourceJson,
1050
1149
  thumbnail
1051
1150
  });
@@ -1057,11 +1156,13 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1057
1156
  // 만든 뒤에는 고칠 수 없으므로 여기서 걸러야 한다.
1058
1157
  const taken = await fetchFigureTypeNames();
1059
1158
  if (taken.includes(this.folded.type)) {
1060
- throw new Error(i18next.t('text.type-name-already-taken', { type: this.folded.type }));
1159
+ throw new Error(i18next.t('figure.text.type-name-already-taken', { type: this.folded.type }));
1061
1160
  }
1062
1161
  const result = await createFigure({
1063
1162
  type: this.folded.type,
1064
1163
  name: this.figure.name,
1164
+ category: this.figure.category,
1165
+ tags: this.figure.tags,
1065
1166
  source: sourceJson,
1066
1167
  thumbnail
1067
1168
  });
@@ -1071,6 +1172,15 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1071
1172
  }
1072
1173
  this.dirty = false;
1073
1174
  }
1175
+ catch (e) {
1176
+ /*
1177
+ **서버가 말한 사유를 그대로 올린다.** 여기서 문장을 다시 지으면 무엇이 걸렸는지 알 수 없다 —
1178
+ 타입 이름이 이미 쓰였다는 것과 권한이 없다는 것은 저작자가 할 일이 다르다.
1179
+
1180
+ `dirty` 는 건드리지 않는다. 저장이 안 됐으므로 미저장인 것이 사실이다.
1181
+ */
1182
+ this.saveFailure = e.message;
1183
+ }
1074
1184
  finally {
1075
1185
  this.saving = false;
1076
1186
  }
@@ -1116,6 +1226,10 @@ __decorate([
1116
1226
  state(),
1117
1227
  __metadata("design:type", Array)
1118
1228
  ], FigureModellerPage.prototype, "violations", void 0);
1229
+ __decorate([
1230
+ state(),
1231
+ __metadata("design:type", Object)
1232
+ ], FigureModellerPage.prototype, "saveFailure", void 0);
1119
1233
  __decorate([
1120
1234
  state(),
1121
1235
  __metadata("design:type", Object)