@operato/scene-storage 10.0.0-beta.41 → 10.0.0-beta.42

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 (82) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/MIGRATION-plan-a-slot-api.md +266 -0
  3. package/PLAN-A-rack-as-slot-holder.md +164 -0
  4. package/dist/crane.js +1 -1
  5. package/dist/crane.js.map +1 -1
  6. package/dist/index.d.ts +3 -4
  7. package/dist/index.js +1 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/rack-grid-3d.d.ts +18 -7
  10. package/dist/rack-grid-3d.js +372 -69
  11. package/dist/rack-grid-3d.js.map +1 -1
  12. package/dist/rack-grid-cell.d.ts +21 -72
  13. package/dist/rack-grid-cell.js +147 -243
  14. package/dist/rack-grid-cell.js.map +1 -1
  15. package/dist/rack-grid.d.ts +277 -56
  16. package/dist/rack-grid.js +1230 -695
  17. package/dist/rack-grid.js.map +1 -1
  18. package/dist/rack-materials.d.ts +9 -0
  19. package/dist/rack-materials.js +55 -0
  20. package/dist/rack-materials.js.map +1 -0
  21. package/dist/storage-rack-3d.d.ts +15 -0
  22. package/dist/storage-rack-3d.js +131 -30
  23. package/dist/storage-rack-3d.js.map +1 -1
  24. package/dist/storage-rack.d.ts +242 -45
  25. package/dist/storage-rack.js +684 -106
  26. package/dist/storage-rack.js.map +1 -1
  27. package/package.json +3 -3
  28. package/src/crane.ts +1 -1
  29. package/src/index.ts +3 -4
  30. package/src/rack-grid-3d.ts +383 -80
  31. package/src/rack-grid-cell.ts +161 -305
  32. package/src/rack-grid.ts +1263 -762
  33. package/src/rack-materials.ts +61 -0
  34. package/src/storage-rack-3d.ts +144 -30
  35. package/src/storage-rack.ts +763 -111
  36. package/test/test-carrier-lifecycle.ts +361 -0
  37. package/test/test-coord-alignment.ts +201 -0
  38. package/test/test-external-to-rack.ts +461 -0
  39. package/test/test-mover-concurrent-bug.ts +304 -0
  40. package/test/test-mover-rollback.ts +290 -0
  41. package/test/test-r19-place-absorb.ts +174 -0
  42. package/test/test-rack-3d-attach-real.ts +301 -0
  43. package/test/test-rack-concurrent.ts +254 -0
  44. package/test/test-rack-edge-cases.ts +323 -0
  45. package/test/test-rack-grid-cell.ts +318 -0
  46. package/test/test-rack-grid-location.ts +657 -0
  47. package/test/test-real-3d-positioning.ts +158 -0
  48. package/test/test-slot-center-convention.ts +116 -0
  49. package/test/test-slot-target.ts +189 -0
  50. package/test/test-storage-rack-batched.ts +606 -0
  51. package/test/test-storage-rack-click.ts +329 -0
  52. package/test/test-storage-rack-slot-api.ts +357 -0
  53. package/test/test-toscene-convention.ts +162 -0
  54. package/test/test-user-scenario-sequential.ts +334 -0
  55. package/translations/en.json +2 -0
  56. package/translations/ja.json +2 -0
  57. package/translations/ko.json +2 -0
  58. package/translations/ms.json +2 -0
  59. package/translations/zh.json +2 -0
  60. package/tsconfig.tsbuildinfo +1 -1
  61. package/dist/rack-column.d.ts +0 -35
  62. package/dist/rack-column.js +0 -258
  63. package/dist/rack-column.js.map +0 -1
  64. package/dist/rack-grid-helpers.d.ts +0 -28
  65. package/dist/rack-grid-helpers.js +0 -71
  66. package/dist/rack-grid-helpers.js.map +0 -1
  67. package/dist/rack-grid-location.d.ts +0 -37
  68. package/dist/rack-grid-location.js +0 -227
  69. package/dist/rack-grid-location.js.map +0 -1
  70. package/dist/storage-cell-3d.d.ts +0 -25
  71. package/dist/storage-cell-3d.js +0 -88
  72. package/dist/storage-cell-3d.js.map +0 -1
  73. package/dist/storage-cell.d.ts +0 -73
  74. package/dist/storage-cell.js +0 -215
  75. package/dist/storage-cell.js.map +0 -1
  76. package/src/rack-column.ts +0 -340
  77. package/src/rack-grid-helpers.ts +0 -77
  78. package/src/rack-grid-location.ts +0 -286
  79. package/src/storage-cell-3d.ts +0 -101
  80. package/src/storage-cell.ts +0 -267
  81. package/test/test-cell-position.ts +0 -105
  82. package/test/test-rack-grid.ts +0 -77
@@ -0,0 +1,657 @@
1
+ /*
2
+ * RackGrid — location 룰 / cellOverrides / bulk 편집 단위 테스트.
3
+ *
4
+ * 검증 대상:
5
+ * - locationOf / cellIdOfLocation 양방향 변환 (룰만, override 없는 경우 null)
6
+ * - cellOverrides 의 section/unit 명시되어야 location 부여 (B 정책)
7
+ * - shelfLabels (shelfLocations 의 빈 자리 1-based fallback)
8
+ * - locPattern 변경 시 location 표현 갱신 + cellId 안정
9
+ * - isEmpty cell 의 location 미부여
10
+ * - bulk: setCellOverride / clearCellOverride / setIsEmpty / setBorder
11
+ * - increaseLocation: cw/ccw/zigzag/zigzag-reverse, skipNumbering, aisle 분리
12
+ *
13
+ * 진짜 RackGrid 인스턴스화는 things-scene full pipeline 필요해서 비현실적. 기존
14
+ * MiniRack 패턴처럼 *알고리즘 인라인 재구현* 한 MiniGrid 로 검증 — RackGrid 의 진짜
15
+ * 알고리즘이 동일한지는 *시각적 코드 리뷰 + 빌드* + *통합 테스트* 로 보완.
16
+ */
17
+
18
+ import 'should'
19
+
20
+ // ── Fixture: RackGrid 의 핵심 알고리즘만 격리한 MiniGrid ──────────────────────
21
+
22
+ interface CellOverride {
23
+ section?: string
24
+ unit?: string
25
+ isEmpty?: boolean
26
+ border?: any
27
+ merged?: boolean
28
+ }
29
+
30
+ class MiniGrid {
31
+ state: any
32
+ private _locationIndexCache?: Map<string, string>
33
+
34
+ constructor(initialState: any = {}) {
35
+ this.state = {
36
+ columns: 5,
37
+ rows: 1,
38
+ shelves: 4,
39
+ sectionDigits: 2,
40
+ unitDigits: 2,
41
+ locPattern: '{z}{s}-{u}-{sh}',
42
+ zone: 'Z',
43
+ cellOverrides: {},
44
+ ...initialState
45
+ }
46
+ }
47
+
48
+ setState(partial: any) {
49
+ this.state = { ...this.state, ...partial }
50
+ // RackGrid 의 onchange* 콜백 시뮬
51
+ if ('cellOverrides' in partial || 'locPattern' in partial ||
52
+ 'shelfLocations' in partial || 'zone' in partial ||
53
+ 'sectionDigits' in partial || 'unitDigits' in partial ||
54
+ 'shelves' in partial) {
55
+ this._locationIndexCache = undefined
56
+ }
57
+ }
58
+
59
+ get columns(): number { return Math.max(1, Math.floor(this.state.columns ?? 5)) }
60
+ get rackRows(): number { return Math.max(1, Math.floor(this.state.rows ?? 1)) }
61
+ get shelves(): number { return Math.max(1, Math.floor(this.state.shelves ?? 4)) }
62
+ get cellOverrides(): { [posKey: string]: CellOverride } { return this.state.cellOverrides ?? {} }
63
+
64
+ get shelfLabels(): string[] {
65
+ const input = this.state.shelfLocations
66
+ const levels = this.shelves
67
+ const parts = (input || '').split(',')
68
+ const out: string[] = []
69
+ for (let i = 0; i < levels; i++) {
70
+ const p = parts[i]
71
+ out[i] = p && p.trim().length > 0 ? p.trim() : String(i + 1)
72
+ }
73
+ return out
74
+ }
75
+
76
+ parseCellId(cellId: string): { col: number; row: number; shelf: number } | null {
77
+ const parts = cellId.split('-')
78
+ if (parts.length !== 3) return null
79
+ const [c, r, s] = parts.map(Number)
80
+ if (!Number.isFinite(c) || !Number.isFinite(r) || !Number.isFinite(s)) return null
81
+ return { col: c, row: r, shelf: s }
82
+ }
83
+
84
+ parsePosKey(posKey: string): { col: number; row: number } | null {
85
+ const parts = posKey.split('-')
86
+ if (parts.length !== 2) return null
87
+ const [c, r] = parts.map(Number)
88
+ if (!Number.isFinite(c) || !Number.isFinite(r)) return null
89
+ return { col: c, row: r }
90
+ }
91
+
92
+ locationOf(cellId: string): string | null {
93
+ const parsed = this.parseCellId(cellId)
94
+ if (!parsed) return null
95
+ const posKey = `${parsed.col}-${parsed.row}`
96
+ const override = this.cellOverrides[posKey]
97
+ if (!override?.section || !override?.unit || override.isEmpty) return null
98
+
99
+ const pattern = this.state.locPattern ?? '{z}{s}-{u}-{sh}'
100
+ const zone = this.state.zone ?? ''
101
+ const shelfLabel = this.shelfLabels[parsed.shelf] ?? String(parsed.shelf + 1)
102
+
103
+ return pattern
104
+ .replace(/\{z\}/g, zone)
105
+ .replace(/\{s\}/g, override.section)
106
+ .replace(/\{u\}/g, override.unit)
107
+ .replace(/\{sh\}/g, shelfLabel)
108
+ }
109
+
110
+ cellIdOfLocation(location: string): string | null {
111
+ return this._locationIndex.get(location) ?? null
112
+ }
113
+
114
+ private get _locationIndex(): Map<string, string> {
115
+ if (this._locationIndexCache) return this._locationIndexCache
116
+ const map = new Map<string, string>()
117
+ const overrides = this.cellOverrides
118
+ const shelves = this.shelves
119
+ for (const posKey of Object.keys(overrides)) {
120
+ const override = overrides[posKey]
121
+ if (!override.section || !override.unit || override.isEmpty) continue
122
+ const parsed = this.parsePosKey(posKey)
123
+ if (!parsed) continue
124
+ for (let shelfIdx = 0; shelfIdx < shelves; shelfIdx++) {
125
+ const cellId = `${parsed.col}-${parsed.row}-${shelfIdx}`
126
+ const loc = this.locationOf(cellId)
127
+ if (loc) map.set(loc, cellId)
128
+ }
129
+ }
130
+ this._locationIndexCache = map
131
+ return map
132
+ }
133
+
134
+ setCellOverride(posKey: string, partial: Partial<CellOverride>): void {
135
+ const current = this.cellOverrides
136
+ const existing = current[posKey] ?? {}
137
+ const merged: any = { ...existing }
138
+ for (const [k, v] of Object.entries(partial)) {
139
+ if (v === undefined) delete merged[k]
140
+ else merged[k] = v
141
+ }
142
+ const next = { ...current }
143
+ if (Object.keys(merged).length === 0) delete next[posKey]
144
+ else next[posKey] = merged
145
+ this.setState({ cellOverrides: next })
146
+ }
147
+
148
+ clearCellOverride(posKey: string): void {
149
+ const current = this.cellOverrides
150
+ if (!(posKey in current)) return
151
+ const next = { ...current }
152
+ delete next[posKey]
153
+ this.setState({ cellOverrides: next })
154
+ }
155
+
156
+ setIsEmpty(posKeys: string[], isEmpty: boolean): void {
157
+ const current = this.cellOverrides
158
+ const next = { ...current }
159
+ for (const posKey of posKeys) {
160
+ const existing = next[posKey] ?? {}
161
+ const merged: any = { ...existing, isEmpty }
162
+ if (!isEmpty && !existing.section && !existing.unit && !existing.border && !existing.merged) {
163
+ delete next[posKey]
164
+ } else {
165
+ next[posKey] = merged
166
+ }
167
+ }
168
+ this.setState({ cellOverrides: next })
169
+ }
170
+
171
+ setBorder(posKeys: string[], style: any, where: 'all' | 'top' | 'left' | 'bottom' | 'right' = 'all'): void {
172
+ const sides = where === 'all' ? ['top', 'left', 'bottom', 'right'] : [where]
173
+ const current = this.cellOverrides
174
+ const next = { ...current }
175
+ for (const posKey of posKeys) {
176
+ const existing = next[posKey] ?? {}
177
+ const border = { ...(existing.border ?? {}) }
178
+ for (const side of sides) {
179
+ if (style == null) delete border[side]
180
+ else border[side] = style
181
+ }
182
+ const merged: any = { ...existing, border }
183
+ if (Object.keys(border).length === 0) delete merged.border
184
+ next[posKey] = merged
185
+ }
186
+ this.setState({ cellOverrides: next })
187
+ }
188
+
189
+ increaseLocation(
190
+ posKeys: string[],
191
+ direction: 'cw' | 'ccw' | 'zigzag' | 'zigzag-reverse' = 'cw',
192
+ skipNumbering: boolean = false,
193
+ startSection: number = 1,
194
+ startUnit: number = 1
195
+ ): void {
196
+ const sectionDigits = Math.max(1, Math.floor(this.state.sectionDigits ?? 2))
197
+ const unitDigits = Math.max(1, Math.floor(this.state.unitDigits ?? 2))
198
+
199
+ const targets: string[] = posKeys.length > 0 ? [...posKeys] : this._allPosKeys()
200
+
201
+ const byRow: (string | null)[][] = []
202
+ let maxRow = -1
203
+ let maxCol = -1
204
+ for (const posKey of targets) {
205
+ const parsed = this.parsePosKey(posKey)
206
+ if (!parsed) continue
207
+ if (!byRow[parsed.row]) byRow[parsed.row] = []
208
+ byRow[parsed.row][parsed.col] = posKey
209
+ if (parsed.row > maxRow) maxRow = parsed.row
210
+ if (parsed.col > maxCol) maxCol = parsed.col
211
+ }
212
+
213
+ const overrides = this.cellOverrides
214
+ const isAisleRow = (row: number): boolean => {
215
+ const cells = byRow[row]
216
+ if (!cells || cells.length === 0) return true
217
+ return cells.every(p => p == null || overrides[p]?.isEmpty === true)
218
+ }
219
+
220
+ const sections: number[][] = []
221
+ let currentSection: number[] = []
222
+ for (let r = 0; r <= maxRow; r++) {
223
+ if (!byRow[r]) continue
224
+ if (isAisleRow(r)) {
225
+ if (currentSection.length > 0) { sections.push(currentSection); currentSection = [] }
226
+ } else {
227
+ currentSection.push(r)
228
+ }
229
+ }
230
+ if (currentSection.length > 0) sections.push(currentSection)
231
+
232
+ const next = { ...overrides }
233
+ let sectionNum = Number(startSection) || 1
234
+
235
+ const setSU = (posKey: string, section: number, unit: number) => {
236
+ const existing = next[posKey] ?? {}
237
+ next[posKey] = {
238
+ ...existing,
239
+ section: String(section).padStart(sectionDigits, '0'),
240
+ unit: String(unit).padStart(unitDigits, '0')
241
+ }
242
+ }
243
+ const clearSU = (posKey: string) => {
244
+ const existing = next[posKey]
245
+ if (!existing) return
246
+ const { section, unit, ...rest } = existing
247
+ if (Object.keys(rest).length === 0) delete next[posKey]
248
+ else next[posKey] = rest
249
+ }
250
+
251
+ for (const sectionRows of sections) {
252
+ for (const row of sectionRows) {
253
+ const cells = byRow[row]
254
+ if (!cells) continue
255
+ const orderedCols = this._orderCols(row, sectionRows, direction, maxCol)
256
+ let unitNum = Number(startUnit) || 1
257
+ for (const col of orderedCols) {
258
+ const posKey = cells[col]
259
+ if (!posKey) continue
260
+ const isEmpty = overrides[posKey]?.isEmpty === true
261
+ if (isEmpty) {
262
+ clearSU(posKey)
263
+ if (!skipNumbering) unitNum++
264
+ } else {
265
+ setSU(posKey, sectionNum, unitNum)
266
+ unitNum++
267
+ }
268
+ }
269
+ }
270
+ sectionNum++
271
+ }
272
+
273
+ this.setState({ cellOverrides: next })
274
+ }
275
+
276
+ private _allPosKeys(): string[] {
277
+ const cols = this.columns
278
+ const rows = this.rackRows
279
+ const out: string[] = []
280
+ for (let r = 0; r < rows; r++) {
281
+ for (let c = 0; c < cols; c++) {
282
+ out.push(`${c}-${r}`)
283
+ }
284
+ }
285
+ return out
286
+ }
287
+
288
+ private _orderCols(
289
+ row: number,
290
+ sectionRows: number[],
291
+ direction: 'cw' | 'ccw' | 'zigzag' | 'zigzag-reverse',
292
+ maxCol: number
293
+ ): number[] {
294
+ const idxInSection = sectionRows.indexOf(row)
295
+ const cols: number[] = []
296
+ for (let c = 0; c <= maxCol; c++) cols.push(c)
297
+
298
+ switch (direction) {
299
+ case 'cw': return idxInSection % 2 === 0 ? cols : cols.slice().reverse()
300
+ case 'ccw': return idxInSection % 2 === 0 ? cols.slice().reverse() : cols
301
+ case 'zigzag': return cols
302
+ case 'zigzag-reverse': return cols.slice().reverse()
303
+ }
304
+ }
305
+ }
306
+
307
+ // ── Group 1: shelfLabels ────────────────────────────────────────────────────
308
+
309
+ describe('RackGrid: shelfLabels', () => {
310
+ it('shelfLocations 미지정 → 1-based 인덱스 default', () => {
311
+ new MiniGrid({ shelves: 4 }).shelfLabels.should.deepEqual(['1', '2', '3', '4'])
312
+ })
313
+ it('완전 명시 라벨 그대로', () => {
314
+ new MiniGrid({ shelves: 3, shelfLocations: 'A,B,C' }).shelfLabels.should.deepEqual(['A', 'B', 'C'])
315
+ })
316
+ it('빈 자리는 default 로 fallback', () => {
317
+ new MiniGrid({ shelves: 4, shelfLocations: ',,,04' }).shelfLabels.should.deepEqual(['1', '2', '3', '04'])
318
+ })
319
+ it('입력 길이가 shelves 보다 짧으면 나머지 default', () => {
320
+ new MiniGrid({ shelves: 4, shelfLocations: 'A,B' }).shelfLabels.should.deepEqual(['A', 'B', '3', '4'])
321
+ })
322
+ })
323
+
324
+ // ── Group 2: locationOf — override 없으면 null (B 정책) ────────────────────
325
+
326
+ describe('RackGrid: locationOf', () => {
327
+ it('cellOverrides 비었으면 모든 cell location null', () => {
328
+ const g = new MiniGrid()
329
+ ;(g.locationOf('0-0-0') === null).should.be.true()
330
+ ;(g.locationOf('2-0-3') === null).should.be.true()
331
+ })
332
+
333
+ it('section/unit 명시된 cell 만 location 부여', () => {
334
+ const g = new MiniGrid({
335
+ cellOverrides: { '0-0': { section: '01', unit: '01' } }
336
+ })
337
+ g.locationOf('0-0-0').should.equal('Z01-01-1')
338
+ g.locationOf('0-0-1').should.equal('Z01-01-2')
339
+ g.locationOf('0-0-3').should.equal('Z01-01-4')
340
+ ;(g.locationOf('1-0-0') === null).should.be.true()
341
+ })
342
+
343
+ it('section 만 명시 / unit 만 명시 → 부여 안됨', () => {
344
+ const g = new MiniGrid({
345
+ cellOverrides: {
346
+ '0-0': { section: '01' },
347
+ '1-0': { unit: '05' }
348
+ }
349
+ })
350
+ ;(g.locationOf('0-0-0') === null).should.be.true()
351
+ ;(g.locationOf('1-0-0') === null).should.be.true()
352
+ })
353
+
354
+ it('isEmpty cell 은 section/unit 있어도 location null', () => {
355
+ const g = new MiniGrid({
356
+ cellOverrides: { '0-0': { section: '01', unit: '01', isEmpty: true } }
357
+ })
358
+ ;(g.locationOf('0-0-0') === null).should.be.true()
359
+ })
360
+
361
+ it('shelfLocations 가 location 의 {sh} 에 반영', () => {
362
+ const g = new MiniGrid({
363
+ shelves: 4,
364
+ shelfLocations: 'A,B,C,D',
365
+ cellOverrides: { '0-0': { section: '01', unit: '01' } }
366
+ })
367
+ g.locationOf('0-0-0').should.equal('Z01-01-A')
368
+ g.locationOf('0-0-3').should.equal('Z01-01-D')
369
+ })
370
+
371
+ it('locPattern 변경 → 표현만 바뀜 (cellId 안정)', () => {
372
+ const g = new MiniGrid({
373
+ cellOverrides: { '0-0': { section: '01', unit: '02' } }
374
+ })
375
+ g.locationOf('0-0-0').should.equal('Z01-02-1')
376
+ g.setState({ locPattern: '{z}-{s}.{u}.{sh}' })
377
+ g.locationOf('0-0-0').should.equal('Z-01.02.1')
378
+ })
379
+ })
380
+
381
+ // ── Group 3: cellIdOfLocation — 역변환 + sparse index ──────────────────────
382
+
383
+ describe('RackGrid: cellIdOfLocation', () => {
384
+ it('등록 안된 location → null', () => {
385
+ ;(new MiniGrid().cellIdOfLocation('Z01-01-1') === null).should.be.true()
386
+ })
387
+
388
+ it('section/unit override 등록 후 역변환 성공', () => {
389
+ const g = new MiniGrid({
390
+ cellOverrides: { '0-0': { section: '01', unit: '01' } }
391
+ })
392
+ g.cellIdOfLocation('Z01-01-1').should.equal('0-0-0')
393
+ g.cellIdOfLocation('Z01-01-2').should.equal('0-0-1')
394
+ g.cellIdOfLocation('Z01-01-4').should.equal('0-0-3')
395
+ })
396
+
397
+ it('locationOf 와 cellIdOfLocation 가 bijective', () => {
398
+ const g = new MiniGrid({
399
+ cellOverrides: {
400
+ '0-0': { section: '01', unit: '01' },
401
+ '1-0': { section: '01', unit: '02' },
402
+ '2-0': { section: '01', unit: '03' }
403
+ }
404
+ })
405
+ for (let col = 0; col < 3; col++) {
406
+ for (let shelf = 0; shelf < 4; shelf++) {
407
+ const cellId = `${col}-0-${shelf}`
408
+ const loc = g.locationOf(cellId)
409
+ ;(loc !== null).should.be.true()
410
+ g.cellIdOfLocation(loc!).should.equal(cellId)
411
+ }
412
+ }
413
+ })
414
+
415
+ it('isEmpty cell 의 location 은 역인덱스에 없음', () => {
416
+ const g = new MiniGrid({
417
+ cellOverrides: {
418
+ '0-0': { section: '01', unit: '01' },
419
+ '1-0': { section: '01', unit: '02', isEmpty: true }
420
+ }
421
+ })
422
+ g.cellIdOfLocation('Z01-01-1').should.equal('0-0-0')
423
+ ;(g.cellIdOfLocation('Z01-02-1') === null).should.be.true()
424
+ })
425
+
426
+ it('locPattern 변경 → 역인덱스 자동 재구축', () => {
427
+ const g = new MiniGrid({
428
+ cellOverrides: { '0-0': { section: '01', unit: '01' } }
429
+ })
430
+ g.cellIdOfLocation('Z01-01-1').should.equal('0-0-0')
431
+ g.setState({ locPattern: '{s}/{u}/{sh}' })
432
+ ;(g.cellIdOfLocation('Z01-01-1') === null).should.be.true()
433
+ g.cellIdOfLocation('01/01/1').should.equal('0-0-0')
434
+ })
435
+ })
436
+
437
+ // ── Group 4: setCellOverride / clearCellOverride ───────────────────────────
438
+
439
+ describe('RackGrid: setCellOverride / clearCellOverride', () => {
440
+ it('빈 cellOverrides 에 section/unit 추가', () => {
441
+ const g = new MiniGrid()
442
+ g.setCellOverride('0-0', { section: '01', unit: '01' })
443
+ g.cellOverrides['0-0'].should.deepEqual({ section: '01', unit: '01' })
444
+ g.locationOf('0-0-0').should.equal('Z01-01-1')
445
+ })
446
+
447
+ it('기존 override 에 partial 머지', () => {
448
+ const g = new MiniGrid({
449
+ cellOverrides: { '0-0': { section: '01', unit: '01' } }
450
+ })
451
+ g.setCellOverride('0-0', { isEmpty: true })
452
+ g.cellOverrides['0-0'].should.deepEqual({ section: '01', unit: '01', isEmpty: true })
453
+ })
454
+
455
+ it('partial 의 undefined 필드는 *해당 키 삭제*', () => {
456
+ const g = new MiniGrid({
457
+ cellOverrides: { '0-0': { section: '01', unit: '01', isEmpty: true } }
458
+ })
459
+ g.setCellOverride('0-0', { isEmpty: undefined })
460
+ g.cellOverrides['0-0'].should.deepEqual({ section: '01', unit: '01' })
461
+ })
462
+
463
+ it('모든 필드 삭제 시 override 자체 제거 (sparse 유지)', () => {
464
+ const g = new MiniGrid({
465
+ cellOverrides: { '0-0': { isEmpty: true } }
466
+ })
467
+ g.setCellOverride('0-0', { isEmpty: undefined })
468
+ Object.keys(g.cellOverrides).length.should.equal(0)
469
+ })
470
+
471
+ it('clearCellOverride — 한 위치 전체 제거', () => {
472
+ const g = new MiniGrid({
473
+ cellOverrides: {
474
+ '0-0': { section: '01', unit: '01' },
475
+ '1-0': { section: '01', unit: '02' }
476
+ }
477
+ })
478
+ g.clearCellOverride('0-0')
479
+ ;('0-0' in g.cellOverrides).should.be.false()
480
+ g.cellOverrides['1-0'].should.deepEqual({ section: '01', unit: '02' })
481
+ })
482
+
483
+ it('clearCellOverride → location 역인덱스 무효화', () => {
484
+ const g = new MiniGrid({
485
+ cellOverrides: { '0-0': { section: '01', unit: '01' } }
486
+ })
487
+ g.cellIdOfLocation('Z01-01-1').should.equal('0-0-0')
488
+ g.clearCellOverride('0-0')
489
+ ;(g.cellIdOfLocation('Z01-01-1') === null).should.be.true()
490
+ })
491
+ })
492
+
493
+ // ── Group 5: setIsEmpty ────────────────────────────────────────────────────
494
+
495
+ describe('RackGrid: setIsEmpty', () => {
496
+ it('여러 위치 동시 토글', () => {
497
+ const g = new MiniGrid()
498
+ g.setIsEmpty(['1-0', '3-0'], true)
499
+ g.cellOverrides['1-0'].isEmpty!.should.be.true()
500
+ g.cellOverrides['3-0'].isEmpty!.should.be.true()
501
+ })
502
+
503
+ it('isEmpty=false + 다른 필드 없으면 override 삭제 (sparse 유지)', () => {
504
+ const g = new MiniGrid({ cellOverrides: { '0-0': { isEmpty: true } } })
505
+ g.setIsEmpty(['0-0'], false)
506
+ ;('0-0' in g.cellOverrides).should.be.false()
507
+ })
508
+
509
+ it('isEmpty=false + section 있으면 override 유지', () => {
510
+ const g = new MiniGrid({
511
+ cellOverrides: { '0-0': { section: '01', unit: '01', isEmpty: true } }
512
+ })
513
+ g.setIsEmpty(['0-0'], false)
514
+ g.cellOverrides['0-0'].should.deepEqual({ section: '01', unit: '01', isEmpty: false })
515
+ })
516
+
517
+ it('isEmpty=true → 해당 cell location null', () => {
518
+ const g = new MiniGrid({
519
+ cellOverrides: { '0-0': { section: '01', unit: '01' } }
520
+ })
521
+ g.locationOf('0-0-0').should.equal('Z01-01-1')
522
+ g.setIsEmpty(['0-0'], true)
523
+ ;(g.locationOf('0-0-0') === null).should.be.true()
524
+ })
525
+ })
526
+
527
+ // ── Group 6: setBorder ─────────────────────────────────────────────────────
528
+
529
+ describe('RackGrid: setBorder', () => {
530
+ it("where='all' 4면 모두 같은 style", () => {
531
+ const g = new MiniGrid()
532
+ const style = { strokeStyle: '#000', lineWidth: 1, lineDash: 'solid' }
533
+ g.setBorder(['0-0'], style, 'all')
534
+ g.cellOverrides['0-0'].border!.should.deepEqual({
535
+ top: style, left: style, bottom: style, right: style
536
+ })
537
+ })
538
+
539
+ it("where='top' 만 설정", () => {
540
+ const g = new MiniGrid()
541
+ const style = { strokeStyle: '#000', lineWidth: 1, lineDash: 'solid' }
542
+ g.setBorder(['0-0'], style, 'top')
543
+ g.cellOverrides['0-0'].border!.should.deepEqual({ top: style })
544
+ })
545
+
546
+ it('style=null → 해당 side 삭제', () => {
547
+ const g = new MiniGrid({
548
+ cellOverrides: {
549
+ '0-0': { border: { top: 'x', left: 'y', bottom: 'z', right: 'w' } }
550
+ }
551
+ })
552
+ g.setBorder(['0-0'], null, 'top')
553
+ g.cellOverrides['0-0'].border!.should.deepEqual({ left: 'y', bottom: 'z', right: 'w' })
554
+ })
555
+
556
+ it('border 다 삭제되면 border 필드 자체 제거', () => {
557
+ const g = new MiniGrid({
558
+ cellOverrides: { '0-0': { section: '01', unit: '01', border: { top: 'x' } } }
559
+ })
560
+ g.setBorder(['0-0'], null, 'top')
561
+ ;('border' in g.cellOverrides['0-0']).should.be.false()
562
+ g.cellOverrides['0-0'].section!.should.equal('01')
563
+ })
564
+ })
565
+
566
+ // ── Group 7: increaseLocation — cw / ccw / aisle / skipNumbering ───────────
567
+
568
+ describe('RackGrid: increaseLocation', () => {
569
+ it("cw — 단일 row, 좌→우 순서로 unit 증가", () => {
570
+ const g = new MiniGrid({ columns: 4, rows: 1 })
571
+ g.increaseLocation([], 'cw', false, 1, 1)
572
+ g.cellOverrides['0-0'].should.deepEqual({ section: '01', unit: '01' })
573
+ g.cellOverrides['1-0'].should.deepEqual({ section: '01', unit: '02' })
574
+ g.cellOverrides['2-0'].should.deepEqual({ section: '01', unit: '03' })
575
+ g.cellOverrides['3-0'].should.deepEqual({ section: '01', unit: '04' })
576
+ })
577
+
578
+ it("cw — 2 row grid, boustrophedon: row 0 좌→우, row 1 우→좌", () => {
579
+ const g = new MiniGrid({ columns: 3, rows: 2 })
580
+ g.increaseLocation([], 'cw', false, 1, 1)
581
+ g.cellOverrides['0-0'].unit!.should.equal('01')
582
+ g.cellOverrides['1-0'].unit!.should.equal('02')
583
+ g.cellOverrides['2-0'].unit!.should.equal('03')
584
+ g.cellOverrides['2-1'].unit!.should.equal('01')
585
+ g.cellOverrides['1-1'].unit!.should.equal('02')
586
+ g.cellOverrides['0-1'].unit!.should.equal('03')
587
+ })
588
+
589
+ it("ccw — 반대 방향", () => {
590
+ const g = new MiniGrid({ columns: 3, rows: 1 })
591
+ g.increaseLocation([], 'ccw', false, 1, 1)
592
+ g.cellOverrides['2-0'].unit!.should.equal('01')
593
+ g.cellOverrides['1-0'].unit!.should.equal('02')
594
+ g.cellOverrides['0-0'].unit!.should.equal('03')
595
+ })
596
+
597
+ it("aisle row 가 section 경계 — section 번호 +1", () => {
598
+ const g = new MiniGrid({
599
+ columns: 3, rows: 3,
600
+ cellOverrides: {
601
+ '0-1': { isEmpty: true },
602
+ '1-1': { isEmpty: true },
603
+ '2-1': { isEmpty: true }
604
+ }
605
+ })
606
+ g.increaseLocation([], 'cw', false, 1, 1)
607
+ g.cellOverrides['0-0'].section!.should.equal('01')
608
+ g.cellOverrides['0-0'].unit!.should.equal('01')
609
+ g.cellOverrides['0-2'].section!.should.equal('02')
610
+ })
611
+
612
+ it("skipNumbering=true 면 isEmpty cell 의 unit 건너뜀", () => {
613
+ const g = new MiniGrid({
614
+ columns: 4, rows: 1,
615
+ cellOverrides: { '2-0': { isEmpty: true } }
616
+ })
617
+ g.increaseLocation([], 'cw', true, 1, 1)
618
+ g.cellOverrides['0-0'].unit!.should.equal('01')
619
+ g.cellOverrides['1-0'].unit!.should.equal('02')
620
+ ;('section' in (g.cellOverrides['2-0'] || {})).should.be.false()
621
+ g.cellOverrides['3-0'].unit!.should.equal('03')
622
+ })
623
+
624
+ it("skipNumbering=false 면 isEmpty 도 번호 증가시킴", () => {
625
+ const g = new MiniGrid({
626
+ columns: 4, rows: 1,
627
+ cellOverrides: { '2-0': { isEmpty: true } }
628
+ })
629
+ g.increaseLocation([], 'cw', false, 1, 1)
630
+ g.cellOverrides['0-0'].unit!.should.equal('01')
631
+ g.cellOverrides['1-0'].unit!.should.equal('02')
632
+ g.cellOverrides['3-0'].unit!.should.equal('04')
633
+ })
634
+
635
+ it("padding 정확 — sectionDigits=3 → '001'", () => {
636
+ const g = new MiniGrid({ columns: 2, rows: 1, sectionDigits: 3, unitDigits: 3 })
637
+ g.increaseLocation([], 'cw', false, 1, 1)
638
+ g.cellOverrides['0-0'].section!.should.equal('001')
639
+ g.cellOverrides['0-0'].unit!.should.equal('001')
640
+ })
641
+
642
+ it("startSection / startUnit 적용", () => {
643
+ const g = new MiniGrid({ columns: 2, rows: 1 })
644
+ g.increaseLocation([], 'cw', false, 5, 10)
645
+ g.cellOverrides['0-0'].section!.should.equal('05')
646
+ g.cellOverrides['0-0'].unit!.should.equal('10')
647
+ })
648
+
649
+ it("posKeys 명시 시 그 cell 만 채번", () => {
650
+ const g = new MiniGrid({ columns: 4, rows: 1 })
651
+ g.increaseLocation(['0-0', '2-0'], 'cw', false, 1, 1)
652
+ g.cellOverrides['0-0'].unit!.should.equal('01')
653
+ g.cellOverrides['2-0'].unit!.should.equal('02')
654
+ ;('1-0' in g.cellOverrides).should.be.false()
655
+ ;('3-0' in g.cellOverrides).should.be.false()
656
+ })
657
+ })