12. Order 서비스
client.order() 으로 접근합니다. 작업지시(Work Order)의 생성 → 시작 → 중단/재개 → 종료까지 전 생명주기를 ISA-88 기반 상태 머신으로 관리합니다.
11.1 작업지시 상태 (ISA-88 기반)
CREATE → WAIT ──start()──► START ──pause()──► PAUSED
│ │
│ resume()
│ │
▼ ▼
END ◄──end()──── START
│
└─ (또는) ──abort()──► ABORTED
상태 (status) | 의미 | 전이 메서드 | ISA-95 상태 (isa95_state) |
|---|---|---|---|
WAIT | 대기 (생성 직후 초기 상태) | create() 결과 | Ready |
START | 진행중 | start(), resume() | Running |
PAUSED | 일시중지 | pause() | Held |
END | 정상 완료 | end() | Completed |
ABORTED | 비정상 종료 | abort() | Aborted |
status 는 저장되는 값 그대로이고 이 다섯 가지뿐입니다. 시작하지 않은 채 취소한 워크오더는 새 상태가 아니라
ABORTED + abort_code=CANCELLED 입니다. 워크오더 행(/order·목록·/order/query)과 실적 문서·plantpulse.order.*
이벤트는 여기에 ISA-95 Part 4 작업 지시 상태 isa95_state 를 덧붙입니다 — 대응은 위 표 하나이고 모든 경로가 같은 값을 냅니다.
상태 전이가 불가능한 호출(예: 이미
START상태에서start()재호출)은 서버가 OK +data.success=false응답을 보내며, V5 메서드는false를 반환합니다.
11.2 메서드 일람
CRUD
| 메서드 | 반환 타입 | HTTP | 엔드포인트 |
|---|---|---|---|
create(OrderRequestV5) | OrderResponseV5 또는 null | POST | /api/v5/order |
update(order_id, OrderRequestV5) | OrderResponseV5 또는 null | PUT | /api/v5/order/{id} |
delete(order_id) | boolean | DELETE | /api/v5/order/{id} |
get(order_id) | OrderResponseV5 또는 null | GET | /api/v5/order/{id} |
list() | List<OrderResponseV5> | GET | /api/v5/order |
exists(order_id) | boolean | GET | /api/v5/order/{id}/exists |
라이프사이클 (모두 boolean 반환)
| 메서드 | HTTP | 엔드포인트 |
|---|---|---|
start(order_id) | POST | /api/v5/order/{id}/start |
end(order_id) | POST | /api/v5/order/{id}/end |
pause(order_id) | POST | /api/v5/order/{id}/pause |
resume(order_id) | POST | /api/v5/order/{id}/resume |
abort(order_id) | POST | /api/v5/order/{id}/abort |
11.3 OrderRequestV5 / OrderResponseV5
| 필드 | 타입 | 설명 |
|---|---|---|
order_id | String | 작업지시 ID (PK) |
order_name | String | 워크오더 이름 — 필수, 공백 불가(422 order.name.required). B2MML JobOrder 의 이름 |
description | String | 설명 |
site_id | String | 소속 사이트 |
asset_id | String | 대상 자산 (Equipment) |
customer_id | String | 고객 (선택) |
product_id | String | 생산할 제품 |
employee_id | String | 담당 직원 (선택) |
planned_quantity | Quantity | 계획 수량 — ISA-95 / B2MML QuantityValueType. 아래 11.5 수량 |
time_per_unit_in_ms | long | 단위당 표준 시간 |
planned_start_timestamp | long | 계획 시작 시각 (밀리초) |
planned_end_timestamp | long | 계획 종료 시각 (밀리초) |
external_id | String | 외부 시스템(MES·ERP)의 워크오더 번호(100자까지). external_system 과 쌍으로 식별된다 — 11.6 |
external_system | String | 그 워크오더를 발행한 외부 시스템 이름(50자까지) — 외부 식별 쌍의 다른 반쪽 |
customer_po_id | String | 이 워크오더가 대응하는 고객 구매발주(PO) 번호 (선택) |
status | String | (응답 전용) 현재 상태 — 저장 값 WAIT·START·PAUSED·END·ABORTED |
isa95_state | String | (응답 전용) ISA-95 작업 지시 상태 — Ready·Running·Held·Completed·Aborted (11.1) |
title → order_name, fix_start_timestamp·fix_end_timestamp → planned_start_timestamp·planned_end_timestamp,
external_order_id → external_id, external_po_id → customer_po_id. 옛 이름은 요청에서 읽지 않고 응답에도 나오지 않습니다 —
업그레이드 노트.
11.4 사용 예시
작업지시 생성
import plantpulse.api.v5.dto.Quantity;
import plantpulse.api.v5.dto.request.OrderRequestV5;
import plantpulse.api.v5.dto.response.OrderResponseV5;
OrderRequestV5 req = new OrderRequestV5();
req.setOrder_id("ORDER_000001");
req.setOrder_name("5월 14일 1교대 생산");
req.setDescription("PROD_A001 500개 생산");
req.setSite_id("SITE_DJ");
req.setAsset_id("ASSET_DJ_M_0001");
req.setProduct_id("PROD_A001");
req.setCustomer_id("CUST_001");
req.setEmployee_id("EMP_E12345");
req.setPlanned_quantity(Quantity.of("500", "EA")); // 값은 10진 «문자열», 단위는 UN/ECE Rec 20 코드
req.setTime_per_unit_in_ms(5_000);
long now = System.currentTimeMillis();
req.setPlanned_start_timestamp(now);
req.setPlanned_end_timestamp(now + 4 * 3600_000L);
req.setExternal_id("MES-2026-0514-001");
req.setExternal_system("MES");
OrderResponseV5 created = client.order().create(req);
표준 라이프사이클
String orderId = "ORDER_000001";
// WAIT → START
if (!client.order().start(orderId)) {
System.err.println("시작 실패 — planned_start_timestamp가 미래이거나 이미 진행중");
return;
}
// 잠시 후 — 일시중지
client.order().pause(orderId);
// 재개
client.order().resume(orderId);
// 정상 종료
client.order().end(orderId);
비정상 종료
// abort 는 별도 인자 없이 호출. 상세 사유는 update() 로 description/notes 에 기록.
if (!client.order().abort("ORDER_000001")) {
System.err.println("중단 실패");
}
단건 조회 / 존재 확인
OrderResponseV5 o = client.order().get("ORDER_000001");
if (o != null) {
System.out.println("상태: " + o.getStatus());
Quantity planned = o.getPlanned_quantity(); // {quantity_string, data_type, unit_of_measure}
System.out.println("계획: " + planned.getQuantity_string() + " " + planned.getUnit_of_measure());
java.math.BigDecimal value = planned.toBigDecimal(); // 계산은 BigDecimal 로 — double 로 바꾸지 않는다
}
boolean exists = client.order().exists("ORDER_000001");
전체 목록
import java.util.List;
List<OrderResponseV5> all = client.order().list();
for (OrderResponseV5 o : all) {
System.out.printf("[%s] %s — %s%n",
o.getStatus(), o.getOrder_id(), o.getOrder_name());
}
진행중 작업지시만 (클라이언트 측 필터)
import java.util.stream.Collectors;
List<OrderResponseV5> active = client.order().list().stream()
.filter(o -> "START".equals(o.getStatus())
|| "PAUSE".equals(o.getStatus()))
.collect(Collectors.toList());
자산별 작업지시
String assetId = "ASSET_DJ_M_0001";
List<OrderResponseV5> assetOrders = client.order().list().stream()
.filter(o -> assetId.equals(o.getAsset_id()))
.collect(Collectors.toList());
상태별 통계
import java.util.Map;
Map<String, Long> byStatus = client.order().list().stream()
.collect(Collectors.groupingBy(
OrderResponseV5::getStatus,
Collectors.counting()));
byStatus.forEach((status, count) ->
System.out.printf("%s: %d건%n", status, count));
수정 / 삭제
// 수정 (계획 시간 연장)
OrderRequestV5 update = new OrderRequestV5();
update.setOrder_id("ORDER_000001");
update.setOrder_name("5월 14일 1교대 생산 (연장)");
// ... 모든 필드 다시 세팅 ...
update.setPlanned_end_timestamp(System.currentTimeMillis() + 8 * 3600_000L);
client.order().update("ORDER_000001", update);
// 삭제 (취소 처리)
client.order().delete("ORDER_000001");
11.5 수량 — ISA-95 Quantity
워크오더의 수량(계획·생산·양품·폐기·재작업 …)은 숫자 하나가 아니라 값과 단위를 함께 든 객체입니다.
모양은 ISA-95(IEC 62264) / B2MML V0700 의 QuantityValueType 과 1:1 입니다.
V5 API(OpenAPI info.version 5.0) · 수량 계약 2026-10-30 · 이름 웨이브 2026-10-31 · 플랫폼 2026.11 판(server-web 974891b4) 기준입니다.
이벤트는 plantpulse.order.*·plantpulse.calendar.* 스키마 major 2, plantpulse.kpi.oee.finalized major 1, DB 는 Flyway V99·V100·V103 과 core 마이그레이션 V12V16·V21V23 —
업그레이드 노트.
[
{ "quantity_string": "1250.5", "data_type": "decimal", "unit_of_measure": "KGM" },
{ "quantity_string": "1200", "data_type": "integer", "unit_of_measure": "EA" }
]
| 칸 | B2MML | 타입 | 규칙 |
|---|---|---|---|
quantity_string | QuantityString | string | 10진 표기 그대로 — 지수 표기 금지, 소수점은 ., 소수 넷째 자리까지. 계수 단위는 정수 |
data_type | DataType | string | integer(계수 단위) 또는 decimal(그 밖). 서버가 단위에서 정한다 — 요청에서는 생략할 수 있고, 주면 단위와 맞아야 한다 |
unit_of_measure | UnitOfMeasure | string | UN/ECE Recommendation 20 공통 코드 — 아래 허용 목록 |
B2MML 이 QuantityString 으로 정의하고, JSON 숫자는 대부분의 파서가 double 로 읽어 10진 정확성을
잃기 때문입니다. SDK 에서는 Quantity.toBigDecimal() 로 읽어 계산하세요. 요청을 만들 때는
Quantity.of("1250.5", "KGM") — data_type 은 비워 두면 서버가 채웁니다.
단위 — 허용 목록 (UN/ECE Rec 20)
| 코드 | 뜻 | data_type | 화면 기호 |
|---|---|---|---|
EA | each — 낱개로 세는 수 | integer | EA |
H87 | piece | integer | pc |
C62 | one (unit) | integer | unit |
BX | box — 상자 수 | integer | box |
SET | set — 세트·키트 수 | integer | set |
KGM | kilogram | decimal | kg |
GRM | gram | decimal | g |
TNE | tonne | decimal | t |
LTR | litre | decimal | L |
MLT | millilitre | decimal | mL |
MTQ | cubic metre | decimal | m³ |
MTR | metre | decimal | m |
MTK | square metre | decimal | m² |
- 허용 단위는 위 13개이고, 계수 단위는
EA·H87·C62·BX·SET다섯입니다. 목록 밖의 코드는 거절됩니다(quantity.unit_invalid). 코드는 대소문자까지 그대로 비교합니다 —ea,KG,PCS는 허용 코드가 아닙니다. - 화면 기호에 없는 코드가 들어오면(예: 옛 데이터) 코드 그대로 보입니다.
- 단위 환산은 하지 않습니다(단위 마스터·환산은 MES 의 몫). 서로 다른 단위의 수량은 더하지 않습니다.
- 화면 기호는 언어와 무관합니다 — 한국어 화면에서도 «개» 가 아니라
EA입니다. 기호는 항상 붙습니다(개수도).
칸 이름 — ISO 22400
칸 이름은 ISA-95 와 ISO 22400-2(IEC 62264 의 KPI 짝)의 용어를 따릅니다. 옛 이름은 응답에 더 이상 나오지 않습니다(호환 칸 없음).
| 의미 | 칸 (Quantity) | 옛 칸 | 근거 |
|---|---|---|---|
| 계획 수량 | planned_quantity | target_units + unit | ISA-95 MaterialRequirement.Quantity · ISO 22400 POQ |
| 생산 수량 | produced_quantity | total_production_count | ISO 22400 PQ |
| 양품 수량 | good_quantity | total_good_count · good_count | ISO 22400 GQ |
| 폐기 수량 | scrap_quantity | total_bad_count · bad_count — 뜻이 좁아졌다: 옛 칸은 불량 전체, 새 칸은 폐기만 | ISO 22400 SQ |
| 재작업 수량 | rework_quantity | (없음 — 새 칸) | ISO 22400 RQ |
| 계획 밖 양품 / 불량 | good_quantity_out_of_plan / scrap_quantity_out_of_plan | out_of_plan_good_count / out_of_plan_bad_count | |
| 측정 양품 / 불량 (PLC) | measured_good_quantity / measured_scrap_quantity | measured_good_count / measured_bad_count | |
| 보정 증감 | good_quantity_delta / scrap_quantity_delta | good_delta / bad_delta | |
| 보정 입력값 | entered_good_quantity / entered_scrap_quantity | entered_good / entered_bad | |
| 검사 수량 | inspected_quantity | inspected_count | |
| 검사 불량 합 | inspected_scrap_quantity | inspected_bad_count | |
| 불량 기록 처분별 | scrap_quantity / rework_quantity | scrap_count / rework_count | ISO 22400 SQ / RQ |
바뀌는 응답: /order, /order/query(include=oee), /order/{id}/oee, /order/{id}/quantity(개정 이력 포함),
/defects, /order/performance, /kpi/oee/history, /asset/{id}/status, MCP 워크오더 도구, 생산 사건 OEE_CALCULATED.
/asset/{id}/status의planned_quantity·good_quantity·scrap_quantity는 진행 중 워크오더의 단위로 나옵니다. 진행 중 워크오더가 없으면 null 입니다.- 비율 칸(
total_good_rate,production_rate, 가용성·성능·품질·OEE)은 무차원이라 숫자 그대로입니다. /asset/{id}/status에도rework_quantity가 있습니다(진행 중 워크오더의 재작업).- PLC 카운터 기록(
GET /api/v5/kpi/oee/count의good_quantity·scrap_quantity)과 MCPget_asset_kpi의 원인별 폐기(scrap_quantity)도 그 시점 워크오더 단위의 Quantity 입니다. 카운터는 정수로 세므로 카운트 1 = 워크오더 단위 1 입니다 — kg 워크오더에 정수 카운터를 물리면 1 kg 단위로만 셉니다. 소수 실적은 카운터가 아니라 수량 정정·실적 입력으로 넣습니다.
폐기와 재작업 — «불량» 은 둘의 합
ISO 22400 대로 불량을 폐기(SQ) 와 재작업(RQ) 으로 나눕니다.
- 불량 = 폐기 + 재작업, 생산 = 양품 + 폐기 + 재작업(
produced_quantity=good_quantity+scrap_quantity+rework_quantity). scrap_quantity는 폐기만입니다. PLC 가 센 불량은 처분을 모르므로 폐기로 셉니다. 재작업은 처분이REWORK인 불량 기록에서만 생깁니다.- 품질 Q 는 양품 ÷ 생산이므로 재작업도 품질을 깎습니다 — 나누어 적을 뿐 OEE 값은 바뀌지 않습니다.
- 화면은 이 구분을 말로 지킵니다: 폐기만 가리키는 자리(생산 히스토리 열, 수량 정정 칸)는 «폐기», 합계를 보이는 자리는 «불량» 이고 재작업이 있으면 «14 kg (폐기 12.75 · 재작업 1.25)» 처럼 내역을 붙입니다 — 생산 관리 — 수량과 단위.
- 연동이 «불량 합계» 가 필요하면
scrap_quantity+rework_quantity를 같은 단위끼리 10진수로 더합니다.scrap_quantity하나를 불량 합계로 읽지 마세요.
어느 칸이 어느 수량을 싣나 — 카운터 표
| 출처 (저장) | V5 칸 / 이벤트 칸 | 싣는 수량 |
|---|---|---|
PLC 카운터 원장 tm_kpi_oee_count | /kpi/oee/count 의 good_quantity · scrap_quantity | PLC 가 센 양품·불량 증분. 처분을 모르므로 불량은 폐기(SQ) |
워크오더 OEE 행 mm_order_oee.total_production_count | produced_quantity · 이벤트 pq/pq_quantity | 생산(PQ) = 양품 + 폐기 + 재작업 |
mm_order_oee.total_good_count | good_quantity · gq/gq_quantity | 양품(GQ) |
mm_order_oee.total_bad_count − total_rework_count | scrap_quantity · sq/sq_quantity | 폐기(SQ). total_bad_count 자체(불량 합)는 와이어에 나가지 않는다 |
mm_order_oee.total_rework_count | rework_quantity · rq/rq_quantity | 재작업(RQ) |
mm_order_oee.out_of_plan_good_count / out_of_plan_bad_count | good_quantity_out_of_plan / scrap_quantity_out_of_plan · gq_out_of_plan/sq_out_of_plan(+_quantity) | 계획정지 중 만든 양 — 위 양품·폐기에 포함, 효율 계산에서는 뺀다 |
수량 정정 mm_order_quantity_correction | /order/{id}/quantity 의 measured_*_quantity · good_quantity_delta/scrap_quantity_delta · (이력) entered_*_quantity | PLC 측정값, 정정 증감, REPLACE 입력값. 폐기 칸은 폐기만 |
불량 기록 mm_defect_record (처분 SCRAP/REWORK) | /defects 의 quantity·inspected_quantity, 실적 문서 defects.scrap_quantity / rework_quantity / inspected_scrap_quantity | 처분별 폐기·재작업, 검사 불량 합(SCRAP + REWORK) |
mm_order.planned_quantity | planned_quantity · 이벤트 planned_quantity | 계획(POQ) |
요청 규칙과 거절
실적·보정·불량 기록에 싣는 수량은 워크오더 planned_quantity 와 같은 단위여야 합니다. 서버는 환산하지 않고 거절합니다.
값·단위 거절은 422 E1002(400 이 아닙니다)이고, 응답(application/problem+json)의 code 가 E1002,
reason 칸이 아래 사유 코드입니다. detail 은 호출자 언어의 문장이므로 분기는 reason 으로 하세요.
| 사유 코드 | 언제 |
|---|---|
quantity.unit_mismatch | 워크오더의 단위와 다른 단위를 보냈다 |
quantity.not_integer | 계수 단위(EA·H87·C62·BX·SET)에 소수를 보냈다 |
quantity.unit_invalid | 단위가 없거나 허용 목록 13개 밖이다 |
quantity.format | quantity_string 이 10진 표기 문자열이 아니다(지수 표기·앞자리 0·+ 부호·숫자 타입), 또는 data_type 이 단위와 맞지 않는다 |
quantity.scale | 소수 다섯째 자리 이상 |
quantity.negative | 음수(보정 증감 칸만 음수를 받는다) |
quantity.range | 절댓값이 1014 이상 — 저장 numeric(18,4) 의 범위 밖 |
- 수량 칸에 객체가 아닌 값(숫자
500등)을 주거나quantity_string·data_type·unit_of_measure밖의 키를 넣으면 400E1000입니다(모양 오류). - 워크오더의 계획 수량은 0 보다 커야 합니다.
기간 KPI — 단위가 섞이면 합치지 않는다
/kpi/period·/kpi/daily 처럼 여러 워크오더를 합치는 응답은 단위가 하나일 때만 합계를 냅니다.
수량 합계 칸은 /kpi/period 의 oee 객체 안 good_count·total_count·in_plan_count, /kpi/daily 의 good_count·bad_count·planned_count 입니다.
/kpi/daily 의 bad_count 는 생산 − 양품, 곧 불량 합(폐기 + 재작업) 입니다.
- 단위가 하나 — 수량 합계 칸은 지금처럼 숫자, 여기에 그 단위
unit_of_measure가 붙습니다. - 단위가 섞임 — 수량 합계 칸과
unit_of_measure는 null, 대신quantities_by_unit에 단위별 양품·생산 수량이 Quantity 로 나옵니다.
{
"good_count": null,
"total_count": null,
"in_plan_count": null,
"unit_of_measure": null,
"quantities_by_unit": [
{ "unit_of_measure": "EA",
"good_quantity": { "quantity_string": "2671", "data_type": "integer", "unit_of_measure": "EA" },
"produced_quantity": { "quantity_string": "2700", "data_type": "integer", "unit_of_measure": "EA" } },
{ "unit_of_measure": "KGM",
"good_quantity": { "quantity_string": "1250.5", "data_type": "decimal", "unit_of_measure": "KGM" },
"produced_quantity": { "quantity_string": "1302", "data_type": "decimal", "unit_of_measure": "KGM" } }
]
}
비율(가용성·성능·품질·OEE)은 무차원이라 단위가 섞여도 그대로 나옵니다.
EMS 단위당 값 — 분모의 단위
energy_per_unit_kwh·co2_per_unit_kg 의 분모는 워크오더의 양품 수량이므로, 응답에 그 단위 unit_of_measure 가 함께 나옵니다
(/order/{id}/ems, /order/ems/*, /order/query?include=ems). 화면은 «kWh/EA», «kWh/kg» 처럼 적습니다.
여러 워크오더를 합치는 집계에서 단위가 섞이면 단위당 값과 단위는 null 입니다.
이벤트
이벤트 스키마는 «끝에 덧붙이기» 만 허용하므로 타입을 바꾸지 않고 칸을 더했습니다.
| 이벤트 | 수량 칸 |
|---|---|
plantpulse.order.* | planned_quantity(number) + planned_quantity_unit_of_measure(Rec 20 코드) |
plantpulse.kpi.oee.finalized | 정본은 gq_quantity·sq_quantity·rq_quantity·pq_quantity·gq_out_of_plan_quantity·sq_out_of_plan_quantity(number) + unit_of_measure. sq 는 폐기만, 재작업은 rq(pq = gq + sq + rq). 정수 칸 gq·sq·rq·pq·… 은 계수 단위일 때만 실리고 소수 단위면 없다 — 다음 major 에서 빠진다 |
생산 사건 OEE_CALCULATED | good_quantity·scrap_quantity(폐기만)·rework_quantity = Quantity 객체 |
11.6 MES 연동 — 실적 피드와 B2MML 대응
외부 워크오더 식별 — (external_system, external_id) 쌍
고객 시스템(MES·ERP)의 워크오더는 발행 시스템 이름과 그 시스템 안의 번호, 두 칸의 쌍으로 식별합니다 — 직원·정비 오더의 외부 식별과 같은 꼴입니다.
- 쌍은 들어오는 길과 나가는 길에 모두 함께 있습니다. V5
POST·PUT·PATCH /api/v5/order가external_system(50자)·external_id(100자)를 받고,GET /order/{id}·목록·/order/query·실적 문서는 둘 다 늘 싣습니다(없으면 null). 콘솔 양식에는external_system칸이 없으므로 V5 로 넣습니다. - 비었거나 공백뿐인 값은 null 로 저장합니다. 길이를 넘거나 제어 문자가 있으면 422
order.external_ref.invalid입니다. - 두 칸이 모두 비어 있지 않을 때 쌍은 유일합니다. 같은 쌍을 두 번째 워크오더에 주면 409
order.external.duplicate입니다. PUT은 요청 칸 전체를 바꿉니다 — 빠진external_system·external_id는 지워집니다. 일부만 바꾸려면PATCH를 쓰세요.external_id만 있고external_system이 없는 워크오더도 아직 받지만, 쌍이 아니므로 아래 «쌍으로 찾기» 로는 찾을 수 없습니다.- 이벤트도 쌍을 싣습니다:
plantpulse.order.*(스키마 major 2)의created·updated·startedaborted가external_id·external_system을, 전이 이벤트(startedaborted)는customer_po_id도 싣습니다. - 워크오더 하나를 쌍으로 읽으려면
GET /api/v5/order/by-external/{system}/{external_id}— 없으면 404, 한쪽이 비면 422order.external_ref.invalid./order/query의 필터도external_id+external_system입니다. 옛 파라미터external_order_id를 보내면 무시하지 않고 422 로 거절합니다.
실적 피드 경로
| HTTP | 경로 | 하는 일 |
|---|---|---|
| GET | /api/v5/order/performance | 실적 피드 — cursor·site_id·limit. 실적이 바뀐 워크오더는 performance_revision 이 하나 올라 끝에 다시 나온다 |
| GET | /api/v5/order/{order_id}/performance | 워크오더 하나의 실적 문서 |
| GET | /api/v5/order/performance/by-external/{external_system}/{external_id} | 쌍으로 찾기 — 배열(쌍이 유일하므로 많아야 하나), 없으면 빈 배열 |
/api/v5/order/performance/by-external/{external_order_id}(시스템 없이 번호만)는 지웠습니다. 호환 경로는 없습니다.
실적 문서의 수량 키
| 위치 | 키 (모두 Quantity) | 옛 키 |
|---|---|---|
plan | planned_quantity | target_units |
quantity | measured_good_quantity / measured_scrap_quantity | measured_good / measured_bad |
quantity | good_quantity / scrap_quantity | good / bad |
quantity.correction | good_quantity_delta / scrap_quantity_delta | good_delta / bad_delta |
oee | good_quantity / scrap_quantity | total_good_count / total_bad_count |
defects | inspected_scrap_quantity / scrap_quantity / rework_quantity | inspected_bad_count / scrap_count / rework_count |
quantity.correction_version, progress.expected_now_units·pace_units 는 이름이 그대로입니다(소수 단위면 소수 숫자).
quantity.scrap_quantity 는 폐기만(PLC 불량 + SCRAP 기록)이고 defects.scrap_quantity 와 뜻이 같습니다. quantity.rework_quantity 는
defects.rework_quantity 와 같은 값입니다. defects.inspected_scrap_quantity 는 검사 불량 전체(SCRAP + REWORK)입니다.
실적 문서의 상태 — 무엇이 바뀌었는지 읽는 법
실적 문서 하나는 워크오더 하나의 지금 상태를 담고, 상태가 바뀔 때마다 판이 올라갑니다.
| 칸 | 뜻 |
|---|---|
actual.status | 저장 상태 WAIT·START·PAUSED·END·ABORTED — 11.1. 취소는 ABORTED + actual.abort_code=CANCELLED |
actual.isa95_state | ISA-95 작업 지시 상태 Ready·Running·Held·Completed·Aborted |
performance_revision | 실적이 바뀐 횟수. 바뀔 때마다 1 씩 오르고 피드는 그 워크오더를 끝에 다시 낸다. 0 은 아직 끝나지 않은 워크오더 — 피드에 없고 /order/{id}/performance 로만 읽힌다 |
quantity.correction_version | 수량 정정 판. 정정이 없으면 측정값 그대로이고, 정정마다 오른다 — 이력은 /order/{id}/quantity 의 개정 이력 |
changed_at · change_cause | 마지막으로 바뀐 시각과 그 원인 — ENDED·QUANTITY(수량 정정)·STOP_REASON·OEE_FINAL·DEFECT(불량 기록)·PROGRESS·BASELINE |
수량은 «값 + 단위» 로만 싣고 계산되지 않은 칸은 0 이 아니라 null 입니다. MES 는 같은 order_id 의 performance_revision 이
더 큰 문서로 이전 문서를 통째로 바꾸면 됩니다.
B2MML 대응표
실적 문서 하나는 ISA-95 Part 4 의 Job Response(Segment Response)에, 그 수량은 Material Actual(Produced)의 Quantity 에 해당합니다.
| ISA-95 / B2MML | PlantPulse V5 | 비고 |
|---|---|---|
QuantityValueType — QuantityString · DataType · UnitOfMeasure | Quantity — quantity_string · data_type · unit_of_measure | 1:1. B2MML 의 선택 칸 Key 는 쓰지 않는다 |
UnitOfMeasure 값 | UN/ECE Rec 20 코드 | 허용 목록은 11.5 |
| Job Order 의 ID | external_id | 발행 시스템 이름은 external_system — 둘이 한 쌍 |
| Job Order · MaterialRequirement 의 Quantity | planned_quantity | ISO 22400 POQ |
| Job Order 의 이름 | order_name | |
| Job Order 의 StartTime · EndTime | planned_start_timestamp · planned_end_timestamp | 계획 시각. 실제 시각은 actual_start_timestamp · actual_end_timestamp |
| Job Response | 실적 문서 하나 (/order/{id}/performance) | |
| MaterialActual(Produced) 의 Quantity | produced_quantity | ISO 22400 PQ |
| 그 가운데 양품 | good_quantity | ISO 22400 GQ |
| 그 가운데 스크랩 | scrap_quantity | ISO 22400 SQ |
| 그 가운데 재작업 | defects.rework_quantity | ISO 22400 RQ |
11.7 활용 시나리오
시나리오 — MES 연동 작업지시 자동 생성
import java.text.SimpleDateFormat;
import java.util.Date;
SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");
String today = df.format(new Date());
// 외부 MES 큐에서 받은 주문을 V5 Order로 변환
for (MesOrder mes : mesQueue) {
OrderRequestV5 req = new OrderRequestV5();
req.setOrder_id("ORDER_" + today + "_" + String.format("%03d", mes.seq));
req.setSite_id("SITE_DJ");
req.setAsset_id(mes.equipId);
req.setProduct_id(mes.productId);
// MES 수량을 그대로 — 단위 환산은 하지 않는다. 단위는 허용 목록(11.5) 안의 Rec 20 코드여야 한다
req.setPlanned_quantity(Quantity.of(mes.qty.toPlainString(), mes.unitCode));
req.setExternal_id(mes.mesOrderId);
req.setExternal_system("MES");
req.setPlanned_start_timestamp(mes.plannedStart);
req.setPlanned_end_timestamp(mes.plannedEnd);
client.order().create(req);
}
시나리오 — 안전한 상태 전이 헬퍼
public static boolean safeStart(APIClient_V5 client, String orderId) {
OrderResponseV5 o = client.order().get(orderId);
if (o == null) {
log.warn("작업지시 없음: " + orderId);
return false;
}
if (!"WAIT".equals(o.getStatus())) {
log.warn("WAIT 상태가 아님: " + o.getStatus());
return false;
}
return client.order().start(orderId);
}
다음 단계
- Customer 서비스, Employee 서비스, Product 서비스 — Order에 부착되는 마스터 데이터
- Calendar 서비스 — 작업지시와 점검 일정 충돌 검사
- 업그레이드 노트 — 수량 Quantity 전환 때 연동이 고칠 것