@things-factory/figure-ui 10.1.4 → 10.1.6
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/graphql/index.ts +122 -15
- package/client/modeller/figure-ask.ts +5 -5
- package/client/modeller/figure-canvas.ts +1 -1
- package/client/modeller/figure-capabilities.ts +372 -0
- package/client/modeller/figure-history-panel.ts +477 -0
- package/client/modeller/figure-inspector.ts +392 -76
- package/client/modeller/figure-parts.ts +15 -15
- package/client/modeller/figure-preview.ts +2 -2
- package/client/modeller/figure-report.ts +102 -37
- package/client/modeller/figure-settings.ts +20 -20
- package/client/modeller/figure-side.ts +54 -8
- package/client/modeller/figure-source.test.ts +15 -1
- package/client/modeller/figure-source.ts +31 -15
- package/client/modeller/figure-thumbnail.ts +49 -4
- package/client/modeller/maturity.ts +10 -10
- package/client/pages/figure-list-page.ts +450 -227
- package/client/pages/figure-modeller-page.ts +282 -43
- package/client/types.ts +42 -0
- package/client/viewparts/figure-thumb.ts +172 -0
- package/dist-client/graphql/index.d.ts +31 -4
- package/dist-client/graphql/index.js +102 -12
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-ask.js +5 -5
- package/dist-client/modeller/figure-ask.js.map +1 -1
- package/dist-client/modeller/figure-canvas.js +1 -1
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-capabilities.d.ts +42 -0
- package/dist-client/modeller/figure-capabilities.js +344 -0
- package/dist-client/modeller/figure-capabilities.js.map +1 -0
- package/dist-client/modeller/figure-history-panel.d.ts +56 -0
- package/dist-client/modeller/figure-history-panel.js +490 -0
- package/dist-client/modeller/figure-history-panel.js.map +1 -0
- package/dist-client/modeller/figure-inspector.d.ts +84 -0
- package/dist-client/modeller/figure-inspector.js +379 -75
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-parts.js +15 -15
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +2 -2
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +18 -1
- package/dist-client/modeller/figure-report.js +105 -37
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-settings.js +19 -19
- package/dist-client/modeller/figure-settings.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +7 -0
- package/dist-client/modeller/figure-side.js +66 -7
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +15 -8
- package/dist-client/modeller/figure-source.js +16 -7
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/figure-thumbnail.js +40 -4
- package/dist-client/modeller/figure-thumbnail.js.map +1 -1
- package/dist-client/modeller/maturity.js +10 -10
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/pages/figure-list-page.d.ts +45 -25
- package/dist-client/pages/figure-list-page.js +439 -247
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +54 -0
- package/dist-client/pages/figure-modeller-page.js +279 -43
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +42 -0
- package/dist-client/types.js.map +1 -1
- package/dist-client/viewparts/figure-thumb.d.ts +29 -0
- package/dist-client/viewparts/figure-thumb.js +168 -0
- package/dist-client/viewparts/figure-thumb.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -5
- package/test/i18n-prefix-guard.test.ts +129 -0
- package/test/korean-register-guard.test.ts +83 -0
- package/translations/en.json +307 -249
- package/translations/ja.json +275 -249
- package/translations/ko.json +307 -249
- package/translations/ms.json +275 -249
- package/translations/zh.json +275 -249
- package/client/viewparts/figure-card.ts +0 -226
- package/dist-client/viewparts/figure-card.d.ts +0 -24
- package/dist-client/viewparts/figure-card.js +0 -221
- package/dist-client/viewparts/figure-card.js.map +0 -1
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
3
|
+
import { css, html, LitElement, nothing } from 'lit';
|
|
4
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
5
|
+
import { i18next, localize } from '@operato/i18n';
|
|
6
|
+
import { ScrollbarStyles } from '@operato/styles';
|
|
7
|
+
import { fetchFigureVersions, inspectFigure, releaseFigure, revertFigureVersion } from '../graphql/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* 이력 — **발행하는 자리이자, 발행된 판들을 보는 자리.**
|
|
10
|
+
*
|
|
11
|
+
* ## 왜 저장 옆이 아니라 여기인가
|
|
12
|
+
*
|
|
13
|
+
* 저장은 하루에 수십 번이고 발행은 드물다. 두 단추를 나란히 두면 저장하려다 발행을 누른다.
|
|
14
|
+
* 그리고 발행은 **소비처에게 하는 약속**이라, 무엇을 약속하는지(지금 몇 판인가 · 저장 안 한 것이
|
|
15
|
+
* 있나 · 지난 판이 무엇이었나)를 함께 보여 주는 자리에 있어야 한다.
|
|
16
|
+
*
|
|
17
|
+
* ## 저장하지 않은 것이 있으면 발행하지 않는다
|
|
18
|
+
*
|
|
19
|
+
* 발행은 **저장된 것**을 내보낸다. 화면에 고친 것이 남아 있는 채로 발행하면, 저작자가 보고 있는
|
|
20
|
+
* 것과 약속한 것이 다르다. 그래서 막고, 왜 막혔는지 적는다.
|
|
21
|
+
*
|
|
22
|
+
* ## 되돌리기는 초안으로 돌아온다
|
|
23
|
+
*
|
|
24
|
+
* 옛 판의 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 되살렸다고 그 판이 다시 발행되지는
|
|
25
|
+
* 않는다 — 다시 내보내려면 발행을 한 번 더 하고, 그때 번호는 이미 쓴 다음 번호를 받는다.
|
|
26
|
+
*/
|
|
27
|
+
let FigureHistoryPanel = class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
28
|
+
constructor() {
|
|
29
|
+
super(...arguments);
|
|
30
|
+
this.figureVersion = 0;
|
|
31
|
+
/** 저장하지 않은 것이 있나 — 있으면 발행을 막는다. */
|
|
32
|
+
this.dirty = false;
|
|
33
|
+
this.versions = [];
|
|
34
|
+
this.busy = false;
|
|
35
|
+
this.failure = '';
|
|
36
|
+
}
|
|
37
|
+
static { this.styles = [
|
|
38
|
+
ScrollbarStyles,
|
|
39
|
+
css `
|
|
40
|
+
:host {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
min-height: 0;
|
|
44
|
+
overflow-y: auto;
|
|
45
|
+
background-color: var(--md-sys-color-surface-container-lowest);
|
|
46
|
+
color: var(--md-sys-color-on-surface);
|
|
47
|
+
font: var(--label-font, inherit);
|
|
48
|
+
font-size: 0.76rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:host([off]) {
|
|
52
|
+
display: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
section {
|
|
56
|
+
padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
|
|
57
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
h3 {
|
|
61
|
+
margin: 0 0 6px 0;
|
|
62
|
+
font-size: var(--ui-t-micro, 11px);
|
|
63
|
+
font-weight: 700;
|
|
64
|
+
letter-spacing: 0.04em;
|
|
65
|
+
text-transform: uppercase;
|
|
66
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* 지금 무엇인가 — 발행을 누르기 전에 읽는 한 줄 */
|
|
70
|
+
div[now] {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: 6px;
|
|
74
|
+
margin-bottom: 8px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
span[state] {
|
|
78
|
+
padding: 2px 8px;
|
|
79
|
+
border-radius: 10px;
|
|
80
|
+
font-size: 0.7rem;
|
|
81
|
+
font-weight: 700;
|
|
82
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
83
|
+
background-color: var(--md-sys-color-secondary-container);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
span[state][released] {
|
|
87
|
+
color: var(--md-sys-color-on-primary);
|
|
88
|
+
background-color: var(--md-sys-color-primary);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
span[ver] {
|
|
92
|
+
font-family: var(--mono-font, monospace);
|
|
93
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
input,
|
|
97
|
+
button {
|
|
98
|
+
font: var(--input-field-font, inherit);
|
|
99
|
+
font-size: 0.76rem;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
input[note] {
|
|
103
|
+
width: 100%;
|
|
104
|
+
box-sizing: border-box;
|
|
105
|
+
padding: 5px 8px;
|
|
106
|
+
color: var(--md-sys-color-on-surface);
|
|
107
|
+
background-color: var(--md-sys-color-surface-container-lowest);
|
|
108
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
109
|
+
border-radius: 6px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
button[release] {
|
|
113
|
+
width: 100%;
|
|
114
|
+
margin-top: 6px;
|
|
115
|
+
padding: 6px 0;
|
|
116
|
+
font-weight: 700;
|
|
117
|
+
color: var(--ui-on-accent, #fff);
|
|
118
|
+
background-color: var(--md-sys-color-primary);
|
|
119
|
+
border: 0;
|
|
120
|
+
border-radius: 6px;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
button[release][disabled] {
|
|
125
|
+
opacity: 0.45;
|
|
126
|
+
cursor: default;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* 왜 못 누르는지 — 눌러 보고 알게 하지 않는다 */
|
|
130
|
+
p[why] {
|
|
131
|
+
margin: 6px 0 0 0;
|
|
132
|
+
font-size: 0.7rem;
|
|
133
|
+
line-height: 1.5;
|
|
134
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
p[failed] {
|
|
138
|
+
margin: 6px 0 0 0;
|
|
139
|
+
font-size: 0.7rem;
|
|
140
|
+
line-height: 1.5;
|
|
141
|
+
color: var(--md-sys-color-error);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
ul {
|
|
145
|
+
margin: 0;
|
|
146
|
+
padding: 0;
|
|
147
|
+
list-style: none;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
li {
|
|
151
|
+
display: grid;
|
|
152
|
+
grid-template-columns: auto 1fr auto;
|
|
153
|
+
gap: 4px 8px;
|
|
154
|
+
align-items: baseline;
|
|
155
|
+
padding: 7px 0;
|
|
156
|
+
border-bottom: 1px dashed var(--md-sys-color-outline-variant);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
li:last-child {
|
|
160
|
+
border-bottom: 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
li span[no] {
|
|
164
|
+
font-family: var(--mono-font, monospace);
|
|
165
|
+
font-weight: 700;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
li span[when] {
|
|
169
|
+
grid-column: 2 / 4;
|
|
170
|
+
font-size: 0.7rem;
|
|
171
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
li span[comment] {
|
|
175
|
+
grid-column: 1 / 4;
|
|
176
|
+
line-height: 1.5;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
li button[revert] {
|
|
180
|
+
padding: 2px 8px;
|
|
181
|
+
color: var(--md-sys-color-on-surface);
|
|
182
|
+
background: none;
|
|
183
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
184
|
+
border-radius: 6px;
|
|
185
|
+
cursor: pointer;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
li button[revert]:hover {
|
|
189
|
+
color: var(--md-sys-color-on-primary-container);
|
|
190
|
+
background-color: var(--md-sys-color-primary-container);
|
|
191
|
+
border-color: transparent;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/*
|
|
195
|
+
게이트 사유. 막는 것은 색으로도 말하고, 알리는 것은 조용히 둔다 — 둘을 같은 톤으로 두면
|
|
196
|
+
「이 중 어느 것이 발행을 막았나」를 사람이 다시 세어야 한다.
|
|
197
|
+
*/
|
|
198
|
+
ul[gate] {
|
|
199
|
+
margin: var(--spacing-small, 6px) 0 0;
|
|
200
|
+
padding: 0;
|
|
201
|
+
list-style: none;
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-direction: column;
|
|
204
|
+
gap: 3px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
li[head] {
|
|
208
|
+
font: var(--label-font, inherit);
|
|
209
|
+
font-size: 0.72rem;
|
|
210
|
+
color: var(--md-sys-color-error);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
li[head][quiet] {
|
|
214
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
li[finding] {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
align-items: flex-start;
|
|
221
|
+
gap: 3px;
|
|
222
|
+
padding: 3px 6px;
|
|
223
|
+
font-size: 0.72rem;
|
|
224
|
+
line-height: 1.5;
|
|
225
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
226
|
+
background-color: var(--md-sys-color-surface-container);
|
|
227
|
+
border-radius: 4px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
li[finding][blocking] {
|
|
231
|
+
color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));
|
|
232
|
+
background-color: var(--md-sys-color-error-container);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
li[finding] code[where] {
|
|
236
|
+
font-family: var(--mono-font, monospace);
|
|
237
|
+
font-size: 0.68rem;
|
|
238
|
+
opacity: 0.85;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* 왜 나쁜지는 곁글로 — 읽어야 하지만 잰 것보다 먼저 눈에 들 필요는 없다 */
|
|
242
|
+
li[finding] span[why-line] {
|
|
243
|
+
opacity: 0.8;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* 무엇을 바꾸면 되나 — 이 줄이 실제로 사람이 할 일이다. 연장 표를 달아 눈에 걸리게 한다 */
|
|
247
|
+
li[finding] span[how-line] {
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: flex-start;
|
|
250
|
+
gap: 4px;
|
|
251
|
+
padding-top: 2px;
|
|
252
|
+
font-weight: 600;
|
|
253
|
+
}
|
|
254
|
+
li[finding] span[how-line] md-icon {
|
|
255
|
+
--md-icon-size: 13px;
|
|
256
|
+
flex: none;
|
|
257
|
+
margin-top: 1px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
p[quiet] {
|
|
261
|
+
margin: 0;
|
|
262
|
+
line-height: 1.6;
|
|
263
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
264
|
+
}
|
|
265
|
+
`
|
|
266
|
+
]; }
|
|
267
|
+
updated(changes) {
|
|
268
|
+
if (changes.has('figureId') || changes.has('figureVersion')) {
|
|
269
|
+
void this.reload();
|
|
270
|
+
}
|
|
271
|
+
/*
|
|
272
|
+
저장이 끝난 순간(dirty 가 풀림) 정본이 바뀌었으니 판정을 다시 묻는다. 저작 중(dirty)에는
|
|
273
|
+
묻지 않는다 — 서버에 있는 것은 저장 전 정본이라, 그 답은 화면에 있는 것과 다른 것을 말한다.
|
|
274
|
+
*/
|
|
275
|
+
if (changes.has('figureId') || changes.has('dirty')) {
|
|
276
|
+
void this.inspect();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async inspect() {
|
|
280
|
+
if (!this.figureId || this.dirty) {
|
|
281
|
+
this.inspection = undefined;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
this.inspection = await inspectFigure(this.figureId);
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
/* 판정을 못 받은 것을 「통과」로 두지 않는다 — 모르는 것이다. 단추는 서버가 막는다. */
|
|
289
|
+
this.inspection = undefined;
|
|
290
|
+
this.failure = e.message;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async reload() {
|
|
294
|
+
if (!this.figureId) {
|
|
295
|
+
this.versions = [];
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
this.versions = await fetchFigureVersions(this.figureId);
|
|
300
|
+
this.failure = '';
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
this.failure = e.message;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
render() {
|
|
307
|
+
return html `
|
|
308
|
+
<section>
|
|
309
|
+
<h3>${i18next.t('figure.label.release')}</h3>
|
|
310
|
+
${this.figureId ? this.renderRelease() : html `<p quiet>${i18next.t('figure.text.save-the-figure-before-releasing')}</p>`}
|
|
311
|
+
</section>
|
|
312
|
+
|
|
313
|
+
<section>
|
|
314
|
+
<h3>${i18next.t('figure.label.released-versions')}</h3>
|
|
315
|
+
${this.versions.length
|
|
316
|
+
? html `<ul>
|
|
317
|
+
${this.versions.map(version => this.renderVersion(version))}
|
|
318
|
+
</ul>`
|
|
319
|
+
: html `<p quiet>${i18next.t('figure.text.nothing-released-yet')}</p>`}
|
|
320
|
+
</section>
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
renderRelease() {
|
|
324
|
+
const released = this.figureState === 'released';
|
|
325
|
+
/*
|
|
326
|
+
게이트가 막는 것과 알리는 것을 함께 낸다. **막는 것은 단추를 잠근다** — 눌러 보고 서버에게
|
|
327
|
+
거절당하는 것으로 알게 하지 않는다. 알리는 것은 잠그지 않고 그대로 보여 준다.
|
|
328
|
+
*/
|
|
329
|
+
const blocking = (this.inspection?.findings ?? []).filter(finding => finding.blocking);
|
|
330
|
+
const advisory = (this.inspection?.findings ?? []).filter(finding => !finding.blocking);
|
|
331
|
+
/*
|
|
332
|
+
── 「발행되나」에 **한 번만** 답한다 ─────────────────────────────────────
|
|
333
|
+
전에는 막는 목록의 머리가 「이것들이 발행을 막습니다」, 알리는 목록의 머리가 「발행은 되지만
|
|
334
|
+
확인해 보십시오」였다. 두 줄이 나란히 서서 화면이 「막힌다」와 「된다」를 함께 말했고, 사람이
|
|
335
|
+
「그래서 되는 건가」를 되물었다.
|
|
336
|
+
|
|
337
|
+
답은 이 한 줄이 하고, 아래 목록들은 **이유**일 뿐이다 — 그래서 목록 머리는 문장이 아니라
|
|
338
|
+
이름씨다(「고쳐야 하는 것」·「막지는 않는 것」).
|
|
339
|
+
*/
|
|
340
|
+
const why = this.dirty
|
|
341
|
+
? i18next.t('figure.text.release-needs-a-save-first')
|
|
342
|
+
: released
|
|
343
|
+
? i18next.t('figure.text.already-released-edit-to-release-again')
|
|
344
|
+
: blocking.length
|
|
345
|
+
? i18next.t('figure.text.cannot-release-now')
|
|
346
|
+
: '';
|
|
347
|
+
return html `
|
|
348
|
+
<div now>
|
|
349
|
+
<span state ?released=${released}>${i18next.t(released ? 'figure.label.figure-released' : 'figure.label.draft')}</span>
|
|
350
|
+
<span ver>${i18next.t('figure.text.version-number', { version: this.figureVersion ?? 0 })}</span>
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<input note placeholder=${i18next.t('figure.text.what-changed-in-this-release')} ?disabled=${!!why || this.busy} />
|
|
354
|
+
|
|
355
|
+
<button release ?disabled=${!!why || this.busy} @click=${() => this.release()}>
|
|
356
|
+
${this.busy ? i18next.t('figure.text.releasing') : i18next.t('figure.button.release-figure')}
|
|
357
|
+
</button>
|
|
358
|
+
|
|
359
|
+
${why ? html `<p why>${why}</p>` : nothing}
|
|
360
|
+
${blocking.length
|
|
361
|
+
? html `<ul gate>
|
|
362
|
+
<li head>${i18next.t('figure.text.must-fix-to-release')}</li>
|
|
363
|
+
${blocking.map(finding => this.renderFinding(finding, true))}
|
|
364
|
+
</ul>`
|
|
365
|
+
: nothing}
|
|
366
|
+
${advisory.length
|
|
367
|
+
? html `<ul gate>
|
|
368
|
+
<li head quiet>${i18next.t('figure.text.does-not-block')}</li>
|
|
369
|
+
${advisory.map(finding => this.renderFinding(finding, false))}
|
|
370
|
+
</ul>`
|
|
371
|
+
: nothing}
|
|
372
|
+
${this.failure ? html `<p failed>${this.failure}</p>` : nothing}
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* 사유 한 줄 — **잰 것 · 왜 · 어떻게** 셋을 함께 말한다.
|
|
377
|
+
*
|
|
378
|
+
* 잰 것만 보여 줬더니 「1 조각이 2 조각으로 갈라진다」에서 사람이 멈췄다. 왜 나쁜지도, 무엇을
|
|
379
|
+
* 바꾸면 되는지도 알 수 없었다. 그 셋은 형식(`figure-model` 의 gate)이 만들고 여기서는 자리만
|
|
380
|
+
* 준다 — 화면이 문장을 지으면 서버가 거절할 때와 다른 말을 하게 된다.
|
|
381
|
+
*/
|
|
382
|
+
renderFinding(finding, blocking) {
|
|
383
|
+
return html `
|
|
384
|
+
<li finding ?blocking=${blocking}>
|
|
385
|
+
<span msg>${finding.message}</span>
|
|
386
|
+
${finding.at ? html `<code where>${finding.at}</code>` : nothing}
|
|
387
|
+
${finding.why ? html `<span why-line>${finding.why}</span>` : nothing}
|
|
388
|
+
${finding.how ? html `<span how-line><md-icon>build</md-icon>${finding.how}</span>` : nothing}
|
|
389
|
+
</li>
|
|
390
|
+
`;
|
|
391
|
+
}
|
|
392
|
+
renderVersion(version) {
|
|
393
|
+
return html `
|
|
394
|
+
<li>
|
|
395
|
+
<span no>${i18next.t('figure.text.version-number', { version: version.version })}</span>
|
|
396
|
+
<span when>
|
|
397
|
+
${version.updatedAt ? new Date(version.updatedAt).toLocaleString() : ''}
|
|
398
|
+
${version.updater?.name ? ` · ${version.updater.name}` : ''}
|
|
399
|
+
</span>
|
|
400
|
+
<button revert ?disabled=${this.busy} @click=${() => this.revert(version.version)}>
|
|
401
|
+
${i18next.t('figure.button.revert')}
|
|
402
|
+
</button>
|
|
403
|
+
${version.comment ? html `<span comment>${version.comment}</span>` : nothing}
|
|
404
|
+
</li>
|
|
405
|
+
`;
|
|
406
|
+
}
|
|
407
|
+
async release() {
|
|
408
|
+
const input = this.renderRoot.querySelector('input[note]');
|
|
409
|
+
const comment = input?.value?.trim();
|
|
410
|
+
this.busy = true;
|
|
411
|
+
this.failure = '';
|
|
412
|
+
try {
|
|
413
|
+
const figure = await releaseFigure(this.figureId, comment || undefined);
|
|
414
|
+
if (input)
|
|
415
|
+
input.value = '';
|
|
416
|
+
/* 정본을 든 자리가 갱신한다 — 여기서 페이지 상태를 고쳐 쓰지 않는다. */
|
|
417
|
+
this.dispatchEvent(new CustomEvent('figure-released', { detail: { figure }, bubbles: true, composed: true }));
|
|
418
|
+
await this.reload();
|
|
419
|
+
await this.inspect();
|
|
420
|
+
}
|
|
421
|
+
catch (e) {
|
|
422
|
+
this.failure = e.message;
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
this.busy = false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async revert(version) {
|
|
429
|
+
/* 되돌아갈 수 없는 조작은 아니지만(판본은 남는다), 지금 화면의 것이 덮인다 — 이름으로 확인한다. */
|
|
430
|
+
if (!confirm(i18next.t('figure.text.revert-to-version-confirm', { version }))) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this.busy = true;
|
|
434
|
+
this.failure = '';
|
|
435
|
+
try {
|
|
436
|
+
const result = await revertFigureVersion(this.figureId, version);
|
|
437
|
+
this.dispatchEvent(new CustomEvent('figure-reverted', { detail: result, bubbles: true, composed: true }));
|
|
438
|
+
await this.reload();
|
|
439
|
+
/*
|
|
440
|
+
**되돌리면 정본이 바뀌므로 판정도 바뀐다.** 이것을 안 하면 옛 판의 사유가 새 정본 위에
|
|
441
|
+
남아 화면이 거짓말한다 — 되돌리기는 `dirty` 도 `figureVersion` 도 건드리지 않으므로
|
|
442
|
+
`updated` 의 감시로는 다시 묻지 않는다.
|
|
443
|
+
*/
|
|
444
|
+
await this.inspect();
|
|
445
|
+
}
|
|
446
|
+
catch (e) {
|
|
447
|
+
this.failure = e.message;
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
this.busy = false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
__decorate([
|
|
455
|
+
property({ type: String }),
|
|
456
|
+
__metadata("design:type", String)
|
|
457
|
+
], FigureHistoryPanel.prototype, "figureId", void 0);
|
|
458
|
+
__decorate([
|
|
459
|
+
property({ type: String }),
|
|
460
|
+
__metadata("design:type", String)
|
|
461
|
+
], FigureHistoryPanel.prototype, "figureState", void 0);
|
|
462
|
+
__decorate([
|
|
463
|
+
property({ type: Number }),
|
|
464
|
+
__metadata("design:type", Object)
|
|
465
|
+
], FigureHistoryPanel.prototype, "figureVersion", void 0);
|
|
466
|
+
__decorate([
|
|
467
|
+
property({ type: Boolean }),
|
|
468
|
+
__metadata("design:type", Object)
|
|
469
|
+
], FigureHistoryPanel.prototype, "dirty", void 0);
|
|
470
|
+
__decorate([
|
|
471
|
+
state(),
|
|
472
|
+
__metadata("design:type", Array)
|
|
473
|
+
], FigureHistoryPanel.prototype, "versions", void 0);
|
|
474
|
+
__decorate([
|
|
475
|
+
state(),
|
|
476
|
+
__metadata("design:type", Object)
|
|
477
|
+
], FigureHistoryPanel.prototype, "busy", void 0);
|
|
478
|
+
__decorate([
|
|
479
|
+
state(),
|
|
480
|
+
__metadata("design:type", Object)
|
|
481
|
+
], FigureHistoryPanel.prototype, "failure", void 0);
|
|
482
|
+
__decorate([
|
|
483
|
+
state(),
|
|
484
|
+
__metadata("design:type", Object)
|
|
485
|
+
], FigureHistoryPanel.prototype, "inspection", void 0);
|
|
486
|
+
FigureHistoryPanel = __decorate([
|
|
487
|
+
customElement('figure-history-panel')
|
|
488
|
+
], FigureHistoryPanel);
|
|
489
|
+
export { FigureHistoryPanel };
|
|
490
|
+
//# sourceMappingURL=figure-history-panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure-history-panel.js","sourceRoot":"","sources":["../../client/modeller/figure-history-panel.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { fetchFigureVersions, inspectFigure, releaseFigure, revertFigureVersion } from '../graphql/index.js'\nimport type { FigureGateFinding, FigureInspection, FigureVersion } from '../types.js'\n\n/**\n * 이력 — **발행하는 자리이자, 발행된 판들을 보는 자리.**\n *\n * ## 왜 저장 옆이 아니라 여기인가\n *\n * 저장은 하루에 수십 번이고 발행은 드물다. 두 단추를 나란히 두면 저장하려다 발행을 누른다.\n * 그리고 발행은 **소비처에게 하는 약속**이라, 무엇을 약속하는지(지금 몇 판인가 · 저장 안 한 것이\n * 있나 · 지난 판이 무엇이었나)를 함께 보여 주는 자리에 있어야 한다.\n *\n * ## 저장하지 않은 것이 있으면 발행하지 않는다\n *\n * 발행은 **저장된 것**을 내보낸다. 화면에 고친 것이 남아 있는 채로 발행하면, 저작자가 보고 있는\n * 것과 약속한 것이 다르다. 그래서 막고, 왜 막혔는지 적는다.\n *\n * ## 되돌리기는 초안으로 돌아온다\n *\n * 옛 판의 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 되살렸다고 그 판이 다시 발행되지는\n * 않는다 — 다시 내보내려면 발행을 한 번 더 하고, 그때 번호는 이미 쓴 다음 번호를 받는다.\n */\n@customElement('figure-history-panel')\nexport class FigureHistoryPanel extends localize(i18next)(LitElement) {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n min-height: 0;\n overflow-y: auto;\n background-color: var(--md-sys-color-surface-container-lowest);\n color: var(--md-sys-color-on-surface);\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n }\n\n :host([off]) {\n display: none;\n }\n\n section {\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n\n h3 {\n margin: 0 0 6px 0;\n font-size: var(--ui-t-micro, 11px);\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 지금 무엇인가 — 발행을 누르기 전에 읽는 한 줄 */\n div[now] {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 8px;\n }\n\n span[state] {\n padding: 2px 8px;\n border-radius: 10px;\n font-size: 0.7rem;\n font-weight: 700;\n color: var(--md-sys-color-on-secondary-container);\n background-color: var(--md-sys-color-secondary-container);\n }\n\n span[state][released] {\n color: var(--md-sys-color-on-primary);\n background-color: var(--md-sys-color-primary);\n }\n\n span[ver] {\n font-family: var(--mono-font, monospace);\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n input,\n button {\n font: var(--input-field-font, inherit);\n font-size: 0.76rem;\n }\n\n input[note] {\n width: 100%;\n box-sizing: border-box;\n padding: 5px 8px;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n }\n\n button[release] {\n width: 100%;\n margin-top: 6px;\n padding: 6px 0;\n font-weight: 700;\n color: var(--ui-on-accent, #fff);\n background-color: var(--md-sys-color-primary);\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n }\n\n button[release][disabled] {\n opacity: 0.45;\n cursor: default;\n }\n\n /* 왜 못 누르는지 — 눌러 보고 알게 하지 않는다 */\n p[why] {\n margin: 6px 0 0 0;\n font-size: 0.7rem;\n line-height: 1.5;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n p[failed] {\n margin: 6px 0 0 0;\n font-size: 0.7rem;\n line-height: 1.5;\n color: var(--md-sys-color-error);\n }\n\n ul {\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n li {\n display: grid;\n grid-template-columns: auto 1fr auto;\n gap: 4px 8px;\n align-items: baseline;\n padding: 7px 0;\n border-bottom: 1px dashed var(--md-sys-color-outline-variant);\n }\n\n li:last-child {\n border-bottom: 0;\n }\n\n li span[no] {\n font-family: var(--mono-font, monospace);\n font-weight: 700;\n }\n\n li span[when] {\n grid-column: 2 / 4;\n font-size: 0.7rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n li span[comment] {\n grid-column: 1 / 4;\n line-height: 1.5;\n }\n\n li button[revert] {\n padding: 2px 8px;\n color: var(--md-sys-color-on-surface);\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n\n li button[revert]:hover {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-color: transparent;\n }\n\n /*\n 게이트 사유. 막는 것은 색으로도 말하고, 알리는 것은 조용히 둔다 — 둘을 같은 톤으로 두면\n 「이 중 어느 것이 발행을 막았나」를 사람이 다시 세어야 한다.\n */\n ul[gate] {\n margin: var(--spacing-small, 6px) 0 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 3px;\n }\n\n li[head] {\n font: var(--label-font, inherit);\n font-size: 0.72rem;\n color: var(--md-sys-color-error);\n }\n\n li[head][quiet] {\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n li[finding] {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 3px;\n padding: 3px 6px;\n font-size: 0.72rem;\n line-height: 1.5;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: var(--md-sys-color-surface-container);\n border-radius: 4px;\n }\n\n li[finding][blocking] {\n color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));\n background-color: var(--md-sys-color-error-container);\n }\n\n li[finding] code[where] {\n font-family: var(--mono-font, monospace);\n font-size: 0.68rem;\n opacity: 0.85;\n }\n\n /* 왜 나쁜지는 곁글로 — 읽어야 하지만 잰 것보다 먼저 눈에 들 필요는 없다 */\n li[finding] span[why-line] {\n opacity: 0.8;\n }\n\n /* 무엇을 바꾸면 되나 — 이 줄이 실제로 사람이 할 일이다. 연장 표를 달아 눈에 걸리게 한다 */\n li[finding] span[how-line] {\n display: flex;\n align-items: flex-start;\n gap: 4px;\n padding-top: 2px;\n font-weight: 600;\n }\n li[finding] span[how-line] md-icon {\n --md-icon-size: 13px;\n flex: none;\n margin-top: 1px;\n }\n\n p[quiet] {\n margin: 0;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n `\n ]\n\n /** 지금 열려 있는 도형. 없으면(새로 만드는 중) 발행할 것이 없다. */\n @property({ type: String }) figureId?: string\n @property({ type: String }) figureState?: 'draft' | 'released'\n @property({ type: Number }) figureVersion = 0\n /** 저장하지 않은 것이 있나 — 있으면 발행을 막는다. */\n @property({ type: Boolean }) dirty = false\n\n @state() private versions: FigureVersion[] = []\n @state() private busy = false\n @state() private failure = ''\n /**\n * 발행 판정. **저장된 정본에 대한 것**이라 저장할 때마다 다시 묻는다(`figureVersion` 이 아니라\n * `dirty` 가 풀리는 순간이 그때다).\n */\n @state() private inspection?: FigureInspection\n\n updated(changes: Map<string, unknown>) {\n if (changes.has('figureId') || changes.has('figureVersion')) {\n void this.reload()\n }\n\n /*\n 저장이 끝난 순간(dirty 가 풀림) 정본이 바뀌었으니 판정을 다시 묻는다. 저작 중(dirty)에는\n 묻지 않는다 — 서버에 있는 것은 저장 전 정본이라, 그 답은 화면에 있는 것과 다른 것을 말한다.\n */\n if (changes.has('figureId') || changes.has('dirty')) {\n void this.inspect()\n }\n }\n\n private async inspect() {\n if (!this.figureId || this.dirty) {\n this.inspection = undefined\n return\n }\n\n try {\n this.inspection = await inspectFigure(this.figureId)\n } catch (e) {\n /* 판정을 못 받은 것을 「통과」로 두지 않는다 — 모르는 것이다. 단추는 서버가 막는다. */\n this.inspection = undefined\n this.failure = (e as Error).message\n }\n }\n\n private async reload() {\n if (!this.figureId) {\n this.versions = []\n return\n }\n\n try {\n this.versions = await fetchFigureVersions(this.figureId)\n this.failure = ''\n } catch (e) {\n this.failure = (e as Error).message\n }\n }\n\n render() {\n return html`\n <section>\n <h3>${i18next.t('figure.label.release')}</h3>\n ${this.figureId ? this.renderRelease() : html`<p quiet>${i18next.t('figure.text.save-the-figure-before-releasing')}</p>`}\n </section>\n\n <section>\n <h3>${i18next.t('figure.label.released-versions')}</h3>\n ${this.versions.length\n ? html`<ul>\n ${this.versions.map(version => this.renderVersion(version))}\n </ul>`\n : html`<p quiet>${i18next.t('figure.text.nothing-released-yet')}</p>`}\n </section>\n `\n }\n\n private renderRelease() {\n const released = this.figureState === 'released'\n\n /*\n 게이트가 막는 것과 알리는 것을 함께 낸다. **막는 것은 단추를 잠근다** — 눌러 보고 서버에게\n 거절당하는 것으로 알게 하지 않는다. 알리는 것은 잠그지 않고 그대로 보여 준다.\n */\n const blocking = (this.inspection?.findings ?? []).filter(finding => finding.blocking)\n const advisory = (this.inspection?.findings ?? []).filter(finding => !finding.blocking)\n\n /*\n ── 「발행되나」에 **한 번만** 답한다 ─────────────────────────────────────\n 전에는 막는 목록의 머리가 「이것들이 발행을 막습니다」, 알리는 목록의 머리가 「발행은 되지만\n 확인해 보십시오」였다. 두 줄이 나란히 서서 화면이 「막힌다」와 「된다」를 함께 말했고, 사람이\n 「그래서 되는 건가」를 되물었다.\n\n 답은 이 한 줄이 하고, 아래 목록들은 **이유**일 뿐이다 — 그래서 목록 머리는 문장이 아니라\n 이름씨다(「고쳐야 하는 것」·「막지는 않는 것」).\n */\n const why = this.dirty\n ? i18next.t('figure.text.release-needs-a-save-first')\n : released\n ? i18next.t('figure.text.already-released-edit-to-release-again')\n : blocking.length\n ? i18next.t('figure.text.cannot-release-now')\n : ''\n\n return html`\n <div now>\n <span state ?released=${released}>${i18next.t(released ? 'figure.label.figure-released' : 'figure.label.draft')}</span>\n <span ver>${i18next.t('figure.text.version-number', { version: this.figureVersion ?? 0 })}</span>\n </div>\n\n <input note placeholder=${i18next.t('figure.text.what-changed-in-this-release')} ?disabled=${!!why || this.busy} />\n\n <button release ?disabled=${!!why || this.busy} @click=${() => this.release()}>\n ${this.busy ? i18next.t('figure.text.releasing') : i18next.t('figure.button.release-figure')}\n </button>\n\n ${why ? html`<p why>${why}</p>` : nothing}\n ${blocking.length\n ? html`<ul gate>\n <li head>${i18next.t('figure.text.must-fix-to-release')}</li>\n ${blocking.map(finding => this.renderFinding(finding, true))}\n </ul>`\n : nothing}\n ${advisory.length\n ? html`<ul gate>\n <li head quiet>${i18next.t('figure.text.does-not-block')}</li>\n ${advisory.map(finding => this.renderFinding(finding, false))}\n </ul>`\n : nothing}\n ${this.failure ? html`<p failed>${this.failure}</p>` : nothing}\n `\n }\n\n /**\n * 사유 한 줄 — **잰 것 · 왜 · 어떻게** 셋을 함께 말한다.\n *\n * 잰 것만 보여 줬더니 「1 조각이 2 조각으로 갈라진다」에서 사람이 멈췄다. 왜 나쁜지도, 무엇을\n * 바꾸면 되는지도 알 수 없었다. 그 셋은 형식(`figure-model` 의 gate)이 만들고 여기서는 자리만\n * 준다 — 화면이 문장을 지으면 서버가 거절할 때와 다른 말을 하게 된다.\n */\n private renderFinding(finding: FigureGateFinding, blocking: boolean) {\n return html`\n <li finding ?blocking=${blocking}>\n <span msg>${finding.message}</span>\n ${finding.at ? html`<code where>${finding.at}</code>` : nothing}\n ${finding.why ? html`<span why-line>${finding.why}</span>` : nothing}\n ${finding.how ? html`<span how-line><md-icon>build</md-icon>${finding.how}</span>` : nothing}\n </li>\n `\n }\n\n private renderVersion(version: FigureVersion) {\n return html`\n <li>\n <span no>${i18next.t('figure.text.version-number', { version: version.version })}</span>\n <span when>\n ${version.updatedAt ? new Date(version.updatedAt).toLocaleString() : ''}\n ${version.updater?.name ? ` · ${version.updater.name}` : ''}\n </span>\n <button revert ?disabled=${this.busy} @click=${() => this.revert(version.version)}>\n ${i18next.t('figure.button.revert')}\n </button>\n ${version.comment ? html`<span comment>${version.comment}</span>` : nothing}\n </li>\n `\n }\n\n private async release() {\n const input = this.renderRoot.querySelector('input[note]') as HTMLInputElement\n const comment = input?.value?.trim()\n\n this.busy = true\n this.failure = ''\n\n try {\n const figure = await releaseFigure(this.figureId!, comment || undefined)\n if (input) input.value = ''\n /* 정본을 든 자리가 갱신한다 — 여기서 페이지 상태를 고쳐 쓰지 않는다. */\n this.dispatchEvent(new CustomEvent('figure-released', { detail: { figure }, bubbles: true, composed: true }))\n await this.reload()\n await this.inspect()\n } catch (e) {\n this.failure = (e as Error).message\n } finally {\n this.busy = false\n }\n }\n\n private async revert(version: number) {\n /* 되돌아갈 수 없는 조작은 아니지만(판본은 남는다), 지금 화면의 것이 덮인다 — 이름으로 확인한다. */\n if (!confirm(i18next.t('figure.text.revert-to-version-confirm', { version }))) {\n return\n }\n\n this.busy = true\n this.failure = ''\n\n try {\n const result = await revertFigureVersion(this.figureId!, version)\n this.dispatchEvent(new CustomEvent('figure-reverted', { detail: result, bubbles: true, composed: true }))\n await this.reload()\n /*\n **되돌리면 정본이 바뀌므로 판정도 바뀐다.** 이것을 안 하면 옛 판의 사유가 새 정본 위에\n 남아 화면이 거짓말한다 — 되돌리기는 `dirty` 도 `figureVersion` 도 건드리지 않으므로\n `updated` 의 감시로는 다시 묻지 않는다.\n */\n await this.inspect()\n } catch (e) {\n this.failure = (e as Error).message\n } finally {\n this.busy = false\n }\n }\n}\n"]}
|
|
@@ -47,6 +47,8 @@ export declare class FigureInspector extends FigureInspector_base {
|
|
|
47
47
|
/** 보드. 위치를 보드 중심 기준으로 바꾸려면 이것이 있어야 한다. */
|
|
48
48
|
/** 도형의 표시 이름. 정본은 저작면이 들고, 여기서는 보여 주고 바꾼다(`figure-rename` 으로 알린다). */
|
|
49
49
|
figureName: string;
|
|
50
|
+
/** 펴 둔 도움말. 부품을 바꿔도 유지된다 — 매번 다시 펴게 하지 않는다. */
|
|
51
|
+
private opened;
|
|
50
52
|
board?: BoardModel;
|
|
51
53
|
/** 보드 위의 부품들. 씬 모델 그대로다. */
|
|
52
54
|
parts: PartModel[];
|
|
@@ -54,6 +56,29 @@ export declare class FigureInspector extends FigureInspector_base {
|
|
|
54
56
|
/** `part` 는 HTMLElement 의 속성(DOMTokenList)이다 — 가리면 형이 어긋난다. */
|
|
55
57
|
private get chosen();
|
|
56
58
|
render(): import("lit-html").TemplateResult<1>;
|
|
59
|
+
/**
|
|
60
|
+
* ── 이 부품이 능력에서 맡는 자리 ────────────────────────────────────────────
|
|
61
|
+
*
|
|
62
|
+
* 「능력」 탭이 도형 전체가 무엇을 하는지 정하고, 어느 부품이 그것을 받쳐 주는지는 여기서
|
|
63
|
+
* 정한다. **부품을 고치는 곳이 한 군데여야** 하기 때문이다 — 목록을 따로 두면 같은 부품을
|
|
64
|
+
* 두 화면에서 고치게 되고, 저작자는 어느 쪽이 이기는지 매번 생각해야 한다.
|
|
65
|
+
*
|
|
66
|
+
* 칸 수는 `repeat` 가 있으면 받지 않는다. 반복하는 자리의 칸 수는 **인스턴스 크기**가 정한다 —
|
|
67
|
+
* 컨베이어가 길어지면 실을 자리가 늘어나는 것이 그것이고, 여기 적은 값은 안 쓰인다.
|
|
68
|
+
*/
|
|
69
|
+
private renderRole;
|
|
70
|
+
/**
|
|
71
|
+
* 역할 하나를 켜고 끈다.
|
|
72
|
+
*
|
|
73
|
+
* **여럿을 함께 맡을 수 있다.** AGV 의 상판은 물건을 놓는 자리이면서 그 자리로 물건이
|
|
74
|
+
* 들어오고 나간다. 그래서 하나를 고르는 선택 상자가 아니라 확인 상자다.
|
|
75
|
+
*
|
|
76
|
+
* 마지막 역할을 끄면 `capability` 를 통째로 지운다. 빈 배열을 남기면 형식이 오류로 보고,
|
|
77
|
+
* 저작자가 보기에는 아무 역할도 없는 평범한 부품인데 발행이 막힌다.
|
|
78
|
+
*/
|
|
79
|
+
private toggleRole;
|
|
80
|
+
private setAccepts;
|
|
81
|
+
private setCapacity;
|
|
57
82
|
/**
|
|
58
83
|
* 도형 자체 — 이름과 타입.
|
|
59
84
|
*
|
|
@@ -124,6 +149,65 @@ export declare class FigureInspector extends FigureInspector_base {
|
|
|
124
149
|
* 성숙도의 묶음 수에 그대로 잡힌다. 고르는 자리에서 미리 말해 준다.
|
|
125
150
|
*/
|
|
126
151
|
private renderBehaviour;
|
|
152
|
+
/**
|
|
153
|
+
* ── 몇 개가 되나 ──────────────────────────────────────────────────────────
|
|
154
|
+
*
|
|
155
|
+
* 저작 화면은 축과 피치만 받고 있었다. 형식이 가진 반복 설정이 그 둘뿐이므로 편집할 것은
|
|
156
|
+
* 다 있었는데, **결과인 개수를 어디에도 표시하지 않았다.** 피치 300 을 적은 저작자가 몇 개가
|
|
157
|
+
* 되는지 알려면 미리보기를 세어야 했다.
|
|
158
|
+
*
|
|
159
|
+
* 개수는 그리는 쪽이 쓰는 `repeatPlan` 을 그대로 불러 얻는다. 여기서 다시 계산하면 화면이
|
|
160
|
+
* 말하는 개수와 실제로 그려지는 개수가 달라진다.
|
|
161
|
+
*
|
|
162
|
+
* ## 두 가지 반복이 있다
|
|
163
|
+
*
|
|
164
|
+
* 컨베이어의 롤러는 길어지면 **개수가 늘고** 한 칸 간격은 그대로다. 셀트레이의 셀은 커지면
|
|
165
|
+
* **셀이 커지고** 개수는 그대로다 — 몇 셀짜리 트레이냐는 화면 크기와 무관하다. 그 갈림을
|
|
166
|
+
* 이 축의 anchor 가 정하는데 화면에는 그 말이 없었다. 그래서 어느 쪽인지 함께 적는다.
|
|
167
|
+
*/
|
|
168
|
+
private renderRepeatCount;
|
|
169
|
+
/**
|
|
170
|
+
* ── 어느 면을 붙잡나 ──────────────────────────────────────────────────────
|
|
171
|
+
*
|
|
172
|
+
* **이 자리가 없었다.** 형식은 축마다 `anchor` 를 지고 다니고(`figure-source.ts` 가 왕복시킨다),
|
|
173
|
+
* 발행 게이트는 「valve-wheel 의 anchor.z 를 max 로 두라」고 사유까지 말하는데, 저작면에는 그
|
|
174
|
+
* 값을 바꿀 자리가 없었다 — 고치라는 말만 있고 고칠 문이 없었다.
|
|
175
|
+
*
|
|
176
|
+
* ## 「자동」이 기본이다
|
|
177
|
+
*
|
|
178
|
+
* 형식에서 `anchor` 는 적지 않아도 된다. 안 적으면 **가까운 면**을 붙잡고, 표본 열넷 중 어느
|
|
179
|
+
* 것도 이 값을 적지 않고 제대로 선다. 그래서 축마다 첫 항목이 「자동」이고, 지금 자동이 무엇으로
|
|
180
|
+
* 읽히는지를 **함께 보여 준다** — 그것을 모르면 무엇을 바꾸는지 모른 채 고르게 된다.
|
|
181
|
+
*
|
|
182
|
+
* 자동이 무엇인지는 형식에게 묻는다(`anchorOfPart`). 여기서 「위 절반이면 max」를 다시 세면
|
|
183
|
+
* 화면이 말하는 면과 실제로 그려지는 면이 갈린다.
|
|
184
|
+
*
|
|
185
|
+
* ## `scale` 인 부품에도 보여 준다
|
|
186
|
+
*
|
|
187
|
+
* 처음에는 감췄다 — 형식 주석에 「`sizing` 이 `scale` 인 부품에는 뜻이 없다」고 적혀 있어서다.
|
|
188
|
+
* **그 문장은 기본값에 대한 것이다.** `anchorOf` 는 적힌 값을 가장 먼저 보고(`if (said) return
|
|
189
|
+
* said`), 그 뒤에야 `sizing` 을 본다. 즉 `scale` 부품에도 면을 적으면 그것이 이긴다.
|
|
190
|
+
*
|
|
191
|
+
* 감춰 두면 어긋남이 난다: 발행 게이트가 막는 MIXER 의 `valve-wheel` 이 `scale` 부품이고,
|
|
192
|
+
* 게이트의 사유는 「그 부품의 anchor.z 를 max 로 두라」다. 고치라는 자리에 문을 닫아 둔 셈이었다.
|
|
193
|
+
*
|
|
194
|
+
* 대신 `scale` 일 때는 자동이 무엇인지가 「함께 커진다」로 보이므로, 저작자가 그것을 바꾸는
|
|
195
|
+
* 것임을 알 수 있다.
|
|
196
|
+
*/
|
|
197
|
+
/**
|
|
198
|
+
* ── 도움말은 자리를 먹지 않는다 ─────────────────────────────────────────────
|
|
199
|
+
*
|
|
200
|
+
* 설명 문단이 열 줄 있었고 그 줄들이 패널 높이의 상당 부분이었다. 속성은 앞으로 더 늘어난다
|
|
201
|
+
* (능력 · 포트 · 애니메이션 · 기준정보) — 늘 보이는 설명은 그 자리를 먼저 먹는다.
|
|
202
|
+
*
|
|
203
|
+
* 그래서 물음표로 접는다. **지우지 않는다** — 그 문장들은 「이걸 고르면 무엇이 달라지나」를
|
|
204
|
+
* 말하는 것이고, 없으면 닫힌 목록을 고를 수 없다. 한 번 읽으면 되는 것이라 접어 두는 것이 맞다.
|
|
205
|
+
*
|
|
206
|
+
* 편 것은 **부품을 바꿔도 그대로 둔다**(`opened` 는 부품에 딸린 상태가 아니다). 같은 것을
|
|
207
|
+
* 계속 고치는 사람이 매번 다시 펴야 하면 접은 것이 되레 방해가 된다.
|
|
208
|
+
*/
|
|
209
|
+
private help;
|
|
210
|
+
private renderAnchors;
|
|
127
211
|
/**
|
|
128
212
|
* 숫자 칸.
|
|
129
213
|
*
|