/* * **원본에만 있는 관계를 조회해서 트윈 상세 화면에 붙인다.** * * ── 무엇을 해결하나 ──────────────────────────────────────────────────────── * 설비 상세 화면에서 사람이 실제로 묻는 것은 「이 설비로 무엇을 얼마나 만들었나」, 「지금 정비 * 중인가」, 「예정된 정비가 있나」다. 그 정보는 트윈에 없고 MES 에 있다. * * 트윈으로 복제하면 원본의 테이블을 전부 미러링하게 됩니다. 조회용 참조는 복제하지 않고 필요할 때 * 물어보는 것이 맞습니다. * * ── 왜 프로세스가 나뉘어 있는가 ──────────────────────────────────────────── * 측정 결과입니다(2026-09-06). * * operato-plant 포트 4000 자기 DB * operato-twin 포트 3000 자기 DB * * 별개 프로세스이므로 plant 안에서 `registerExternalResolver` 를 불러도 twin 프로세스에는 등록되지 * 않습니다. 그래서 resolver 는 twin 안에 있고, 본체는 어댑터를 통해 원본에 HTTP 로 물어봅니다. * * 주소와 인증은 이미 연결 설정(`TwinReference.connectionConfig`)에 있습니다. 새로 만들 것이 없습니다. * * ── 왜 부팅에 한 번만 등록하나 ────────────────────────────────────────────── * 연결은 나중에 추가될 수 있습니다. 연결마다 resolver 를 등록하면 새 연결이 생길 때 재기동해야 * 합니다. resolver 하나가 호출될 때 연결을 조회하므로 재기동이 필요 없습니다. */ import { getRepository } from '@things-factory/shell' import { TwinInstance } from '../twin-instance/twin-instance.js' import { TwinReference } from './twin-reference.js' import { getAdapter, type SiteDescriptor } from './reference-adapter.js' import { registerExternalResolver, type ExternalIncoming } from '../twin-model/external-resolver.js' import { usableLinkGroups } from './reference-link-groups.js' /** * 이 트윈의 원본에 관계를 물어본다. * * 물어볼 수 없는 경우가 여럿이고 **전부 빈 배열입니다** — 원본을 모르는 트윈, 등록되지 않은 어댑터, * `fetchLinks` 를 구현하지 않은 어댑터. 이 셋은 결함이 아닙니다. 가상 원본과 시뮬레이터는 조회할 * 관계가 없습니다. * * 실패는 다릅니다 — 예외를 그대로 올립니다. 호출하는 쪽(`externalIncoming`)이 잡아서 경고를 남기고 * 나머지 관계는 그대로 표시합니다. */ export async function fetchReferenceLinks( domainId: string, instanceId: string, axis: string, itemId: string ): Promise { const ref = await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } }) if (!ref) return [] const adapter = getAdapter(ref.adapterType) if (!adapter?.fetchLinks) return [] /* * 사이트를 함께 넘깁니다 — 원본 하나가 여러 사이트를 낼 수 있습니다(`discoverSites`). 이 트윈이 * 어느 사이트에서 왔는지는 `origin.siteId` 에 있고, 없으면 넘기지 않습니다. */ const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } }) const siteId = (inst?.origin as any)?.siteId const site: SiteDescriptor | undefined = siteId ? { siteId } : undefined return usableLinkGroups(await adapter.fetchLinks(ref.connectionConfig ?? {}, site, axis, itemId)) } /** 부팅에서 한 번 등록한다. */ export function installReferenceLinkResolver(): void { registerExternalResolver({ /* * `entity` 는 계약의 `target.entity` 와 맞추는 값입니다. 이 resolver 는 들어오는 관계만 답하고 * 특정 관계에 붙지 않으므로, 계약에 없는 이름을 씁니다. */ entity: 'reference.links', incoming: (domainId, instanceId, axis, itemId) => fetchReferenceLinks(domainId, instanceId, axis, itemId) }) }