/* * 현장의 조업 달력이 계획 조업 시간에 닿나. * * ── 왜 필요한가 (2026-09-07) ──────────────────────────────────────────────── * 계획 조업 시간(PBT)이 「재기 시작한 뒤 흐른 시간 전부」였다. 주말도 야간도 휴일도 계획 조업으로 * 세었다. [산술] 8시간 × 주5일이면 168시간 중 40시간이 조업이니 PBT 가 4.2배 크다. * * plant 레인이 같은 자리를 먼저 겪고 실측을 냈다 — 3교대를 물려받으면 `availability 0.4583`, * 달력이 아예 없으면 수를 안 낸다. 트윈은 창 전체를 계획으로 쳐서 수를 냈다. * * ── 이 시험이 지키는 것은 「자리가 있고 부르는 곳이 있다」다 ───────────────── * 오늘 이 저장소에서 그것을 여섯 번 놓쳤다. 값을 받을 칸을 만들고 아무도 채우지 않거나, 채우고 * 아무도 안 읽는 자리다. 그래서 여기서는 **누적기가 그 값을 실제로 쓰는지**를 본다 — 달력을 얹은 * 것과 안 얹은 것의 답이 달라야 한다. */ import test from 'node:test' import assert from 'node:assert/strict' import { OeeAccumulator } from '../server/engine/oee-accumulator.js' const HOUR = 3_600_000 const MON = Date.UTC(2026, 8, 7, 0, 0, 0) /** 08:00~16:00 만 조업 — 하루 열여섯 시간이 비근무다. */ const EIGHT_HOURS = [{ id: 'day', fromTime: '08:00', toTime: '16:00' }] as any[] /** 여덟 시간 내내 돈 설비 한 대. */ function ranAllShift(calendar?: any[]) { const acc = new OeeAccumulator() acc.setWorkCalendar(calendar ? { workCalendar: calendar } : undefined) /* * 봉투의 `data` 에 `moverId` 다 — `payload.data` 도 `equipmentId` 도 아니다. 처음 그렇게 써서 * 누적기가 아무것도 안 받았고, 시험 넷이 빨개져서 알았다. 읽어서 고쳤다(`oee-accumulator.ts:111`). */ acc.apply({ eventType: 'equipment.status', eventTime: new Date(MON + 8 * HOUR).toISOString(), data: { moverId: 'EQ-1', status: 'busy' } } as any) return acc.counters('EQ-1', MON + 24 * HOUR) } test('달력을 얹으면 계획 정지가 나온다 — 계측 창 안의 비근무만', () => { /* * ⚠ 하루가 아니다. 계측 창은 **첫 이벤트부터**이므로 08:00~24:00 의 열여섯 시간이고, 그중 조업이 * 08:00~16:00 이니 비근무가 여덟 시간이다. * * 처음 이 값을 열여섯으로 적었다 — 「하루 중 열여섯 시간이 비근무」라는 달력의 성질을 계측 창에 * 그대로 옮긴 것이다. 재보고 고쳤다. 계획 정지는 **달력의 성질이 아니라 이 창에서 잰 값**이다. */ const c = ranAllShift(EIGHT_HOURS) as any assert.equal(c.holdMs, 8 * HOUR) assert.equal(c.metricsSinceMs, MON + 8 * HOUR, '창이 첫 이벤트부터라는 것이 위 계산의 전제다') }) test('달력이 없으면 그 칸을 만들지 않는다 — 0 은 「쉬는 시간이 없다」는 주장이다', () => { const c = ranAllShift() as any assert.equal('holdMs' in c, false) }) test('얹은 것과 안 얹은 것의 답이 다르다 — 같으면 그 값이 안 쓰이는 것이다', () => { /* * 이 시험이 이 파일의 요점이다. 오늘 제가 낸 결함 중 하나가 「값을 넣어 줄 사람이 있는지 아무것도 * 검사하지 않은 시험」이었다 — 고쳐도 통과하고 안 고쳐도 통과했다. */ const withCal = ranAllShift(EIGHT_HOURS) as any const without = ranAllShift() as any assert.notEqual(withCal.holdMs, without.holdMs) }) test('실 생산 시간은 달력과 무관하다 — APT 는 잰 값이다', () => { /* * 달력은 PBT 를 정하고 APT 는 안 건드린다. 휴일에 돌린 열한 시간이 사라지던 것이 그 반대 방향의 * 결함이었고, 그것은 자르는 쪽이었다. */ const withCal = ranAllShift(EIGHT_HOURS) const without = ranAllShift() assert.equal(withCal.runMs, without.runMs) assert.equal(withCal.runMs, 16 * HOUR, '08:00 에 busy 로 들어가 24:00 까지면 열여섯 시간이다') }) test('잰 구간의 시작은 그대로다 — 그 칸에 파생값을 얹지 않는다', () => { /* * 같은 효과를 `metricsSinceMs` 를 당겨서 낼 수도 있는데, 그 칸은 「언제부터 쟀나」라는 사실이다. * plant 레인이 그 방식으로 하고 있었고 `holdMs` 로 옮겼다 — 수는 자릿수까지 같았다. */ const c = ranAllShift(EIGHT_HOURS) assert.equal(c.metricsSinceMs, MON + 8 * HOUR) }) /* * ── 설비의 달력이 현장의 것을 이긴다 ──────────────────────────────────────── * 이 자리가 실제 자료를 살렸다. rosarito 트윈들이 설비 41대에 3교대 × 평일 달력을 이미 갖고 있었는데, * 처음 배선할 때 **현장 달력만 읽었다.** 현장에 달력을 선언한 공간이 하나도 없어서, 있는 자료를 두고 * 계획 조업 시간이 창 전체로 남아 있었다. * * 사용자가 「모든 공간에 달력을 세우라」고 한 것이 그 말을 듣고 나온 지시였고, 재보니 세울 것이 아니라 * **읽을 것이 있었다.** */ const ROUND_CLOCK = [{ id: 'always', fromTime: '00:00', toTime: '24:00' }] as any[] function withModel(model: any) { const acc = new OeeAccumulator() acc.setWorkCalendar(model) acc.apply({ eventType: 'equipment.status', eventTime: new Date(MON + 8 * HOUR).toISOString(), data: { moverId: 'EQ-1', status: 'busy' } } as any) return acc.counters('EQ-1', MON + 24 * HOUR) as any } test('설비가 자기 달력을 선언하면 그것을 쓴다 — 24시간 도는 로가 현장 근무조에 안 잘린다', () => { const c = withModel({ workCalendar: EIGHT_HOURS, equipment: [{ id: 'EQ-1', workCalendar: ROUND_CLOCK }] }) assert.equal(c.holdMs, 0, '자기 달력이 종일 가동이면 계획 정지가 없다') }) test('설비가 선언하지 않으면 현장의 달력으로 떨어진다', () => { const c = withModel({ workCalendar: EIGHT_HOURS, equipment: [{ id: 'EQ-OTHER', workCalendar: ROUND_CLOCK }] }) assert.equal(c.holdMs, 8 * HOUR, '이 설비의 것이 없으니 현장 것을 쓴다') }) test('현장이 없고 설비만 있어도 답한다 — 지금 실제 자료가 그 모양이다', () => { /* * rosarito 가 이렇다: 공간에 달력이 없고 설비 41대에 3교대가 있다. 현장 달력만 읽으면 이 자료가 * 통째로 안 쓰인다. */ const c = withModel({ equipment: [{ id: 'EQ-1', workCalendar: EIGHT_HOURS }] }) assert.equal(c.holdMs, 8 * HOUR) }) test('둘 다 없으면 그 칸을 만들지 않는다', () => { const c = withModel({ equipment: [{ id: 'EQ-1' }] }) assert.equal('holdMs' in c, false) })