@operato/scene-storage 10.0.0-beta.41 → 10.0.0-beta.43
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/CHANGELOG.md +20 -0
- package/MIGRATION-plan-a-slot-api.md +266 -0
- package/PLAN-A-rack-as-slot-holder.md +164 -0
- package/dist/crane.js +1 -1
- package/dist/crane.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/parcel-3d.js +42 -9
- package/dist/parcel-3d.js.map +1 -1
- package/dist/rack-grid-3d.d.ts +18 -7
- package/dist/rack-grid-3d.js +372 -69
- package/dist/rack-grid-3d.js.map +1 -1
- package/dist/rack-grid-cell.d.ts +21 -72
- package/dist/rack-grid-cell.js +147 -243
- package/dist/rack-grid-cell.js.map +1 -1
- package/dist/rack-grid.d.ts +277 -56
- package/dist/rack-grid.js +1230 -695
- package/dist/rack-grid.js.map +1 -1
- package/dist/rack-materials.d.ts +9 -0
- package/dist/rack-materials.js +55 -0
- package/dist/rack-materials.js.map +1 -0
- package/dist/storage-rack-3d.d.ts +15 -0
- package/dist/storage-rack-3d.js +131 -30
- package/dist/storage-rack-3d.js.map +1 -1
- package/dist/storage-rack.d.ts +242 -45
- package/dist/storage-rack.js +684 -106
- package/dist/storage-rack.js.map +1 -1
- package/package.json +3 -3
- package/src/crane.ts +1 -1
- package/src/index.ts +3 -4
- package/src/parcel-3d.ts +41 -9
- package/src/rack-grid-3d.ts +383 -80
- package/src/rack-grid-cell.ts +161 -305
- package/src/rack-grid.ts +1263 -762
- package/src/rack-materials.ts +61 -0
- package/src/storage-rack-3d.ts +144 -30
- package/src/storage-rack.ts +763 -111
- package/test/test-carrier-lifecycle.ts +361 -0
- package/test/test-coord-alignment.ts +201 -0
- package/test/test-external-to-rack.ts +461 -0
- package/test/test-mover-concurrent-bug.ts +304 -0
- package/test/test-mover-rollback.ts +290 -0
- package/test/test-r19-place-absorb.ts +174 -0
- package/test/test-rack-3d-attach-real.ts +301 -0
- package/test/test-rack-concurrent.ts +254 -0
- package/test/test-rack-edge-cases.ts +323 -0
- package/test/test-rack-grid-cell.ts +318 -0
- package/test/test-rack-grid-location.ts +657 -0
- package/test/test-real-3d-positioning.ts +158 -0
- package/test/test-slot-center-convention.ts +116 -0
- package/test/test-slot-target.ts +189 -0
- package/test/test-storage-rack-batched.ts +606 -0
- package/test/test-storage-rack-click.ts +329 -0
- package/test/test-storage-rack-slot-api.ts +357 -0
- package/test/test-toscene-convention.ts +162 -0
- package/test/test-user-scenario-sequential.ts +334 -0
- package/translations/en.json +2 -0
- package/translations/ja.json +2 -0
- package/translations/ko.json +2 -0
- package/translations/ms.json +2 -0
- package/translations/zh.json +2 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/rack-column.d.ts +0 -35
- package/dist/rack-column.js +0 -258
- package/dist/rack-column.js.map +0 -1
- package/dist/rack-grid-helpers.d.ts +0 -28
- package/dist/rack-grid-helpers.js +0 -71
- package/dist/rack-grid-helpers.js.map +0 -1
- package/dist/rack-grid-location.d.ts +0 -37
- package/dist/rack-grid-location.js +0 -227
- package/dist/rack-grid-location.js.map +0 -1
- package/dist/storage-cell-3d.d.ts +0 -25
- package/dist/storage-cell-3d.js +0 -88
- package/dist/storage-cell-3d.js.map +0 -1
- package/dist/storage-cell.d.ts +0 -73
- package/dist/storage-cell.js +0 -215
- package/dist/storage-cell.js.map +0 -1
- package/src/rack-column.ts +0 -340
- package/src/rack-grid-helpers.ts +0 -77
- package/src/rack-grid-location.ts +0 -286
- package/src/storage-cell-3d.ts +0 -101
- package/src/storage-cell.ts +0 -267
- package/test/test-cell-position.ts +0 -105
- 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
|
+
})
|