@things-factory/figure-ui 10.1.14 → 10.1.15
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/client/modeller/figure-ai-target.ts +12 -0
- package/client/modeller/figure-canvas.ts +18 -2
- package/client/modeller/figure-inspector.ts +48 -7
- package/client/modeller/figure-side.ts +1 -0
- package/client/modeller/figure-source.test.ts +2 -0
- package/client/modeller/figure-source.ts +17 -21
- package/client/pages/figure-modeller-page.ts +323 -46
- package/dist-client/modeller/figure-ai-target.d.ts +10 -0
- package/dist-client/modeller/figure-ai-target.js +10 -0
- package/dist-client/modeller/figure-ai-target.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +1 -0
- package/dist-client/modeller/figure-canvas.js +21 -2
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +4 -1
- package/dist-client/modeller/figure-inspector.js +47 -6
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-side.js +1 -0
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +15 -21
- package/dist-client/modeller/figure-source.js +5 -0
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +11 -0
- package/dist-client/pages/figure-modeller-page.js +337 -49
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/ai-proposal-contract.test.ts +3 -3
- package/translations/en.json +10 -1
- package/translations/ko.json +11 -1
|
@@ -8,10 +8,12 @@ import '../modeller/figure-preview.js';
|
|
|
8
8
|
import { shouldStartNewFigure } from '../modeller/new-figure-entry.js';
|
|
9
9
|
import '../modeller/figure-parts.js';
|
|
10
10
|
import '../modeller/figure-inspector.js';
|
|
11
|
-
import { css, html } from 'lit';
|
|
11
|
+
import { css, html, nothing } from 'lit';
|
|
12
12
|
import { customElement, state } from 'lit/decorators.js';
|
|
13
13
|
import { i18next, localize } from '@operato/i18n';
|
|
14
14
|
import { navigate, PageView } from '@operato/shell';
|
|
15
|
+
import { openOverlay } from '@operato/layout';
|
|
16
|
+
import { requestFigureAI, consumePendingFigureProposal } from '../modeller/figure-ai-target.js';
|
|
15
17
|
import { validate } from '@hatiolab/figure-model';
|
|
16
18
|
import * as edits from '../modeller/part-edits.js';
|
|
17
19
|
import * as proposals from '../modeller/proposal.js';
|
|
@@ -128,10 +130,15 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
128
130
|
* 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
|
|
129
131
|
*/
|
|
130
132
|
this.saveFailure = '';
|
|
133
|
+
this.showSaveModal = false;
|
|
134
|
+
this.saveModalType = '';
|
|
135
|
+
this.saveModalName = '';
|
|
136
|
+
this.saveModalError = '';
|
|
131
137
|
/** 인라인 요청창과 AI 도크가 함께 쓰는 현재 Figure의 제한된 세션 피드백. */
|
|
132
138
|
this.proposalFeedback = [];
|
|
133
139
|
this.undoStack = [];
|
|
134
140
|
this.redoStack = [];
|
|
141
|
+
this.sceneRevision = 0;
|
|
135
142
|
this.lastHistoryTime = 0;
|
|
136
143
|
this.announceAiContext = () => {
|
|
137
144
|
if (!this.active)
|
|
@@ -142,14 +149,35 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
142
149
|
};
|
|
143
150
|
this.acceptDockProposal = (event) => {
|
|
144
151
|
const detail = event.detail;
|
|
145
|
-
if (!
|
|
152
|
+
if (!detail)
|
|
146
153
|
return;
|
|
147
|
-
if (detail?.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded)) {
|
|
155
|
+
try {
|
|
156
|
+
const parsedBase = typeof detail.baseSource === 'string' ? JSON.parse(detail.baseSource) : detail.baseSource;
|
|
157
|
+
if (parsedBase?.type && this.folded?.type && parsedBase.type !== this.folded.type) {
|
|
158
|
+
// type 불일치 시에만 차단하고 그 외 미세 차이는 수용
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// ignore
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined);
|
|
166
|
+
if (source) {
|
|
167
|
+
this.applySourceDirectly(source, detail.proposal);
|
|
168
|
+
detail.accepted = true;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
this.onFigureAiStage = (event) => {
|
|
172
|
+
const detail = event.detail;
|
|
173
|
+
const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined);
|
|
174
|
+
if (source) {
|
|
175
|
+
this.applySourceDirectly(source, detail.proposal);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
this.onSaveRequest = () => {
|
|
179
|
+
if (this.active)
|
|
180
|
+
this.save();
|
|
153
181
|
};
|
|
154
182
|
this.handleKeyDown = (e) => {
|
|
155
183
|
if (!this.active || this.mode !== 'edit' || this.proposalSession)
|
|
@@ -385,6 +413,27 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
385
413
|
opacity: 0.35;
|
|
386
414
|
cursor: default;
|
|
387
415
|
}
|
|
416
|
+
div[modes] button[ai-support] {
|
|
417
|
+
display: inline-flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
gap: 4px;
|
|
420
|
+
padding: 3px 10px;
|
|
421
|
+
margin-right: 6px;
|
|
422
|
+
font: var(--label-font, inherit);
|
|
423
|
+
font-size: 0.74rem;
|
|
424
|
+
font-weight: 600;
|
|
425
|
+
color: var(--md-sys-color-primary);
|
|
426
|
+
background: var(--md-sys-color-primary-container);
|
|
427
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
428
|
+
border-radius: 6px;
|
|
429
|
+
cursor: pointer;
|
|
430
|
+
}
|
|
431
|
+
div[modes] button[ai-support]:hover {
|
|
432
|
+
background-color: var(--md-sys-color-surface-container-high);
|
|
433
|
+
}
|
|
434
|
+
div[modes] button[ai-support] md-icon {
|
|
435
|
+
--md-icon-size: 15px;
|
|
436
|
+
}
|
|
388
437
|
|
|
389
438
|
/*
|
|
390
439
|
후보가 오면 가운데를 둘로 나눈다.
|
|
@@ -571,6 +620,86 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
571
620
|
line-height: 1.5;
|
|
572
621
|
color: var(--md-sys-color-error);
|
|
573
622
|
}
|
|
623
|
+
.save-modal-backdrop {
|
|
624
|
+
position: fixed;
|
|
625
|
+
inset: 0;
|
|
626
|
+
background: rgba(0, 0, 0, 0.5);
|
|
627
|
+
backdrop-filter: blur(2px);
|
|
628
|
+
display: flex;
|
|
629
|
+
align-items: center;
|
|
630
|
+
justify-content: center;
|
|
631
|
+
z-index: 1000;
|
|
632
|
+
}
|
|
633
|
+
.save-modal-card {
|
|
634
|
+
background: var(--md-sys-color-surface-container-high, #fff);
|
|
635
|
+
color: var(--md-sys-color-on-surface, #1f1f1f);
|
|
636
|
+
border-radius: 12px;
|
|
637
|
+
padding: 24px;
|
|
638
|
+
width: 380px;
|
|
639
|
+
max-width: 90vw;
|
|
640
|
+
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.28);
|
|
641
|
+
display: flex;
|
|
642
|
+
flex-direction: column;
|
|
643
|
+
gap: 12px;
|
|
644
|
+
}
|
|
645
|
+
.save-modal-title {
|
|
646
|
+
margin: 0;
|
|
647
|
+
font-size: 1.15rem;
|
|
648
|
+
font-weight: 700;
|
|
649
|
+
}
|
|
650
|
+
.save-modal-desc {
|
|
651
|
+
margin: 0;
|
|
652
|
+
font-size: 0.8rem;
|
|
653
|
+
color: var(--md-sys-color-on-surface-variant, #666);
|
|
654
|
+
}
|
|
655
|
+
.save-modal-field {
|
|
656
|
+
display: flex;
|
|
657
|
+
flex-direction: column;
|
|
658
|
+
gap: 4px;
|
|
659
|
+
}
|
|
660
|
+
.save-modal-field label {
|
|
661
|
+
font-size: 0.76rem;
|
|
662
|
+
font-weight: 600;
|
|
663
|
+
color: var(--md-sys-color-on-surface-variant, #555);
|
|
664
|
+
}
|
|
665
|
+
.save-modal-field input {
|
|
666
|
+
padding: 8px 10px;
|
|
667
|
+
border: 1px solid var(--md-sys-color-outline, #ccc);
|
|
668
|
+
border-radius: 6px;
|
|
669
|
+
font-size: 0.88rem;
|
|
670
|
+
background: var(--md-sys-color-surface, #fff);
|
|
671
|
+
color: inherit;
|
|
672
|
+
}
|
|
673
|
+
.save-modal-error {
|
|
674
|
+
color: var(--md-sys-color-error, #b00020);
|
|
675
|
+
font-size: 0.78rem;
|
|
676
|
+
}
|
|
677
|
+
.save-modal-actions {
|
|
678
|
+
display: flex;
|
|
679
|
+
justify-content: flex-end;
|
|
680
|
+
gap: 8px;
|
|
681
|
+
margin-top: 8px;
|
|
682
|
+
}
|
|
683
|
+
.save-modal-actions button {
|
|
684
|
+
padding: 6px 14px;
|
|
685
|
+
border-radius: 6px;
|
|
686
|
+
font-size: 0.82rem;
|
|
687
|
+
cursor: pointer;
|
|
688
|
+
border: none;
|
|
689
|
+
}
|
|
690
|
+
.save-modal-actions .btn-cancel {
|
|
691
|
+
background: none;
|
|
692
|
+
color: var(--md-sys-color-on-surface, inherit);
|
|
693
|
+
}
|
|
694
|
+
.save-modal-actions .btn-confirm {
|
|
695
|
+
background: var(--md-sys-color-primary, #0066cc);
|
|
696
|
+
color: var(--md-sys-color-on-primary, #fff);
|
|
697
|
+
font-weight: 600;
|
|
698
|
+
}
|
|
699
|
+
.save-modal-actions .btn-confirm:disabled {
|
|
700
|
+
opacity: 0.5;
|
|
701
|
+
cursor: not-allowed;
|
|
702
|
+
}
|
|
574
703
|
`; }
|
|
575
704
|
willUpdate(changed) {
|
|
576
705
|
if (changed.has('board') || changed.has('parts')) {
|
|
@@ -598,14 +727,44 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
598
727
|
}));
|
|
599
728
|
}
|
|
600
729
|
}
|
|
730
|
+
applySourceDirectly(rawSource, proposal) {
|
|
731
|
+
const source = typeof rawSource === 'string' ? JSON.parse(rawSource) : rawSource;
|
|
732
|
+
if (!source || !Array.isArray(source.parts))
|
|
733
|
+
return;
|
|
734
|
+
this.recordHistory();
|
|
735
|
+
try {
|
|
736
|
+
const { board, parts } = fromFigureSource(source);
|
|
737
|
+
this.board = board;
|
|
738
|
+
this.parts = parts;
|
|
739
|
+
this.dirty = true;
|
|
740
|
+
this.selected = parts.length ? 0 : -1;
|
|
741
|
+
if (source.type) {
|
|
742
|
+
this.figure = { ...(this.figure || {}), type: source.type };
|
|
743
|
+
if (this.board) {
|
|
744
|
+
this.board = { ...this.board, figureType: source.type };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
this.proposalSession = undefined;
|
|
748
|
+
this.sceneRevision++;
|
|
749
|
+
this.requestUpdate();
|
|
750
|
+
document.dispatchEvent(new CustomEvent('notify', { detail: { message: `${source.type || 'Figure'} 모델이 3D 뷰어에 반영되었습니다.` } }));
|
|
751
|
+
}
|
|
752
|
+
catch (e) {
|
|
753
|
+
console.error('[FigureModeller] Failed to apply AI proposal source:', e);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
601
756
|
connectedCallback() {
|
|
602
757
|
super.connectedCallback();
|
|
603
758
|
window.addEventListener('figure-ai-context-request', this.announceAiContext);
|
|
604
759
|
window.addEventListener('figure-ai-review', this.acceptDockProposal);
|
|
760
|
+
window.addEventListener('figure-ai-stage', this.onFigureAiStage);
|
|
761
|
+
window.addEventListener('figure-save-request', this.onSaveRequest);
|
|
605
762
|
window.addEventListener('keydown', this.handleKeyDown);
|
|
606
763
|
}
|
|
607
764
|
disconnectedCallback() {
|
|
608
765
|
window.removeEventListener('keydown', this.handleKeyDown);
|
|
766
|
+
window.removeEventListener('figure-save-request', this.onSaveRequest);
|
|
767
|
+
window.removeEventListener('figure-ai-stage', this.onFigureAiStage);
|
|
609
768
|
window.removeEventListener('figure-ai-context-request', this.announceAiContext);
|
|
610
769
|
window.removeEventListener('figure-ai-review', this.acceptDockProposal);
|
|
611
770
|
window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }));
|
|
@@ -635,6 +794,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
635
794
|
?collapse-left=${this.collapsed.left}
|
|
636
795
|
?collapse-right=${this.collapsed.right}
|
|
637
796
|
@figure-rename=${(e) => this.rename(e.detail.name)}
|
|
797
|
+
@figure-type-change=${(e) => this.typeChanged(e.detail.type)}
|
|
638
798
|
@figure-catalog=${(e) => this.catalog(e.detail)}
|
|
639
799
|
@figure-released=${(e) => this.released(e.detail.figure)}
|
|
640
800
|
@figure-reverted=${() => this.figure?.id && this.load(this.figure.id)}
|
|
@@ -675,6 +835,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
675
835
|
.parts=${this.parts}
|
|
676
836
|
.picked=${this.pickedName}
|
|
677
837
|
.view=${this.view}
|
|
838
|
+
.revision=${this.sceneRevision}
|
|
678
839
|
></figure-canvas>
|
|
679
840
|
</div>
|
|
680
841
|
<div lane candidate>
|
|
@@ -700,6 +861,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
700
861
|
.parts=${this.parts}
|
|
701
862
|
.picked=${this.pickedName}
|
|
702
863
|
.view=${this.view}
|
|
864
|
+
.revision=${this.sceneRevision}
|
|
703
865
|
></figure-canvas>`}
|
|
704
866
|
</div>
|
|
705
867
|
|
|
@@ -719,6 +881,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
719
881
|
@update-view=${(e) => (this.view = e.detail.view)}
|
|
720
882
|
></figure-side>
|
|
721
883
|
</div>
|
|
884
|
+
${this.renderSaveModal()}
|
|
722
885
|
`;
|
|
723
886
|
}
|
|
724
887
|
/**
|
|
@@ -773,6 +936,17 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
773
936
|
`
|
|
774
937
|
: ''}
|
|
775
938
|
<div spacer></div>
|
|
939
|
+
<button
|
|
940
|
+
ai-support
|
|
941
|
+
title=${i18next.t('figure.button.ask')}
|
|
942
|
+
@click=${() => {
|
|
943
|
+
requestFigureAI({ figureId: this.figure?.id, source: this.folded });
|
|
944
|
+
openOverlay('figure-ai-dock', { backdrop: false });
|
|
945
|
+
}}
|
|
946
|
+
>
|
|
947
|
+
<md-icon>auto_awesome</md-icon>
|
|
948
|
+
<span>AI</span>
|
|
949
|
+
</button>
|
|
776
950
|
${this.saveFailure ? html `<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
|
|
777
951
|
${this.dirty ? html `<span dirty>${i18next.t('figure.text.unsaved-changes')}</span>` : ''}
|
|
778
952
|
<button save ?disabled=${!this.canSave} @click=${() => this.save()}>
|
|
@@ -1021,7 +1195,11 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1021
1195
|
* 저장이 거절되면 `dirty` 를 그대로 두므로 다시 누를 수 있다.
|
|
1022
1196
|
*/
|
|
1023
1197
|
get canSave() {
|
|
1024
|
-
if (this.saving || !this.dirty || !this.folded
|
|
1198
|
+
if (this.saving || !this.dirty || !this.folded) {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
// 기존 저장된 모델은 이름이 있어야 바로 저장 가능. 신규 모델은 저장 팝업에서 이름을 입력할 수 있음
|
|
1202
|
+
if (this.figure?.id && !this.figure?.name) {
|
|
1025
1203
|
return false;
|
|
1026
1204
|
}
|
|
1027
1205
|
return validate(this.folded).errors.length === 0;
|
|
@@ -1036,6 +1214,10 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1036
1214
|
if (changes.active || id !== this.figure?.id) {
|
|
1037
1215
|
await this.load(id);
|
|
1038
1216
|
}
|
|
1217
|
+
const pending = consumePendingFigureProposal();
|
|
1218
|
+
if (pending?.source) {
|
|
1219
|
+
this.applySourceDirectly(pending.source, pending.proposal);
|
|
1220
|
+
}
|
|
1039
1221
|
return;
|
|
1040
1222
|
}
|
|
1041
1223
|
/*
|
|
@@ -1052,16 +1234,32 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1052
1234
|
*/
|
|
1053
1235
|
if (!this.figure) {
|
|
1054
1236
|
this.startNew();
|
|
1237
|
+
const pending = consumePendingFigureProposal();
|
|
1238
|
+
if (pending?.source) {
|
|
1239
|
+
this.applySourceDirectly(pending.source, pending.proposal);
|
|
1240
|
+
}
|
|
1055
1241
|
return;
|
|
1056
1242
|
}
|
|
1057
1243
|
if (this.figure.id) {
|
|
1058
1244
|
this.startNew();
|
|
1245
|
+
const pending = consumePendingFigureProposal();
|
|
1246
|
+
if (pending?.source) {
|
|
1247
|
+
this.applySourceDirectly(pending.source, pending.proposal);
|
|
1248
|
+
}
|
|
1059
1249
|
return;
|
|
1060
1250
|
}
|
|
1061
1251
|
if (shouldStartNewFigure(changes, !!this.figure, this.figure?.id)) {
|
|
1062
1252
|
this.startNew();
|
|
1253
|
+
const pending = consumePendingFigureProposal();
|
|
1254
|
+
if (pending?.source) {
|
|
1255
|
+
this.applySourceDirectly(pending.source, pending.proposal);
|
|
1256
|
+
}
|
|
1063
1257
|
return;
|
|
1064
1258
|
}
|
|
1259
|
+
const pending = consumePendingFigureProposal();
|
|
1260
|
+
if (pending?.source) {
|
|
1261
|
+
this.applySourceDirectly(pending.source, pending.proposal);
|
|
1262
|
+
}
|
|
1065
1263
|
}
|
|
1066
1264
|
async load(id) {
|
|
1067
1265
|
const generation = ++this.loadGeneration;
|
|
@@ -1130,6 +1328,16 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1130
1328
|
this.figure = { ...this.figure, name };
|
|
1131
1329
|
this.dirty = true;
|
|
1132
1330
|
}
|
|
1331
|
+
typeChanged(type) {
|
|
1332
|
+
if (!type || this.figure?.id)
|
|
1333
|
+
return;
|
|
1334
|
+
const sanitized = type.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase();
|
|
1335
|
+
if (this.board)
|
|
1336
|
+
this.board.figureType = sanitized;
|
|
1337
|
+
this.figure = { ...this.figure, type: sanitized };
|
|
1338
|
+
this.folded = this.board ? toFigureSource(this.board, this.parts) : undefined;
|
|
1339
|
+
this.dirty = true;
|
|
1340
|
+
}
|
|
1133
1341
|
/**
|
|
1134
1342
|
* 분류와 태그를 고친다.
|
|
1135
1343
|
*
|
|
@@ -1358,48 +1566,50 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1358
1566
|
console.warn(`[thumbnail] ${id} 를 채우지 못했습니다 — ${e.message}`);
|
|
1359
1567
|
}
|
|
1360
1568
|
}
|
|
1361
|
-
|
|
1569
|
+
save() {
|
|
1362
1570
|
if (!this.canSave || !this.folded || !this.figure) {
|
|
1363
1571
|
return;
|
|
1364
1572
|
}
|
|
1573
|
+
if (!this.figure.id) {
|
|
1574
|
+
// 신규 저장: 타입 코드와 표시 이름을 명확히 팝업으로 확인/입력받음
|
|
1575
|
+
let defaultType = this.board?.figureType || this.figure.type || '';
|
|
1576
|
+
if (!defaultType || defaultType.startsWith('FIGURE_')) {
|
|
1577
|
+
defaultType = 'FIGURE';
|
|
1578
|
+
}
|
|
1579
|
+
this.saveModalType = defaultType;
|
|
1580
|
+
this.saveModalName = this.figure.name || '';
|
|
1581
|
+
this.saveModalError = '';
|
|
1582
|
+
this.showSaveModal = true;
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
void this.doSave();
|
|
1586
|
+
}
|
|
1587
|
+
async doSave() {
|
|
1588
|
+
if (!this.folded || !this.figure)
|
|
1589
|
+
return;
|
|
1365
1590
|
this.saving = true;
|
|
1366
1591
|
this.saveFailure = '';
|
|
1592
|
+
this.saveModalError = '';
|
|
1367
1593
|
try {
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
못 찍으면 보내지 않는다(`undefined`). 빈 그림을 넣어 「그림이 있다」고 말하지 않는다.
|
|
1376
|
-
*/
|
|
1377
|
-
const thumbnail = await captureThumbnail(this.board, this.parts);
|
|
1378
|
-
if (this.figure.id) {
|
|
1379
|
-
const result = await updateFigure(this.figure.id, {
|
|
1380
|
-
name: this.figure.name,
|
|
1381
|
-
/*
|
|
1382
|
-
카탈로그의 값도 함께 보낸다. 화면에서 고쳐 놓고 저장에 안 실으면 저장 뒤에 조용히
|
|
1383
|
-
사라진다 — 저작자는 저장했다고 보고 다시 열면 없다.
|
|
1384
|
-
*/
|
|
1385
|
-
category: this.figure.category,
|
|
1386
|
-
tags: this.figure.tags,
|
|
1387
|
-
source: sourceJson,
|
|
1388
|
-
thumbnail
|
|
1389
|
-
});
|
|
1390
|
-
this.figure = result.figure;
|
|
1391
|
-
this.violations = result.violations;
|
|
1392
|
-
}
|
|
1393
|
-
else {
|
|
1394
|
-
// 새로 만들 때만 타입 이름이 정해진다. 이미 쓰이는 이름이면 저장 전에 막는다 —
|
|
1395
|
-
// 만든 뒤에는 고칠 수 없으므로 여기서 걸러야 한다.
|
|
1594
|
+
if (!this.figure.id) {
|
|
1595
|
+
const typeToSave = (this.saveModalType || this.board?.figureType || this.figure.type || 'FIGURE').trim().toUpperCase();
|
|
1596
|
+
const nameToSave = (this.saveModalName || this.figure.name || typeToSave).trim();
|
|
1597
|
+
if (!typeToSave) {
|
|
1598
|
+
throw new Error('타입 이름을 입력하세요.');
|
|
1599
|
+
}
|
|
1396
1600
|
const taken = await fetchFigureTypeNames();
|
|
1397
|
-
if (taken.includes(
|
|
1398
|
-
throw new Error(i18next.t('figure.text.type-name-already-taken', { type:
|
|
1601
|
+
if (taken.includes(typeToSave)) {
|
|
1602
|
+
throw new Error(i18next.t('figure.text.type-name-already-taken', { type: typeToSave }));
|
|
1399
1603
|
}
|
|
1604
|
+
if (this.board)
|
|
1605
|
+
this.board.figureType = typeToSave;
|
|
1606
|
+
this.figure = { ...this.figure, type: typeToSave, name: nameToSave };
|
|
1607
|
+
this.folded = { ...this.folded, type: typeToSave };
|
|
1608
|
+
const sourceJson = JSON.stringify(this.folded);
|
|
1609
|
+
const thumbnail = await captureThumbnail(this.board, this.parts);
|
|
1400
1610
|
const result = await createFigure({
|
|
1401
|
-
type:
|
|
1402
|
-
name:
|
|
1611
|
+
type: typeToSave,
|
|
1612
|
+
name: nameToSave,
|
|
1403
1613
|
category: this.figure.category,
|
|
1404
1614
|
tags: this.figure.tags,
|
|
1405
1615
|
source: sourceJson,
|
|
@@ -1407,23 +1617,81 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1407
1617
|
});
|
|
1408
1618
|
this.figure = result.figure;
|
|
1409
1619
|
this.violations = result.violations;
|
|
1620
|
+
this.showSaveModal = false;
|
|
1621
|
+
this.dirty = false;
|
|
1410
1622
|
navigate(`figure-modeller/${result.figure.id}`);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
// 기존 모델 업데이트: folded.type이 figure.type과 일치하도록 보장
|
|
1626
|
+
if (this.figure.type && this.folded.type !== this.figure.type) {
|
|
1627
|
+
this.folded = { ...this.folded, type: this.figure.type };
|
|
1628
|
+
if (this.board)
|
|
1629
|
+
this.board.figureType = this.figure.type;
|
|
1411
1630
|
}
|
|
1631
|
+
const sourceJson = JSON.stringify(this.folded);
|
|
1632
|
+
const thumbnail = await captureThumbnail(this.board, this.parts);
|
|
1633
|
+
const result = await updateFigure(this.figure.id, {
|
|
1634
|
+
name: this.figure.name,
|
|
1635
|
+
category: this.figure.category,
|
|
1636
|
+
tags: this.figure.tags,
|
|
1637
|
+
source: sourceJson,
|
|
1638
|
+
thumbnail
|
|
1639
|
+
});
|
|
1640
|
+
this.figure = result.figure;
|
|
1641
|
+
this.violations = result.violations;
|
|
1412
1642
|
this.dirty = false;
|
|
1413
1643
|
}
|
|
1414
1644
|
catch (e) {
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
`dirty` 는 건드리지 않는다. 저장이 안 됐으므로 미저장인 것이 사실이다.
|
|
1420
|
-
*/
|
|
1421
|
-
this.saveFailure = e.message;
|
|
1645
|
+
const msg = e.message;
|
|
1646
|
+
this.saveFailure = msg;
|
|
1647
|
+
this.saveModalError = msg;
|
|
1422
1648
|
}
|
|
1423
1649
|
finally {
|
|
1424
1650
|
this.saving = false;
|
|
1425
1651
|
}
|
|
1426
1652
|
}
|
|
1653
|
+
renderSaveModal() {
|
|
1654
|
+
if (!this.showSaveModal)
|
|
1655
|
+
return nothing;
|
|
1656
|
+
return html `
|
|
1657
|
+
<div class="save-modal-backdrop" @click=${(e) => { if (e.target === e.currentTarget)
|
|
1658
|
+
this.showSaveModal = false; }}>
|
|
1659
|
+
<div class="save-modal-card" role="dialog" aria-modal="true">
|
|
1660
|
+
<h3 class="save-modal-title">${i18next.t('figure.title.save-new-figure', { defaultValue: '도형 초안 저장' })}</h3>
|
|
1661
|
+
<p class="save-modal-desc">${i18next.t('figure.text.save-new-figure-desc', { defaultValue: '보드와 캔버스에서 식별할 고유 타입 코드와 이름을 정합니다.' })}</p>
|
|
1662
|
+
|
|
1663
|
+
<div class="save-modal-field">
|
|
1664
|
+
<label>${i18next.t('figure.label.figure-type', { defaultValue: '타입 코드 (영문 대문자)' })}</label>
|
|
1665
|
+
<input
|
|
1666
|
+
.value=${this.saveModalType}
|
|
1667
|
+
placeholder="예: OHT, AGV_LIFTER"
|
|
1668
|
+
@input=${(e) => (this.saveModalType = e.target.value.trim().toUpperCase())}
|
|
1669
|
+
/>
|
|
1670
|
+
</div>
|
|
1671
|
+
|
|
1672
|
+
<div class="save-modal-field">
|
|
1673
|
+
<label>${i18next.t('figure.label.name', { defaultValue: '도형 이름' })}</label>
|
|
1674
|
+
<input
|
|
1675
|
+
.value=${this.saveModalName}
|
|
1676
|
+
placeholder="예: 오버헤드 호이스트 트랜스포트"
|
|
1677
|
+
@input=${(e) => (this.saveModalName = e.target.value)}
|
|
1678
|
+
/>
|
|
1679
|
+
</div>
|
|
1680
|
+
|
|
1681
|
+
${this.saveModalError ? html `<div class="save-modal-error">${this.saveModalError}</div>` : nothing}
|
|
1682
|
+
|
|
1683
|
+
<div class="save-modal-actions">
|
|
1684
|
+
<button class="btn-cancel" @click=${() => (this.showSaveModal = false)}>
|
|
1685
|
+
${i18next.t('figure.button.cancel', { defaultValue: '취소' })}
|
|
1686
|
+
</button>
|
|
1687
|
+
<button class="btn-confirm" ?disabled=${this.saving || !this.saveModalType} @click=${() => this.doSave()}>
|
|
1688
|
+
${this.saving ? i18next.t('figure.text.saving-figure', { defaultValue: '저장 중...' }) : i18next.t('figure.button.save', { defaultValue: '저장' })}
|
|
1689
|
+
</button>
|
|
1690
|
+
</div>
|
|
1691
|
+
</div>
|
|
1692
|
+
</div>
|
|
1693
|
+
`;
|
|
1694
|
+
}
|
|
1427
1695
|
};
|
|
1428
1696
|
__decorate([
|
|
1429
1697
|
state(),
|
|
@@ -1469,6 +1737,22 @@ __decorate([
|
|
|
1469
1737
|
state(),
|
|
1470
1738
|
__metadata("design:type", Object)
|
|
1471
1739
|
], FigureModellerPage.prototype, "saveFailure", void 0);
|
|
1740
|
+
__decorate([
|
|
1741
|
+
state(),
|
|
1742
|
+
__metadata("design:type", Object)
|
|
1743
|
+
], FigureModellerPage.prototype, "showSaveModal", void 0);
|
|
1744
|
+
__decorate([
|
|
1745
|
+
state(),
|
|
1746
|
+
__metadata("design:type", Object)
|
|
1747
|
+
], FigureModellerPage.prototype, "saveModalType", void 0);
|
|
1748
|
+
__decorate([
|
|
1749
|
+
state(),
|
|
1750
|
+
__metadata("design:type", Object)
|
|
1751
|
+
], FigureModellerPage.prototype, "saveModalName", void 0);
|
|
1752
|
+
__decorate([
|
|
1753
|
+
state(),
|
|
1754
|
+
__metadata("design:type", Object)
|
|
1755
|
+
], FigureModellerPage.prototype, "saveModalError", void 0);
|
|
1472
1756
|
__decorate([
|
|
1473
1757
|
state(),
|
|
1474
1758
|
__metadata("design:type", Object)
|
|
@@ -1485,6 +1769,10 @@ __decorate([
|
|
|
1485
1769
|
state(),
|
|
1486
1770
|
__metadata("design:type", Array)
|
|
1487
1771
|
], FigureModellerPage.prototype, "redoStack", void 0);
|
|
1772
|
+
__decorate([
|
|
1773
|
+
state(),
|
|
1774
|
+
__metadata("design:type", Object)
|
|
1775
|
+
], FigureModellerPage.prototype, "sceneRevision", void 0);
|
|
1488
1776
|
FigureModellerPage = __decorate([
|
|
1489
1777
|
customElement('figure-modeller-page')
|
|
1490
1778
|
], FigureModellerPage);
|