/* * 사이트 발견 결과 — **"못 찾았다" 와 "없다" 를 구분한다.** 순수. * * ── 왜 있나 (2026-08-06) ──────────────────────────────────────────────────── * 실 시스템을 연결해 "거기 있는 것" 으로 트윈을 만드는 경로에서, 발견이 실패하면 화면이 * **"사이트를 찾지 못했습니다"** 라고만 말했다. 네 가지가 그 한 문장에 섞여 있었다. * * ① 그 레퍼런스가 등록돼 있지 않다 (`!ref → []`) * ② 어댑터 종류를 모른다 (`!adapter → []`) * ③ 연결·조회가 실패했다 (`catch { return [] }`) * ④ 정말로 사이트가 없다 (빈 배열) * * ①②③ 은 **고칠 수 있는 문제**이고 ④ 는 사실이다. 그런데 화면이 같은 말을 하므로 사용자는 자격이 * 틀렸는지, 주소가 틀렸는지, 정말 공장이 없는지 알 수 없다 — 실 시스템 연결에서 가장 자주 막히는 * 자리인데 거기서 아무 단서도 주지 않았다. * * ── 왜 문장이 아니라 코드인가 ─────────────────────────────────────────────── * 이 프로젝트가 이미 쓰는 규약을 따른다(주의 신호·커맨드 거절·인제스트 경고와 같은 모양): * `code`(언어 중립) + `params`(문장에 끼울 값) + `message`(영어 canonical, 로그·폴백). * 화면은 `twin.connect.discover.` 로 사람 말을 만든다. * * ── 원 시스템의 오류 문장을 감추지 않는다 ──────────────────────────────────── * 어댑터가 준 사유(자격 거부·타임아웃·404)는 **그 시스템만 아는 사실**이라 우리가 다시 쓸 수 없다. * `detail` 로 그대로 실어 보내 화면이 보조 문장으로 보인다 — 원문이 있으면 사용자가 자기 관리자에게 * 물어볼 수 있고, 없으면 우리 짐작만 남는다. */ /** 발견이 왜 아무것도 주지 못했나 — 언어 중립 코드. */ export type DiscoveryErrorCode = 'reference-not-found' | 'adapter-unknown' | 'discovery-failed' | 'discovery-timeout' export interface DiscoveryError { code: DiscoveryErrorCode params?: Record /** 영어 canonical — 로그에 남고, 번역이 없으면 그대로 보인다. */ message: string } export interface DiscoveryResult { /** 발견된 사이트들. 실패했으면 빈 배열이고 `error` 가 이유를 말한다. */ sites: S[] /** 실패 사유. **없으면 성공이다** — 빈 배열 + 사유 없음 = "연결은 됐지만 사이트가 없다". */ error?: DiscoveryError } /** 등록된 레퍼런스가 없다 — 목록이 낡았거나 다른 테넌트의 것이다. */ export const referenceNotFound = (source: string): DiscoveryError => ({ code: 'reference-not-found', params: { source }, message: `reference "${source}" is not registered in this tenant` }) /** 어댑터 종류를 모른다 — 배포에 그 커넥터가 없다(패키지 미설치·오타). */ export const adapterUnknown = (adapterType: string, source: string): DiscoveryError => ({ code: 'adapter-unknown', params: { adapterType, source }, message: `no connector of type "${adapterType}" is available for reference "${source}"` }) /** 연결·조회가 실패했다 — 원 시스템이 준 사유를 그대로 싣는다. */ export const discoveryFailed = (source: string, detail: string): DiscoveryError => ({ code: 'discovery-failed', params: { source, detail }, message: `could not read sites from "${source}": ${detail}` }) /** 기다려도 답이 없었다 — 무한히 도는 화면보다 사유가 낫다. */ export const discoveryTimedOut = (source: string, seconds: number): DiscoveryError => ({ code: 'discovery-timeout', params: { source, seconds }, message: `"${source}" did not answer within ${seconds}s` }) /** * 어댑터가 준 것을 결과로 처리한다. * * **사이트가 없는 것을 실패로 만들지 않는다** — 연결이 되고 조회가 성공했는데 공장이 하나도 없는 * 시스템은 있을 수 있다(권한이 좁거나 아직 등록되지 않은 경우). 그것은 사실이고, 화면은 "없다" 고 * 말해야 한다. 실패로 합치면 사용자가 있지도 않은 문제를 찾는다. */ export function discovered(sites: readonly S[] | undefined | null): DiscoveryResult { return { sites: Array.isArray(sites) ? [...sites] : [] } } /** 실패를 결과로 처리한다 — 사이트는 비어 있다(반쪽 목록을 성공처럼 보이게 하지 않는다). */ export function failed(error: DiscoveryError): DiscoveryResult { return { sites: [], error } } /** 어댑터가 오류를 낸 것에서 사람이 읽을 사유를 뽑는다 — 형태를 가정하지 않는다. */ export function detailOf(e: unknown): string { if (e instanceof Error && e.message) return e.message if (typeof e === 'string' && e) return e /* 무엇이 왔는지 모르면 그렇게 말한다 — 빈 문장으로 "이유 없음" 처럼 보이게 하지 않는다. */ return 'the connector failed without a message' }