본문으로 건너뛰기

18. 통합 예제 모음

V5 클라이언트로 자주 수행하는 엔드투엔드 시나리오를 모아둡니다.

18.1 신규 공장 부팅 — 전체 마스터 데이터 등록

새 사이트를 등록하고, 자산 트리(Area → Line → Equipment) → OPC → Tag 까지 한 번에 셋업하는 시나리오입니다.

import plantpulse.api.v5.APIClient_V5;
import plantpulse.api.v5.dto.request.*;
import plantpulse.api.v5.dto.response.*;

public class BootstrapNewSite {
public static void main(String[] args) throws Exception {

try (APIClient_V5 client = new APIClient_V5(
"HTTPS", "100.68.69.41", 7443, "api", TOKEN, false)) {

client.connect();

// 1) 사이트 등록
SiteRequestV5 site = new SiteRequestV5();
site.setSite_id("SITE_DJ");
site.setSite_name("대전 1공장");
site.setLat("36.3504");
site.setLng("127.3845");
client.site().create(site);

// 2) Area
AssetRequestV5 area = new AssetRequestV5();
area.setSite_id("SITE_DJ");
area.setParent_asset_id("SITE_DJ");
area.setAsset_id("ASSET_DJ_A_0001");
area.setAsset_name("조립구역");
area.setAsset_type("A");
client.asset().create(area);

// 3) Line
AssetRequestV5 line = new AssetRequestV5();
line.setSite_id("SITE_DJ");
line.setParent_asset_id("ASSET_DJ_A_0001");
line.setAsset_id("ASSET_DJ_L_0001");
line.setAsset_name("1번 조립 라인");
line.setAsset_type("L");
client.asset().create(line);

// 4) Equipment (3대)
for (int i = 1; i <= 3; i++) {
AssetRequestV5 eq = new AssetRequestV5();
eq.setSite_id("SITE_DJ");
eq.setParent_asset_id("ASSET_DJ_L_0001");
eq.setAsset_id(String.format("ASSET_DJ_M_%04d", i));
eq.setAsset_name("CNC #" + i);
eq.setAsset_type("M");
eq.setTable_type("CNC");
client.asset().create(eq);
}

// 5) OPC (PLC)
OPCRequestV5 plc = new OPCRequestV5();
plc.setOpc_id("OPC_PLC_L1_001");
plc.setOpc_name("Line1 메인 PLC");
plc.setOpc_type("PLC");
plc.setOpc_sub_type("SIEMENS_S7");
plc.setSite_id("SITE_DJ");
plc.setOpc_server_ip("192.168.10.50");
plc.setOpc_agent_ip("192.168.10.5");
plc.setOpc_agent_port(60000);
client.opc().create(plc);

// 6) Tag (각 Equipment에 RPM/온도/카운트 3개씩)
for (int i = 1; i <= 3; i++) {
String equipId = String.format("ASSET_DJ_M_%04d", i);
for (String metric : new String[]{"RPM", "TEMP", "COUNT"}) {
TagRequestV5 tag = new TagRequestV5();
tag.setSite_id("SITE_DJ");
tag.setOpc_id("OPC_PLC_L1_001");
tag.setLinked_asset_id(equipId);
tag.setTag_id(String.format("TAG_PLC_L1_M%04d_%s", i, metric));
tag.setTag_name("CNC#" + i + " " + metric);
tag.setTag_source("OPC");
tag.setJava_type("Double");
tag.setUnit(metricUnit(metric));
client.tag().create(tag);
}
}

System.out.println("부팅 완료");
}
}

private static String metricUnit(String m) {
switch (m) {
case "RPM": return "RPM";
case "TEMP": return "C";
case "COUNT": return "EA";
default: return "";
}
}
}

18.2 ERP 마스터 동기화 (멱등)

ERP에서 받은 고객/제품/직원 데이터를 V5 마스터로 동기화. exists() 확인 후 create() 또는 update() 분기.

public static void syncCustomers(APIClient_V5 client, List<ErpCustomer> erpData) {
for (ErpCustomer erp : erpData) {
String customerId = "CUST_" + erp.code;

CustomerRequestV5 req = new CustomerRequestV5();
req.setCustomer_id(customerId);
req.setCustomer_name(erp.name);
req.setSite_id(erp.siteId);
req.setExternal_customer_id(erp.id);
req.setCompany_code(erp.companyCode);
req.setManager_name(erp.contact);
req.setManager_email(erp.email);

if (client.customer().exists(customerId)) {
client.customer().update(customerId, req);
} else {
client.customer().create(req);
}
}
}

18.3 MES 작업지시 생성·시작·종료

public static void runMesWorkOrder(APIClient_V5 client, MesOrder mes) {
// 1) Order 생성
String orderId = "ORD_" + mes.date + "_" + String.format("%03d", mes.seq);
OrderRequestV5 req = new OrderRequestV5();
req.setOrder_id(orderId);
req.setTitle(mes.title);
req.setSite_id("SITE_DJ");
req.setAsset_id(mes.equipId);
req.setProduct_id(mes.productId);
req.setEmployee_id(mes.operatorId);
req.setCustomer_id(mes.customerId);
req.setTarget_units(mes.qty);
req.setUnit("EA");
req.setTime_per_unit_in_ms(mes.cycleTime);
req.setFix_start_timestamp(mes.plannedStart);
req.setFix_end_timestamp(mes.plannedEnd);
req.setExternal_order_id(mes.mesOrderId);

OrderResponseV5 created = client.order().create(req);
if (created == null) {
log.error("작업지시 생성 실패: " + orderId);
return;
}

// 2) 작업 시작 (생성 직후가 아닌, 라인 IDLE 신호 받은 시점에 호출)
if (!client.order().start(orderId)) {
log.warn("시작 실패 — 현재 상태 확인 필요");
return;
}

// 3) 작업 진행 모니터링 ...
// ...

// 4) 작업 종료
client.order().end(orderId);
}

18.4 라인 전체 헬스 대시보드

라인 산하 Equipment의 Tag 수, 최근 알람, 현재 작업지시를 한 번에 모으는 대시보드 데이터 수집기.

import java.util.List;
import java.util.stream.Collectors;

public static DashboardSnapshot collectLineHealth(APIClient_V5 client, String lineId) {
DashboardSnapshot snap = new DashboardSnapshot();

// 1) Line 산하 Equipment
List<AssetResponseV5> equipments = client.asset().list().stream()
.filter(a -> lineId.equals(a.getParent_asset_id()))
.filter(a -> "M".equals(a.getAsset_type()))
.collect(Collectors.toList());

snap.equipmentCount = equipments.size();

// 2) 각 Equipment 통계
for (AssetResponseV5 eq : equipments) {
long tagCount = client.tag().countByAsset(eq.getAsset_id());
snap.totalTags += tagCount;

PathResponseV5 path = client.path().getByAsset(eq.getAsset_id());
snap.paths.put(eq.getAsset_id(),
String.format("%s > %s > %s > %s",
path.getSite_name(), path.getArea_name(),
path.getLine_name(), path.getEquipment_name()));
}

// 3) 최근 알람 — 라인 산하 자산만
List<String> assetIds = equipments.stream()
.map(AssetResponseV5::getAsset_id)
.collect(Collectors.toList());

snap.recentAlarms = client.alarm().list(200).stream()
.filter(a -> a.getTag_location() != null
&& assetIds.stream()
.anyMatch(id -> a.getTag_location().contains(id)))
.collect(Collectors.toList());

// 4) 현재 진행중 작업지시
snap.activeOrders = client.order().list().stream()
.filter(o -> "START".equals(o.getStatus())
|| "PAUSE".equals(o.getStatus()))
.filter(o -> assetIds.contains(o.getAsset_id()))
.collect(Collectors.toList());

return snap;
}

18.5 Tag 알람 임계치 일괄 적용

운영 환경 변경에 따라 특정 라인의 모든 온도 태그에 알람 임계치를 일괄 적용.

public static void applyTempAlarmsToLine(APIClient_V5 client, String lineId) {
// 1) 라인 산하 Equipment
List<AssetResponseV5> equipments = client.asset().list().stream()
.filter(a -> lineId.equals(a.getParent_asset_id()))
.collect(Collectors.toList());

// 2) 각 Equipment의 온도 태그
for (AssetResponseV5 eq : equipments) {
List<TagResponseV5> tempTags = client.tag().listByAsset(eq.getAsset_id())
.stream()
.filter(t -> "C".equals(t.getUnit()))
.collect(Collectors.toList());

// 3) patchAlarm으로 임계치 일괄 적용 (서버에서 CEP/EQL 재배포)
for (TagResponseV5 t : tempTags) {
TagAlarmPatchRequestV5 alarm = new TagAlarmPatchRequestV5();
alarm.setUse_alarm("Y");
alarm.setLo_lo("0");
alarm.setLo("10");
alarm.setHi("80");
alarm.setHi_hi("95");
alarm.setTrip_hi("100");
alarm.setDuplicate_check_minutes(5);
alarm.setSend_email("Y");
client.tag().patchAlarm(t.getTag_id(), alarm);
}
}
}

18.6 점검 일정과 작업지시 충돌 검사

import java.util.Set;
import java.util.stream.Collectors;

public static List<OrderResponseV5> conflictsWithMaintenance(
APIClient_V5 client, String fromIso, String toIso) {

// 1) 점검 일정의 자산 ID 집합
Set<String> mtnAssets = client.calendar().search(fromIso, toIso).stream()
.filter(c -> "MTN".equals(c.getTarget_type()))
.map(CalendarResponseV5::getAsset_id)
.collect(Collectors.toSet());

// 2) 같은 기간 진행중·예정 작업지시 중 점검 대상 자산
return client.order().list().stream()
.filter(o -> "WAIT".equals(o.getStatus()) || "START".equals(o.getStatus()))
.filter(o -> mtnAssets.contains(o.getAsset_id()))
.collect(Collectors.toList());
}

18.7 Flow 헬스 알림

에러가 발생한 Flow를 발견하면 알림을 보내는 모니터링 워커.

public static void monitorFlowHealth(APIClient_V5 client) throws InterruptedException {
Map<String, Long> lastErrorCounts = new HashMap<>();

while (true) {
for (FlowResponseV5 flow : client.flow().list()) {
long prev = lastErrorCounts.getOrDefault(flow.getFlow_id(), 0L);
long curr = flow.getError_count();

if (curr > prev) {
long newErrors = curr - prev;
sendAlert(String.format("Flow %s 에서 신규 에러 %d건 발생",
flow.getFlow_name(), newErrors));
}
lastErrorCounts.put(flow.getFlow_id(), curr);
}
Thread.sleep(30_000);
}
}

18.8 Tag CUD 라이프사이클 (테스트 코드 패턴)

스모크 테스트나 통합 테스트에서 자주 쓰는 패턴 — 생성/조회/수정/삭제 후 격리 자원 청소까지.

public static void tagCudCycle(APIClient_V5 client) {
long ts = System.currentTimeMillis();
String opcId = "OPC_TEST_" + ts;
String tagId = "TAG_TEST_" + ts;

// 1) 부모 OPC 생성 (Tag의 FK)
OPCRequestV5 opcReq = new OPCRequestV5();
opcReq.setOpc_id(opcId);
opcReq.setOpc_name("test-opc-" + ts);
opcReq.setOpc_type("VIRTUAL");
opcReq.setSite_id("SITE_00001");
opcReq.setOpc_agent_ip("127.0.0.1");
opcReq.setOpc_agent_port(0);
client.opc().create(opcReq);

try {
// 2) Tag 생성
TagRequestV5 req = new TagRequestV5();
req.setTag_id(tagId);
req.setTag_name("test-tag-" + ts);
req.setOpc_id(opcId);
req.setSite_id("SITE_00001");
req.setJava_type("Double");
req.setUnit("C");
req.setTag_source("OPC");
TagResponseV5 created = client.tag().create(req);
assert created != null;

// 3) 조회 + 존재 확인
TagResponseV5 fetched = client.tag().get(tagId);
assert fetched != null;
assert client.tag().exists(tagId);

// 4) 수정
req.setTag_name("test-tag-" + ts + "-updated");
TagResponseV5 updated = client.tag().update(tagId, req);
assert updated != null;
assert req.getTag_name().equals(updated.getTag_name());

// 5) 삭제
assert client.tag().delete(tagId);
assert !client.tag().exists(tagId);
} finally {
// 6) 임시 OPC 정리
client.opc().delete(opcId);
}
}

다음 단계

  • 도메인별 상세 매뉴얼은 README 의 목차에서 선택하세요.
  • 도메인 ID 규칙 으로 ID 설계 원칙을 다시 확인하시면 좋습니다.