@things-factory/figure-service 10.1.171 → 10.1.176
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/dist-server/service/figure/figure-format.js +13 -13
- package/dist-server/service/figure/figure-format.js.map +1 -1
- package/dist-server/service/figure/figure-tools.js +2 -2
- package/dist-server/service/figure/figure-tools.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/service/figure/figure-format.test.ts +13 -5
- package/server/service/figure/figure-format.ts +13 -13
- package/server/service/figure/figure-tools.test.ts +15 -0
- package/server/service/figure/figure-tools.ts +2 -2
- package/translations/en.json +9 -9
- package/translations/ko.json +9 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-service",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.176",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -27,16 +27,16 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@hatiolab/figure-model": "^0.7.2",
|
|
29
29
|
"@operato/typeorm-history": "^10.0.0",
|
|
30
|
-
"@things-factory/ai-client-base": "^10.1.
|
|
31
|
-
"@things-factory/attachment-base": "^10.1.
|
|
32
|
-
"@things-factory/auth-base": "^10.1.
|
|
30
|
+
"@things-factory/ai-client-base": "^10.1.176",
|
|
31
|
+
"@things-factory/attachment-base": "^10.1.176",
|
|
32
|
+
"@things-factory/auth-base": "^10.1.176",
|
|
33
33
|
"@things-factory/env": "^10.1.149",
|
|
34
|
-
"@things-factory/setting-base": "^10.1.
|
|
34
|
+
"@things-factory/setting-base": "^10.1.176",
|
|
35
35
|
"@things-factory/shell": "^10.1.165",
|
|
36
36
|
"graphql-scalars": "^1.22.4",
|
|
37
37
|
"graphql-upload": "^15.0.2",
|
|
38
38
|
"jszip": "^3.10.1",
|
|
39
39
|
"koa-router": "^7.4.0"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "24928f127d73de0e9946423bd4c3295f54cce03d"
|
|
42
42
|
}
|
|
@@ -177,7 +177,7 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
177
177
|
it('넘은 부품은 이름 · 축 · 넘은 양을 화면 말투로 말하고, 왜와 어떻게를 함께 낸다', () => {
|
|
178
178
|
const finding = inspectSource(outside(), null, translatorOf(KO)).findings.find(f => f.code === 'part-outside-occupancy')!
|
|
179
179
|
|
|
180
|
-
expect(finding.message).toMatch(
|
|
180
|
+
expect(finding.message).toMatch(/^도형이 차지하는 공간을 'arm' 부품이 X 로 [\d.]+mm 넘습니다\.$/)
|
|
181
181
|
expect(finding.why).toMatch(/니다\.$/)
|
|
182
182
|
expect(finding.how).toContain('부품에 맞추기')
|
|
183
183
|
expect(finding.how).toMatch(/세요\.$/)
|
|
@@ -202,10 +202,18 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
202
202
|
expect(said.findings.some(f => f.code === 'extension-gone')).toBe(false)
|
|
203
203
|
})
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
/* 첫 15분 다시 재기(10-16 저녁): 판정 문구만 옛 말 「공간 선언」을 썼다. 화면 말은 ADR-0097 §9 Q2 A 의 표다. */
|
|
206
|
+
it('판정 문구는 화면 말을 쓴다 — 「차지하는 공간」, 「공간 선언」은 쓰지 않는다', () => {
|
|
207
|
+
const said = Object.entries(KO).filter(([key]) => key.startsWith('figure.gate.'))
|
|
208
|
+
expect(said.filter(([, text]) => /공간 선언|공간을[^.]*선언/.test(text)).map(([key]) => key)).toEqual([])
|
|
209
|
+
const finding = inspectSource(outside(), null, translatorOf(KO)).findings.find(f => f.code === 'part-outside-occupancy')!
|
|
210
|
+
expect(`${finding.message}${finding.why}${finding.how}`).toContain('차지하는 공간')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('차지하는 공간을 정하지 않은 도형은 무엇을 누르면 되는지 말한다', () => {
|
|
206
214
|
const finding = inspectSource(bare(), null, translatorOf(KO)).findings.find(f => f.code === 'occupancy-undeclared')!
|
|
207
215
|
|
|
208
|
-
expect(finding.message).toBe('이 도형은 차지하는 공간을 아직
|
|
216
|
+
expect(finding.message).toBe('이 도형은 차지하는 공간을 아직 정하지 않았습니다.')
|
|
209
217
|
expect(finding.how).toContain('부품에 맞추기')
|
|
210
218
|
})
|
|
211
219
|
|
|
@@ -213,7 +221,7 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
213
221
|
const empty = serializeFigureAsset(createFigureAsset('Empty'))
|
|
214
222
|
const finding = inspectSource(empty, null, translatorOf(KO)).findings.find(f => f.code === 'occupancy-undeclared')!
|
|
215
223
|
|
|
216
|
-
expect(finding.how).toBe('먼저 도형을 추가한 뒤, 「부품에 맞추기」로 공간을
|
|
224
|
+
expect(finding.how).toBe('먼저 도형을 추가한 뒤, 「부품에 맞추기」로 차지하는 공간을 정하세요.')
|
|
217
225
|
expect(inspectSource(empty).findings.find(f => f.code === 'occupancy-undeclared')!.how).toMatch(/^Add a shape first/)
|
|
218
226
|
})
|
|
219
227
|
|
|
@@ -289,7 +297,7 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
289
297
|
it('번역이 없으면 영어로 답한다 — 키를 찍지 않는다', () => {
|
|
290
298
|
const finding = inspectSource(outside()).findings.find(f => f.code === 'part-outside-occupancy')!
|
|
291
299
|
|
|
292
|
-
expect(finding.message).toMatch(/^'arm' goes [\d.]+ mm past the
|
|
300
|
+
expect(finding.message).toMatch(/^'arm' goes [\d.]+ mm past the space the figure takes on X\.$/)
|
|
293
301
|
expect(finding.message).not.toContain('figure.gate.')
|
|
294
302
|
})
|
|
295
303
|
|
|
@@ -119,8 +119,8 @@ export type Translate = (key: string, options?: Record<string, unknown>) => stri
|
|
|
119
119
|
* refusal. The English line is the fallback when a key is missing, so a missing translation reads as English
|
|
120
120
|
* rather than as the key printed raw.
|
|
121
121
|
*
|
|
122
|
-
* The words are the modeller's:
|
|
123
|
-
* (`figure.text.mounting-face-explained`). A fact, then the way out.
|
|
122
|
+
* The words are the modeller's (ADR-0097 §9 Q2 A): 「차지하는 공간」 for the volume a figure takes up, 「부품에 맞추기」 for
|
|
123
|
+
* setting it from the parts, and 「장착면」 for the face it stands on (`figure.text.mounting-face-explained`). A fact, then the way out.
|
|
124
124
|
*/
|
|
125
125
|
function figureWording(violation: FigureViolation, say: Say, hasParts: boolean): Pick<GateFinding, 'message' | 'why' | 'how'> {
|
|
126
126
|
const part = violation.part ?? ''
|
|
@@ -132,28 +132,28 @@ function figureWording(violation: FigureViolation, say: Say, hasParts: boolean):
|
|
|
132
132
|
switch (violation.code) {
|
|
133
133
|
case 'PART_OUTSIDE_OCCUPANCY':
|
|
134
134
|
return {
|
|
135
|
-
message: say(key('message'), { part, axis, mm }, `'${part}' goes ${mm} mm past the
|
|
136
|
-
why: say(key('why'), {}, 'The
|
|
137
|
-
how: say(key('how'), { part, axis, mm }, `Use "fit to parts" to
|
|
135
|
+
message: say(key('message'), { part, axis, mm }, `'${part}' goes ${mm} mm past the space the figure takes on ${axis}.`),
|
|
136
|
+
why: say(key('why'), {}, 'The space the figure takes sets the scale it is placed at. What lies past it is left out of that scale, so on a board this part lands outside the place given to the figure.'),
|
|
137
|
+
how: say(key('how'), { part, axis, mm }, `Use "fit to parts" to fit the space the figure takes to the parts again, or move '${part}' ${mm} mm inward on ${axis}.`)
|
|
138
138
|
}
|
|
139
139
|
case 'OCCUPANCY_UNDECLARED':
|
|
140
140
|
return violation.axis
|
|
141
141
|
? {
|
|
142
|
-
message: say(`figure.gate.occupancy-empty-axis.message`, { axis }, `The
|
|
143
|
-
why: say(key('why'), {}, '
|
|
144
|
-
how: say(key('how'), {}, 'Use "fit to parts" to
|
|
142
|
+
message: say(`figure.gate.occupancy-empty-axis.message`, { axis }, `The space the figure takes is empty on ${axis}.`),
|
|
143
|
+
why: say(key('why'), {}, 'The space a figure takes sets the scale it is placed at on a board.'),
|
|
144
|
+
how: say(key('how'), {}, 'Use "fit to parts" to set the space the figure takes from the parts as they are now.')
|
|
145
145
|
}
|
|
146
146
|
: {
|
|
147
|
-
message: say(key('message'), {}, '
|
|
148
|
-
why: say(key('why'), {}, '
|
|
147
|
+
message: say(key('message'), {}, 'The space this figure takes is not set yet.'),
|
|
148
|
+
why: say(key('why'), {}, 'The space a figure takes sets the scale it is placed at on a board.'),
|
|
149
149
|
/*
|
|
150
150
|
A new figure is refused this way before it has a part, and 「fit to parts」 has nothing to measure then —
|
|
151
151
|
the screen does not offer it. Telling the author to press it sent them looking for a button that was not
|
|
152
152
|
there (:3300, 2026-09-24).
|
|
153
153
|
*/
|
|
154
154
|
how: hasParts
|
|
155
|
-
? say(key('how'), {}, 'Use "fit to parts" to
|
|
156
|
-
: say(key('how-no-parts'), {}, 'Add a shape first, then use "fit to parts" to
|
|
155
|
+
? say(key('how'), {}, 'Use "fit to parts" to set the space the figure takes from the parts as they are now.')
|
|
156
|
+
: say(key('how-no-parts'), {}, 'Add a shape first, then use "fit to parts" to set the space the figure takes from it.')
|
|
157
157
|
}
|
|
158
158
|
case 'PARTS_OFF_PLACEMENT_FACE':
|
|
159
159
|
return {
|
|
@@ -269,7 +269,7 @@ export function inspectSource(
|
|
|
269
269
|
code: 'state-range-sampled',
|
|
270
270
|
/* Sizes as well as motion: the instance size range is swept for every figure (figure-model, designer 2026-09-27). */
|
|
271
271
|
message: say('figure.gate.state-range-sampled.message', { states }, `The whole range of sizes and motion could not be proven, so ${states} combinations of size and pose were checked.`),
|
|
272
|
-
why: say('figure.gate.state-range-sampled.why', {}, 'Between the combinations that were checked, a part may still go past the
|
|
272
|
+
why: say('figure.gate.state-range-sampled.why', {}, 'Between the combinations that were checked, a part may still go past the space the figure takes.'),
|
|
273
273
|
how: say('figure.gate.state-range-sampled.how', {}, 'To cover the whole range, re-author what could not be proven.'),
|
|
274
274
|
/*
|
|
275
275
|
Which motions stopped the proof, by the name a person gave them: the kernel reports each operation it could
|
|
@@ -249,6 +249,21 @@ describe('규율이 도구와 함께 나간다', () => {
|
|
|
249
249
|
expect(category.guidance).toMatch(/breaks the bindings/)
|
|
250
250
|
})
|
|
251
251
|
|
|
252
|
+
/*
|
|
253
|
+
:3300, 2026-10-16, a real model: asked for "폭 600mm, 높이 800mm", the conversation's model passed the step writer
|
|
254
|
+
"폭 Y=600mm, 높이 Z=800mm" -- told to name sizes by axis, it guessed which was up, and the writer's Y is up. The
|
|
255
|
+
person's words go to the writer as they are; the writer knows the figure's axes.
|
|
256
|
+
*/
|
|
257
|
+
it('넘기는 요청은 사람의 말 그대로 — 축을 붙여 옮기지 않는다', () => {
|
|
258
|
+
const category = getToolCategories().find((one: any) => one.name === 'figure-authoring')!
|
|
259
|
+
const prompt = category.specs.find((spec: any) => spec.name === 'proposeFigureCandidate').schema.properties.prompt.description
|
|
260
|
+
|
|
261
|
+
expect(category.guidance).toMatch(/pass the person's words as they are/)
|
|
262
|
+
expect(category.guidance).toMatch(/Y is up/)
|
|
263
|
+
expect(category.guidance).not.toMatch(/Name coordinates and sizes by their X\/Y\/Z axes/)
|
|
264
|
+
expect(prompt).toMatch(/do not turn sizes into axes/)
|
|
265
|
+
})
|
|
266
|
+
|
|
252
267
|
it('타입 이름을 지어내지 말라고 말한다 — 저장되는 식별자다', () => {
|
|
253
268
|
const category = getToolCategories().find((one: any) => one.name === 'figure-authoring')!
|
|
254
269
|
|
|
@@ -432,7 +432,7 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
432
432
|
properties: {
|
|
433
433
|
prompt: {
|
|
434
434
|
type: 'string',
|
|
435
|
-
description: 'What to make or change, in the words the author used.'
|
|
435
|
+
description: 'What to make or change, in the words the author used, with the sizes as they said them: do not turn sizes into axes.'
|
|
436
436
|
},
|
|
437
437
|
figureId: {
|
|
438
438
|
type: 'string',
|
|
@@ -596,7 +596,7 @@ export function registerFigureToolCategories(): void {
|
|
|
596
596
|
' A proposal is a list of modeller commands. The modeller applies it as one step only when the person runs it from the dock card, and one undo takes it all back. Do not say the figure has changed before that.',
|
|
597
597
|
' A revision can be proposed only for the figure open in the modeller. Say what the result\'s `steps` do as they are; do not invent changes the commands do not make.',
|
|
598
598
|
'- Never save the source yourself. Propose; the author decides on the screen whether to take it.',
|
|
599
|
-
'-
|
|
599
|
+
'- In the prompt to `proposeFigureCandidate`, pass the person\'s words as they are: their sizes, counts and names. Do not turn them into axes; the writer knows the figure\'s axes. When you name an axis to the person, the figure\'s Y is up.',
|
|
600
600
|
'- **For a request to change a figure, call `getFigureSource` first.** Revising without reading proposes against a guess, and a renamed part breaks the bindings of the boards it is already placed on.',
|
|
601
601
|
'- On the catalog nothing is open. To read an existing figure, find the id of the name or type the person said with `listFigures`; ask which figure only when it is ambiguous. To revise an existing figure, tell the person to open it in the modeller and ask there. For a new figure, do not read existing sources: call `proposeFigureCandidate` with create: true.',
|
|
602
602
|
'- Do not invent type names; check them with `listFigures`. **A type name is a stored identifier**, and boards find the figure by it. A new figure\'s type is set by a person at its first save.',
|
package/translations/en.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"error.figure-proposal-failed": "The figure could not be made.",
|
|
3
3
|
"error.figure-release-does-not-compile": "the released source of {type} (version {version}) does not compile. open it in the modeller, fix it and release it again before it draws on a board.",
|
|
4
|
-
"figure.gate.occupancy-empty-axis.message": "The
|
|
5
|
-
"figure.gate.occupancy-undeclared.how": "Use \"fit to parts\" to
|
|
6
|
-
"figure.gate.occupancy-undeclared.how-no-parts": "Add a shape first, then use \"fit to parts\" to
|
|
7
|
-
"figure.gate.occupancy-undeclared.message": "
|
|
8
|
-
"figure.gate.occupancy-undeclared.why": "
|
|
9
|
-
"figure.gate.part-outside-occupancy.how": "Use \"fit to parts\" to
|
|
10
|
-
"figure.gate.part-outside-occupancy.message": "'{part}' goes {mm} mm past the
|
|
11
|
-
"figure.gate.part-outside-occupancy.why": "The
|
|
4
|
+
"figure.gate.occupancy-empty-axis.message": "The space the figure takes is empty on {axis}.",
|
|
5
|
+
"figure.gate.occupancy-undeclared.how": "Use \"fit to parts\" to set the space the figure takes from the parts as they are now.",
|
|
6
|
+
"figure.gate.occupancy-undeclared.how-no-parts": "Add a shape first, then use \"fit to parts\" to set the space the figure takes from it.",
|
|
7
|
+
"figure.gate.occupancy-undeclared.message": "The space this figure takes is not set yet.",
|
|
8
|
+
"figure.gate.occupancy-undeclared.why": "The space a figure takes sets the scale it is placed at on a board.",
|
|
9
|
+
"figure.gate.part-outside-occupancy.how": "Use \"fit to parts\" to fit the space the figure takes to the parts again, or move '{part}' {mm} mm inward on {axis}.",
|
|
10
|
+
"figure.gate.part-outside-occupancy.message": "'{part}' goes {mm} mm past the space the figure takes on {axis}.",
|
|
11
|
+
"figure.gate.part-outside-occupancy.why": "The space the figure takes sets the scale it is placed at. What lies past it is left out of that scale, so on a board this part lands outside the place given to the figure.",
|
|
12
12
|
"figure.gate.parts-off-placement-face.how": "Move '{part}' {mm} mm on Y so the face meets the plane, or choose a different mounting face.",
|
|
13
13
|
"figure.gate.parts-off-placement-face.message": "The mounting face of '{part}' is {mm} mm off the plane the figure stands on.",
|
|
14
14
|
"figure.gate.parts-off-placement-face.why": "The figure is declared to stand on that face. If the face does not reach the plane, the figure floats on the board by that much, and whatever is placed on it floats too.",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"figure.gate.placement-face-tilted.why": "A tilted face cannot lie on the plane, so how the figure stands is not settled.",
|
|
24
24
|
"figure.gate.state-range-sampled.how": "To cover the whole range, re-author what could not be proven.",
|
|
25
25
|
"figure.gate.state-range-sampled.message": "The whole range of sizes and motion could not be proven, so {states} combinations of size and pose were checked.",
|
|
26
|
-
"figure.gate.state-range-sampled.why": "Between the combinations that were checked, a part may still go past the
|
|
26
|
+
"figure.gate.state-range-sampled.why": "Between the combinations that were checked, a part may still go past the space the figure takes.",
|
|
27
27
|
"figure.gate.type-is-placeholder.how": "Set the type in the type field, then publish.",
|
|
28
28
|
"figure.gate.type-is-placeholder.message": "'{type}' is a temporary type given when the figure was first saved.",
|
|
29
29
|
"figure.gate.type-is-placeholder.why": "Boards call a figure by its type, and it cannot change once the figure is published.",
|
package/translations/ko.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"error.figure-proposal-failed": "도형을 만들지 못했습니다.",
|
|
3
3
|
"error.figure-release-does-not-compile": "{type} 의 발행판({version})이 지금 컴파일되지 않습니다. 저작 화면에서 열어 수정한 후 다시 발행해야 보드에 나옵니다.",
|
|
4
|
-
"figure.gate.occupancy-empty-axis.message": "
|
|
5
|
-
"figure.gate.occupancy-undeclared.how": "「부품에 맞추기」를 누르면 지금 부품으로 공간을
|
|
6
|
-
"figure.gate.occupancy-undeclared.how-no-parts": "먼저 도형을 추가한 뒤, 「부품에 맞추기」로 공간을
|
|
7
|
-
"figure.gate.occupancy-undeclared.message": "이 도형은 차지하는 공간을 아직
|
|
8
|
-
"figure.gate.occupancy-undeclared.why": "
|
|
9
|
-
"figure.gate.part-outside-occupancy.how": "「부품에 맞추기」로 공간을
|
|
10
|
-
"figure.gate.part-outside-occupancy.message": "'{part}' 부품이
|
|
11
|
-
"figure.gate.part-outside-occupancy.why": "
|
|
4
|
+
"figure.gate.occupancy-empty-axis.message": "차지하는 공간의 {axis} 가 비어 있습니다.",
|
|
5
|
+
"figure.gate.occupancy-undeclared.how": "「부품에 맞추기」를 누르면 지금 부품으로 차지하는 공간을 정합니다.",
|
|
6
|
+
"figure.gate.occupancy-undeclared.how-no-parts": "먼저 도형을 추가한 뒤, 「부품에 맞추기」로 차지하는 공간을 정하세요.",
|
|
7
|
+
"figure.gate.occupancy-undeclared.message": "이 도형은 차지하는 공간을 아직 정하지 않았습니다.",
|
|
8
|
+
"figure.gate.occupancy-undeclared.why": "도면에 놓일 때의 배율은 차지하는 공간으로 정합니다.",
|
|
9
|
+
"figure.gate.part-outside-occupancy.how": "「부품에 맞추기」로 차지하는 공간을 지금 부품에 다시 맞추거나, '{part}' 부품을 {axis} 로 {mm}mm 안으로 옮기세요.",
|
|
10
|
+
"figure.gate.part-outside-occupancy.message": "도형이 차지하는 공간을 '{part}' 부품이 {axis} 로 {mm}mm 넘습니다.",
|
|
11
|
+
"figure.gate.part-outside-occupancy.why": "도면에 놓일 때의 배율은 차지하는 공간으로 정합니다. 넘은 만큼은 그 배율에 들어가지 않아서, 도면에서 이 부품이 도형에 주어진 자리 밖으로 나갑니다.",
|
|
12
12
|
"figure.gate.parts-off-placement-face.how": "'{part}' 부품을 Y 로 {mm}mm 옮겨 그 면을 평면에 맞추거나, 장착면을 다른 면으로 다시 정하세요.",
|
|
13
13
|
"figure.gate.parts-off-placement-face.message": "장착면으로 정한 '{part}' 부품의 면이 놓이는 평면에서 {mm}mm 떨어져 있습니다.",
|
|
14
14
|
"figure.gate.parts-off-placement-face.why": "도형은 그 면으로 놓인다고 선언돼 있습니다. 닿지 않으면 도면에서 그만큼 떠 보이고, 자리에 놓인 물건도 함께 뜹니다.",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"figure.gate.placement-face-tilted.why": "기운 면은 평면에 닿을 수 없어서, 도형이 어떤 자세로 놓이는지 정해지지 않습니다.",
|
|
24
24
|
"figure.gate.state-range-sampled.how": "범위 전체를 보장하려면 증명하지 못한 항목을 다시 저작하세요.",
|
|
25
25
|
"figure.gate.state-range-sampled.message": "크기와 움직임의 범위 전체를 증명하지 못해 크기·자세 조합 {states}개만 검사했습니다.",
|
|
26
|
-
"figure.gate.state-range-sampled.why": "검사한 조합 사이에서 부품이 공간
|
|
26
|
+
"figure.gate.state-range-sampled.why": "검사한 조합 사이에서 부품이 도형이 차지하는 공간 밖으로 나갈 수 있습니다.",
|
|
27
27
|
"figure.gate.type-is-placeholder.how": "타입 항목에 도형 타입을 지정한 후 발행해 주세요.",
|
|
28
28
|
"figure.gate.type-is-placeholder.message": "'{type}' 타입은 처음 저장할 때 붙은 임시 타입입니다.",
|
|
29
29
|
"figure.gate.type-is-placeholder.why": "보드는 이 타입으로 도형을 부르고, 발행한 뒤에는 바꿀 수 없습니다.",
|