React 컴포넌트와 훅
App 프런트엔드 코드를 위해 @nexia/sdk가 공개하는 컴포넌트와 훅, 등록 API입니다.
React 컴포넌트와 훅
작업에 맞는 항목에서 import와 예시를 찾으세요. 패키지 루트는 순수 계약과 헬퍼, /host는 호스트에 연결된 런타임 API, /testing은 테스트 헬퍼를 제공합니다. 등록한 화면은 바인딩이 설정된 NEXIA 호스트 안에서 실행합니다.
API 찾기
| 작업 | 시작할 API |
|---|---|
| 경로·Inspector 등록 | autoRegisterPackageSurfaces |
| 화면 배치 | WorkSurface, NxPageFrame, NxAdaptiveSplit |
| 폼 작성 | NxFormField, NxTextInput, 계층형 선택 |
| 목록 조회·표시 | ResourceTable, useResourceListParams |
| Resource의 다른 보기 | Board, calendar, scheduler |
| 데이터 가져오기·이관 | 마이그레이션 workspace와 작업 상태 |
| 조회·권한 판정 | api, query 훅, 권한 헬퍼 |
| 관련 작업 열기 | Inspector와 작업 탭 |
| 오류 표시·값 서식 | 메시지와 formatter |
| 업무 흐름 연결 | Approval, Signature, agent 바인딩 |
| 테스트 호스트 사용 | appTestServer, appTestI18n |
작업별 호스트 API
아래 API는 모두 @nexia/sdk/host에서 가져옵니다. Core의 resources/js/app-sdk/configure-host.ts가 컴포넌트·훅·runtime·registry를 연결합니다. SDK export와 Core binding은 tenant/user에게 실제 사용 권한이 있다는 뜻이 아닙니다. 순수 헬퍼는 binding 없이 실행하며, useFeedbackAction과 useDataMigrationBackgroundOperation처럼 SDK가 조합하는 헬퍼는 기존 호스트 API·훅을 사용합니다.
| 공개 API | 역할 |
|---|---|
NxCanonicalRoutePane, useWorkSurfaceLabels | Canonical route pane과 공통 제목·breadcrumb label |
NxLinkButton, NxIconLink, NxDropdownMenu, NxTooltip, NxHelpTooltip | 경로 링크, 동작 메뉴와 문맥 도움말 |
NxInsightSurface, NxInsightFilterBar, NxOverviewBand, useInsightFilterState | 분석 화면 배치, 필터 상태와 요약 카드 |
NxOrgChart, OperatingUnitManagementSurface | 조직도 렌더러와 호스트 Operating Unit 관리 화면 |
NxOrganizationTargetSelector, useOrganizationTargetsQuery, useOrganizationListScope | 읽기 인가된 조직 target과 일반 목록 URL 범위; 페이지 소유 선택기는 Route Surface organizationScope 슬롯에 배치 |
useLegalEntityApplicabilityQuery, useLegalEntityMembersQuery, useOperatingUnitSelection | 전달한 권한·문맥에 따른 적용 가능 범위, 구성원 조회와 Operating Unit 선택 |
NxMissingRequiredReferencesAlert, ResourceInspectorState, ResourceListToolbarMoreMenu | 필수 참조 안내, Inspector 로딩·오류·빈 상태 표시와 목록 더보기 동작 |
useRegisterResourceCreateReceiver, useResourceCreateCompletion | 문맥에 따른 생성 결과 수신기를 등록하고 원래 화면으로 완료 전달 |
SignatureAuthenticationMethodSelector, SignatureInvitationChannelSelector, useTrustedAssetsQuery | 서명 인증·초대 방식 선택과 인가된 trusted asset 조회 |
useApprovalSharedLinesQuery, processUserTaskFormRegistry | 공유 결재선 조회와 App Process user-task 폼 등록 |
시그니처
순수 계약은 패키지 root에서 import합니다. 호스트에 바인딩된 컴포넌트와 훅, registry, HTTP client는 host subpath에서 import합니다.
import type { ResourceListSchema } from "@nexia/sdk";
import {
WorkSurface,
ResourceTable,
NxSearchField,
useResourceListParams,
api,
can,
} from "@nexia/sdk/host";패키지가 런타임 라이브러리를 peer dependency로 선언하므로, 여러분 App은 자기 것을 번들에 넣지 않고 호스트의 사본을 공유합니다.
{
"peerDependencies": {
"@nexia/sdk": "0.5.0",
"@tanstack/react-query": "^5.99.0",
"axios": "^1.11.0",
"react": "^19.1.0"
}
}이것들을 App의 dependencies에 절대 추가하지 마세요. 번들 안 두 번째 React나 Query 클라이언트가 컨텍스트를 깨뜨립니다.
App 프런트엔드 경계 규칙
App 프런트엔드 소스가 import하는 모든 외부 패키지는 그 App의 package.json에 선언해야 합니다. Core가 제공하거나 hoist한 패키지도 암묵적 의존성이 아닙니다. 예를 들어 현재 호스트 설치가 axios를 우연히 해석하더라도 선언 없이 import하면 phantom dependency입니다.
런타임 singleton인 react, react-dom, @tanstack/react-query, @nexia/sdk는 peerDependencies에 둡니다. App이 axios를 import한다면 저장소 관례인 ^1.11.0 peer로 선언합니다. 테스트 전용 도구는 devDependencies에 둘 수 있지만 런타임 import가 그것에 기대면 안 됩니다.
경계를 넘는 상호작용은 SDK 계약만 거칩니다. 순수 계약은 패키지 root에서, 호스트에 바인딩된 API는 /host subpath에서 사용합니다. #app/*는 resources/js 안을, #lang/*는 고정된 resources/lang 계약 경로 안을 해석합니다. 둘 다 App 내부 package import일 뿐이며 Core alias, App 간 import, 다른 패키지 내부 import를 허용하지 않습니다.
package imports target은 정확히 일치해야 하므로 #app/resources/orders/order-resource-contract.ts처럼 #app specifier에 소스 확장자를 포함합니다.
호스트는 다음의 고정된 형태로만 App을 발견합니다.
composer.json→extra.nexia.appresources/js/index.tsresources/js/resources/*/*-resource-contract.tsresources/lang
이 discovery surface를 옮기거나 이름을 바꾸지 마세요. 그 내용과 App 아래의 다른 모든 모듈은 App-private이며, discovery는 일반 import API가 아닙니다.
SDK 자신은 path alias가 아니라 exports condition으로 해석됩니다.
development condition이 TypeScript 소스를 직접 제공하므로 dev 서버, Vitest,
typecheck에는 빌드 산출물이 필요 없습니다. Production 빌드는 import
condition으로 dist를 해석하며, task frontend:build가 이를 먼저 컴파일합니다.
SDK subpath를 찾지 못하면 import를 바꾸기 전에 설치된 SDK 버전과 package exports를 확인하세요.
최소 예시
App 엔트리는 화면과 Inspector 어댑터를 지연 등록합니다. 관례적 배치라면 호출 두 번으로 끝납니다.
import {
autoRegisterPackageResourceInspectors,
autoRegisterPackageSurfaces,
} from "@nexia/sdk/host";
autoRegisterPackageResourceInspectors(
import.meta.glob("./resources/*/inspector/register-*-inspector.ts", {
eager: true,
})
);
autoRegisterPackageSurfaces({
appPrefix: "Workshop",
appKey: "workshop",
overview: () => import("./overview/WorkshopOverviewSurface"),
surfaces: import.meta.glob("./resources/*/surface/*Surface.tsx"),
});
appComponentRegistry는 호스트 전용입니다. 플랫폼의resources/js/shell/runtime/app-component-registry.ts에 존재하지만@nexia/sdk가 export하지 않으므로, App Package는 금지된@shell/별칭으로만 닿을 수 있습니다. 옛 문서는appComponentRegistry.register(...)를 보여줍니다. 대신autoRegisterPackageSurfaces를, 에이전트 컴포넌트에는registerAgentComponent를 쓰세요.
두 등록 모두 파일 시스템에서 파생됩니다. autoRegisterPackageResourceInspectors는
./resources/{resource}/inspector/register-{resource}-inspector.ts 밖의 경로를
거부하므로, Inspector side-effect import 목록을 손으로 관리할 일은 없습니다.
인자
AutoRegisterPackageSurfacesOptions입니다.
| 필드 | 타입 | 필수 | 동작 |
|---|---|---|---|
appPrefix | string | 예 | 파생 컴포넌트 이름의 PascalCase 접두, 예를 들어 Workshop |
appKey | string | 아니오 | App key, 등록 범위를 정하는 데 쓰임 |
overview | PackageSurfaceLoader | 아니오 | App의 필수 overview 화면 |
surfaces | Record<string, () => Promise<unknown>> | 예 | 경로 → 지연 로더 맵, 보통 import.meta.glob에서 |
overrides | Record<string, PackageSurfaceOverride> | 아니오 | 파생 경로를 벗어난 화면의 명시적 이름 → 로더 또는 로더와 선행 route prefetcher |
컴포넌트 이름은 화면 파일 경로에서 파생됩니다. 옮겨졌거나 이름이 바뀐 화면은 overrides 항목이 필요하고, 없으면 백엔드가 게시한 pageElements() 이름에 바인딩이 없습니다.
Route prefetch override
PackageSurfaceOverride에는 기존의 bare lazy loader를 그대로 넣거나,
load와 선택적 routePrefetcher를 가진 명시적 등록을 넣을 수 있습니다.
Note 상세 endpoint와 생성된 cache key를 사용하는 예시입니다. noteDetailQueryOptions를 Resource 내부 쿼리 모듈로 옮겨 entry와 NoteShowSurface에서 함께 import하고, 상세 화면의 기존 로컬 함수는 제거하세요. endpoint가 저장된 소유자를 기준으로 인가합니다. Legal Entity 목록을 미리 읽기 위해 Shell 문맥에서 대상을 추정하지 마세요.
import { queryOptions } from "@tanstack/react-query";
import { api, autoRegisterPackageSurfaces } from "@nexia/sdk/host";
import {
type NoteRecord,
noteApiRoute,
} from "#app/resources/notes/note-resource-contract.ts";
function noteDetailQueryOptions(tenantId: string, id: string) {
return queryOptions({
queryKey: ["workshop-note", tenantId, tenantId, id],
queryFn: async ({ signal }) => {
const { data } = await api.get<{ note: NoteRecord }>(
noteApiRoute(id),
{ signal },
);
return data;
},
});
}
autoRegisterPackageSurfaces({
appPrefix: "Workshop",
appKey: "workshop",
overview: () => import("./overview/WorkshopOverviewSurface"),
surfaces: import.meta.glob("./resources/*/surface/*Surface.tsx"),
overrides: {
WorkshopNoteShowSurface: {
load: () => import("./resources/notes/surface/NoteShowSurface"),
routePrefetcher: ({ queryClient, tenantId, params }) => {
if (!tenantId || !params.id) return false;
void queryClient.prefetchQuery(
noteDetailQueryOptions(tenantId, params.id),
);
return true;
},
},
},
});호스트는 surface chunk 요청과 같은 navigation tick에 이 prefetcher를 시작합니다.
true를 반환하면 해당 route를 처리한 것으로 보고 surface module의 뒤늦은
prefetch fallback을 생략합니다. 권한이나 route parameter가 없을 때처럼
false를 반환하면 fallback을 유지합니다. Context에는 Legal Entity, pathname,
search parameter, route pattern, 해석된 route parameter도 들어 있습니다.
Prefetcher가 surface component를 eager import하지 않게 분리하세요. 그렇게 import하면 surface가 App entry chunk에 들어가 code splitting이 사라집니다.
반응형 레이아웃 인자
NxResponsiveRegion은 일반 HTMLAttributes<HTMLDivElement>를 받고, 자신에게
실제로 할당된 가로 폭을 하위 CSS 컨테이너 쿼리에 제공합니다.
NxAdaptiveSplit은 그 영역을 직접 만들며 다음 옵션을 더합니다.
| Prop | 타입 | 필수 | 기본값 | 동작 |
|---|---|---|---|---|
children | ReactNode | 예 | — | 소스 순서대로 렌더링. 주 콘텐츠 다음에 aside를 두는 구성이 기본 관례 |
density | compact | default | 아니오 | default | 간격을 1rem 또는 1.5rem으로 설정 |
asideWidth | narrow | default | wide | 아니오 | default | 두 번째 열에 18rem, 22.5rem, 26rem 중 하나를 예약 |
twoColumnAt | compact | default | wide | 아니오 | default | 할당 폭이 48rem, 56rem, 64rem 중 선택한 기준에 도달하면 2열 활성화 |
className | string | 아니오 | — | 적응형 그리드에 클래스 추가 |
containerClassName | string | 아니오 | — | 바깥 컨테이너 쿼리 경계에 클래스 추가 |
결과 또는 반환: 레이아웃 프리미티브
레이아웃 마크업을 쓰는 대신 이것들을 import하세요. Shell의 간격과 밀도, 다크 모드 동작을 담고 있습니다.
| 컴포넌트 | 쓰는 곳 |
|---|---|
WorkSurface, WorkSection | 표준 App 페이지 프레임과 그 섹션 |
NxPageFrame, AppRouteFrame | 페이지 수준 프레임 |
NxResponsiveRegion, NxAdaptiveSplit | 브라우저가 아니라 탭·pane에 할당된 폭에 반응하는 레이아웃 |
NxTabNav | App 작업 영역 하나 안에서 섹션을 전환하는 밑줄 탭 |
NxSectionStack, NxSectionCard, NxDetailRow, NxActionButton | 섹션 간격, 카드 섹션, 라벨 있는 값, 행 동작 |
NxFieldGroup, NxFieldset, NxFieldLegend, NxFormSection, NxFormField | 폼 구조 |
NxResourceInformationView, NxResourceInformationForm | 하나의 정렬된 Resource 정보 스키마를 읽기·쓰기 화면으로 투영 |
NxEmptyView, NxLoadingBlock | 빈 상태와 로딩 상태 |
NxModalDialog, NxAlert | 오버레이와 인라인 알림 |
화면 내 탭 내비게이션
NxTabNav는 호스트의 공용 밑줄 탭 상호작용을 렌더링하고, 선택 값과 아래에
보여 줄 내용은 App이 소유합니다. 이 컴포넌트는 이동하거나 데이터를 조회하지
않고 상태를 저장하지도 않습니다. 탭을 새로고침 뒤에도 유지하거나 URL로 공유해야
한다면 App이 URL 동기화를 맡습니다.
import type { NxTabNavItem } from "@nexia/sdk";
import { NxTabNav } from "@nexia/sdk/host";
const tabs: NxTabNavItem[] = [
{ id: "daily", label: t("attendance.tabs.daily") },
{
id: "issues",
label: t("attendance.tabs.issues"),
badge: openIssueCount,
},
];
<NxTabNav
aria-label={t("attendance.tabs.label")}
items={tabs}
value={activeTab}
onChange={setActiveTab}
scrollable
/>;items, value, onChange는 필수입니다. 각 NxTabNavItem에는 id와
label이 있고, badge, disabled, testId를 선택적으로 넣을 수 있습니다.
전체 탭이 할당된 pane 폭에 들어가지 않을 수 있으면 scrollable을 사용하고,
aria-label에는 탭 하나의 라벨을 반복하지 말고 탭 그룹의 이름을 넣습니다.
섹션 간격의 소유권
AppRouteFrame은 직계 섹션 자식 사이의 간격을 소유합니다. 완성된 섹션을
둘 이상 형제로 렌더링하는 재사용 컴포넌트는 NxSectionStack을 사용하고,
space-y-section이나 로컬 gap으로 같은 계약을 다시 만들지 않습니다.
NxFormSection은 간격을 명시적으로 지정해야 합니다. 한 섹션 안의 필드는
spacing="stack"을 사용하고, 직계 자식이 서로 독립된 섹션 카드이면
spacing="section"을 사용합니다.
공용 Resource 정보 스키마
일반 CRUD Resource는 defineResourceInformationSchema로 섹션과 필드를 한 번만
선언합니다. 필드 하나가 라벨 키, 도움말 또는 정의 키, 필수성 분류, 반응형 너비,
투영 모드, 읽기 값, Edit 컨트롤을 함께 소유합니다.
NxResourceInformationView와 NxResourceInformationForm은 이 계약에서 같은
섹션과 필드 순서를 유지합니다.
미저장 폼을 자동 입력하려면 NxResourceInformationForm에
binding={{ effect: 'draft', disabled: pending }}을 전달합니다. 공개된 Resource
경로가 리소스 키, 모드, 레코드 식별자와 경로 범위를 제공합니다. 해당 경로 소유자
밖에서는 resourceKey, resourceId, scopeKey를 명시하세요. 기존 스키마의
필드를 별도 Agent 목록에 복제하거나 모든 Resource에 Agent 등록을 추가하지 않습니다.
아래 NxTextInput.onValueChange처럼 컨트롤의 값 변경 콜백을 사용하세요.
이벤트만 전달하는 onChange는 안전한 초안 입력 계약을 제공하지 않습니다.
기존 필드 라벨과 현재 선택 가능한 옵션이 화면에 등록된 액션을 설명합니다.
숨김·민감·읽기 전용·비활성·미지원 컨트롤은 제외합니다. binding이 없거나
effect: 'autosave'이면 초안 액션을 공개하지 않으며, 페이지의 권한 검사도 유지합니다.
공용 배치는 전달된 필드 최대 32개를 쓰기 전에 검증하고 사용자 수정을 보호하며,
화면 반영 후 미저장 초안 결과를 반환합니다. 저장은 실행하지 않습니다.
Agent가 실행하는 화면 콜백은 effect: 'read' | 'draft'를 선언합니다. 공용 목록·
선택지 검색·폼 호스트가 공통 등록 지점에서 제공하므로 Resource 작성자가 중복
선언하지 않습니다. 분류되지 않은 콜백과 임의 DOM 이벤트 입력·클릭은 실행하지
않습니다. 명시적으로 요청된 저장은 작업 정책과 확인 흐름을 갖춘 기존 서버
변경 도구를 사용합니다.
원격 단일 선택 컨트롤의 NxCombobox.searchOptions에는 scopeKey와
인가된 옵션을 반환하는 search(query, signal?)를 전달합니다. 선택기가 이미
사용하는 조회와 캐시를 재사용하세요. 검색은 값을 선택하거나 저장하지 않으며,
초안 입력은 현재 로드된 목록의 활성 옵션만 허용합니다. 이 검색 계약은 다중
선택을 공개하지 않습니다.
중첩 정보 스키마는 기존 섹션·필드 키로 입력 대상을 식별합니다. 반복 섹션에는
행 순서가 바뀌어도 유지되는 키가 필요하며 배열 위치는 이 조건을 충족하지 않습니다.
binding 안의 NxOrgChart는 기존 선택 가능한 노드와 제어형
selectedId/onSelect로 초안 선택을 수행합니다. 노드 이동, 계층 편집,
읽기 전용 Inspector 차트의 자동 조작 권한을 추가하는 계약은 아닙니다.
정보 스키마가 없는 커스텀 폼도 NxFormSection.binding과 일반 입력란의
NxFormField.fieldKey로 같은 공용 초안 기능을 사용할 수 있습니다. 스키마가
필드 식별자를 제공하면 중복 선언하지 않습니다. sensitive인 필드는 제외하며
기존 입력 함수와 선택지를 그대로 사용합니다. 권한이 없거나 저장 중일 때,
포함된 폼의 변경 콜백이 데이터를 저장할 수 있을 때는 바인딩을 비활성화합니다.
반복 행에는 안정적인 식별자가 필요합니다.
import {
defineResourceInformationSchema,
resourceInformationValue,
} from "@nexia/sdk";
import {
NxTextInput,
} from "@nexia/sdk/host";
interface DetailContext { item: { name: string } }
interface FormContext { name: string; setName: (value: string) => void }
const schema = defineResourceInformationSchema<DetailContext, FormContext>()({
sections: [
{
key: "basics",
titleKey: "orders.sections.basics",
fields: [
{
key: "name",
labelKey: "orders.name.label",
helpKey: "orders.name.help",
requirement: "save",
view: ({ item }) => resourceInformationValue(item.name),
edit: {
kind: "control",
render: ({ context }, slot) => (
<NxTextInput
id={slot.id}
aria-describedby={slot.describedBy}
value={context.name}
onValueChange={context.setName}
/>
),
},
},
],
},
],
});정적인 View/Create/Edit 생략에는 modes를 사용합니다. 컨텍스트에 따른
visibility.view, visibility.create, visibility.edit는 이 mode 계약을 확인한 뒤
실행됩니다. 넓은 필드는 span: "full"로 선언합니다. Section은 Form 전용
formColumns를 1 또는 2로 정하고 선택적 aggregate/projection source를 가질 수
있으며 View는 순서가 있는 detail list를 유지합니다. visibleEmptyRequirements는
NxResourceInformationView에서 비어 있어도 보여 줄 requirement 종류를 정합니다.
복합 입력은 custom Edit 투영을
사용하되, 그 필드의 View 값과 정본 위치는 그대로 둡니다. Route 로딩, mutation,
권한 판단, 관련 Resource 동작, Inspector 밀도는 Surface가 계속 소유합니다.
같은 정보를 위해 Show와 Form 필드 목록이나 화면별 섹션 번역 키를 따로 만들지
마세요.
컨테이너 기준 반응형 레이아웃
주 콘텐츠와 보조 영역으로 나누는 흔한 화면에는 NxAdaptiveSplit을 씁니다.
컴포넌트 자체에 충분한 폭이 들어올 때만 2열이 됩니다. 브라우저가 넓더라도
Activity Center를 열거나 탭을 좁은 분할 pane으로 옮기면 aside가 주 콘텐츠
아래로 내려갑니다.
import {
NxAdaptiveSplit,
NxSectionCard,
NxSectionStack,
} from "@nexia/sdk/host";
export function OrderDetail() {
return (
<NxAdaptiveSplit>
<NxSectionStack>
<NxSectionCard title="주문">주문 필드</NxSectionCard>
</NxSectionStack>
<aside>
<NxSectionStack>
<NxSectionCard title="처리">주문 처리</NxSectionCard>
<NxSectionCard title="이력">주문 이력</NxSectionCard>
</NxSectionStack>
</aside>
</NxAdaptiveSplit>
);
}2열 분할이 아닌 레이아웃은 NxResponsiveRegion으로 경계를 만듭니다. 하위의
컨테이너 variant는 window.innerWidth가 아니라 가장 가까운 영역을 측정합니다.
import { NxResponsiveRegion } from "@nexia/sdk/host";
<NxResponsiveRegion>
<div className="grid gap-4 @min-[36rem]:grid-cols-2">
<section>요약</section>
<section>근거</section>
</div>
</NxResponsiveRegion>;Core는 두 facade를 네이티브 CSS 구현에 바인딩합니다. 실제 소스 예시는
resources/js/core/approval/ApprovalCaseSurface.tsx입니다. 작업 영역이 분할
기준보다 좁아지면 처리와 이력이 문서 아래로 이동합니다. resize listener나
ResizeObserver는 사용하지 않습니다.
조직 선택
SDK는 범용 NxCombobox도 이미 제공합니다. 조직 전용 컨트롤은 공통 선택
컴포넌트를 재사용하면서 조직에 맞는 표시 규칙을 적용합니다.
페이지 목록의 조직 범위는 NxOrganizationTargetSelector로 표시합니다.
useOrganizationListScope와 함께 @nexia/sdk/host에서 가져오고,
해당 페이지의 읽기 권한을 명시합니다. 훅의 호출 형식은
useOrganizationListScope(tenantId, permission, mode = "legal-entity-list", enabled = true)입니다.
| 선택기 모드 | 입력과 선택 값 |
|---|---|
legal-entity-list | 인가된 options; 여러 Legal Entity를 고르는 value: string[], onChange(ids) |
legal-entity-operating-unit-list | 정확히 인가된 쌍인 targets; LE와 OU 축을 담는 value: OrganizationTargetQuery, onChange(query) |
legal-entity-single | 인가된 options; 생성·실행 대상을 하나 고르는 value: string | null, onChange(id) |
인가된 선택지가 하나면 선택기는 요약을 표시하며 value를 설정하거나
onChange를 호출하지 않습니다. 단일 대상의 기본값은 Surface가 결정합니다.
선택 동작이 필요한 생성·복구 폼에서는 아래의 폼 계약을 사용하세요.
훅은 두 목록 모드를 지원합니다. Route Surface 안에서 반환된 selectorProps를
프레임 슬롯에 바로 연결하세요.
import {
NxOrganizationTargetSelector,
NxPageFrame,
useOrganizationListScope,
} from "@nexia/sdk/host";
const scope = useOrganizationListScope(tenantId, readPermission);
<NxPageFrame
organizationScope={<NxOrganizationTargetSelector {...scope.selectorProps} />}
>
{children}
</NxPageFrame>;예시의 tenantId, readPermission, children은 사용하는 Surface가 제공합니다.
LE/OU 목록은 훅의 세 번째 인자에 "legal-entity-operating-unit-list"를 전달합니다.
NxPageFrame은 breadcrumb 다음, actions 앞에 선택기를 배치합니다.
AppRouteFrame도 organizationScope를 받습니다. 프레임은 배치를 맡고,
Surface는 권한·지원 축·목록 질의를 맡습니다. actions, 표 도구 모음,
일반 열 필터에 같은 선택기를 중복 배치하지 마세요.
scope.requestParams !== undefined일 때만 목록을 요청하고, 반환된
URLSearchParams를 요청에 전달하며 선택 범위를 캐시 키에도 포함합니다.
빈 파라미터 집합은 인가된 모든 대상을 뜻합니다. undefined는 로딩 중이거나
범위가 잘못되었거나 사용할 수 없거나 비활성화된 상태입니다. 이를 빈 집합으로
바꿔 더 넓은 목록을 요청하지 마세요. 선택기는 status로 준비되지 않은 상태를
표시합니다. 목록의 표준 URL 키는 legal_entity_public_ids[]와
operating_unit_public_ids[]입니다.
대상 목록은 해당 권한으로 useOrganizationTargetsQuery를 호출해 가져옵니다.
응답의 정확한 LE/OU 쌍을 유지하고 독립 목록을 곱해 조합하지 마세요. 최종 인가는
백엔드가 수행합니다. 사용자 정의 관계나 여러 권한을 사용하는 페이지는 자체
URL adapter와 대상 조회를 유지하면서 공통 선택기를 쓸 수 있습니다. 표준 테넌트
기준정보에는 선택기가 없습니다. 다만 테넌트 소유 사용자 정의 관계나 Operating
Unit 페이지는 조직 읽기 범위를 명시할 수 있습니다. 레코드 소유권만으로 목록
범위를 정하지 않습니다.
폼의 대상 입력은 헤더 범위 슬롯이 아니라 본문에 둡니다.
NxResourceInformationForm.legalEntity는 패키지 루트에서 공개하는ResourceFormLegalEntity를 받습니다. 생성은{ kind: "select", value, options, onChange }에 선택적으로disabled,error를 더합니다. 수정은 저장된 레코드나 인가된 조회에서 얻은{ kind: "stored", label }을 사용합니다. 호스트가 첫 스키마 섹션 맨 앞에 전체 너비로 배치합니다. 필요 없으면 생략하고, 폼 스키마의 중복 소유자 필드는 제거하되 View/Inspector 필드는 유지하세요.- 사용자 정의 폼은
/host의NxResourceFormLegalEntityField를 첫 입력 섹션 안에 두고target과mode="create"또는mode="edit"를 전달합니다. /host의NxOperatingUnitSelector는 기본 문구가 지역화된 전체 너비의 단일 OU 폼 입력입니다. 인가된options,value,onChange를 전달합니다. 대상 조회와 LE 변경 시 종속 값 초기화는 App이 맡습니다. 페이지나 Shell의 범위를 변경하는 컨트롤이 아닙니다.
기존 OperatingUnitScopeField는 scope prop으로 OperatingUnitScopeResult를
받아 선택된 OU 범위 패널을 표시합니다. 위의 LE/OU 다중 선택 목록 계약이나
폼 입력과는 용도가 다릅니다. 이 계약이 필요한 기존 흐름은 용도를 명시하고,
새 페이지 목록 범위는 NxOrganizationTargetSelector를 사용하세요. 상세·수정은
저장된 소유자를 표시하며 어떤 선택기도 Shell 전역의 활성 조직을 만들지 않습니다.
폼과 입력 컨트롤
| 컴포넌트 | 비고 |
|---|---|
NxTextInput, NxTextArea | 텍스트 입력 |
NxSelect, NxCombobox | 선택. NxComboboxOption / NxSelectOption이 옵션 타입을 정함 |
NxCheckbox | 불리언 |
NxRadio, NxRadioGroup | 세로 또는 가로 group 방향을 가진 단일 선택 |
NxDatePicker, NxDateRangePicker | locale-aware 단일 날짜와 제한된 날짜 범위 선택 |
NxFileDropzone | 업로드. useUploadAttachment 또는 useUploadMedia와 짝지어 쓰기 |
NxColumnBrowser | 계층형 선택을 화면 안에서 탐색하는 Miller column |
NxColumnPicker | NxColumnBrowser를 조합한 모달 선택 흐름 |
NxOperatingUnitSelector, NxResourceFormLegalEntityField | 인가된 OU 입력과 공용 Legal Entity 폼 필드 |
OperatingUnitScopeField | Operating Unit 범위 선택 |
NxButton, NxIconButton, NxActionButton | 텍스트, 아이콘 전용, 의도 타입 동작. 아이콘 전용 버튼에는 aria-label 필수 |
NxLinkButton, NxIconLink | 버튼형·아이콘 전용 경로 링크. 아이콘 전용 링크에는 aria-label 필수 |
NxRefreshControl | compact, section, snapshot 배치를 위한 호스트 소유 수동 새로고침 |
제출, 재시도, 대화상자 열기, 데이터 변경 같은 명령에는 NxButton,
NxIconButton, NxActionButton을 사용합니다. 컨트롤을 누를 때 경로가 바뀌면
NxLinkButton href 또는 NxIconLink href를 사용합니다. 실제 링크 목적지를
유지하므로 브라우저 링크 동작을 지원하고, 내부 경로를 우클릭하면 Shell의
새 작업 탭에서 열기를 사용할 수 있습니다. NxLinkButton의 onNavigate는
선택적인 이동 부수 효과이며 href를 대신하지 않습니다.
NxCombobox multiple에서는 호스트가 패널의 고정 푸터에 지역화된 닫기 동작을
추가합니다. 선택 값을 필드 아래의 제거 가능한 칩으로 계속 보여줘야 한다면
showSelectedItems를 설정합니다. 필드에 더 구체적인 접근성 레이블이 필요하면
selectedItemsAriaLabel과 removeSelectedItemLabel을 사용합니다.
카드형 라디오
NxRadio의 variant는 "default" | "card"를 받습니다. 생략하면 기존 원형
라디오를 표시합니다. 선택지에 설명이나 넓은 클릭 영역이 필요할 때 card를
사용하세요. 두 표현 모두 네이티브 라디오와 NxRadioGroup의 value/name/onChange
계약을 사용하며, 키보드 이동과 비활성화 동작도 같습니다. inputSize는 기본형의
원형 표시 크기에만 적용됩니다.
label은 React 콘텐츠를 받으므로 설명은 별도 속성 대신 label 안에 넣습니다.
label 내부에는 버튼·링크·다른 입력을 중첩하지 마세요. 그룹에 접근 가능한 이름을
제공하고, 같은 너비의 카드는 그룹 안에 grid를 배치해 구성합니다. 복수 선택은
체크박스를 사용하며, 누르는 즉시 작업을 실행하는 버튼은 라디오로 바꾸지 않습니다.
import { NxRadio, NxRadioGroup } from "@nexia/sdk/host";
<NxRadioGroup value={mode} onChange={setMode} aria-label="입력 방식">
<div className="grid gap-3 md:grid-cols-2">
<NxRadio variant="card" value="new" label={
<>
새 항목 등록
<span className="mt-1 block font-normal">새 항목의 정보를 입력합니다.</span>
</>
} />
<NxRadio variant="card" value="existing" label="기존 항목 사용" />
</div>
</NxRadioGroup>이 옵션을 사용하는 App에는 업데이트된 SDK와 카드형을 구현한 Core 호스트가 모두 필요합니다. 기존 호출부는 수정하지 않아도 됩니다.
즐겨찾기
App이 Core에서 이미 즐겨찾기 가능 대상으로 선언한 지속적인 대상을 표시할 때는
호스트 바인딩 FavoriteButton을 사용합니다. 컴포넌트와 대상 타입은 package root에서
가져옵니다.
import { FavoriteButton, type FavoriteTarget } from "@nexia/sdk";
const target: FavoriteTarget = {
kind: "record",
resourceKey: "workshop.note",
resourceId: note.public_id,
};
<FavoriteButton target={target} size="xs" testId="note-favorite" />;FavoriteTarget은 레코드 식별자(resourceKey, resourceId) 또는 서버가 선언한
페이지 식별자(key)입니다. Button props는 target, 선택적
size("xs", "sm", "md"), 선택적 testId뿐입니다. 상태, 요청, 레이블, 서버
바인딩은 호스트가 소유합니다. App은 이 control에 title, href, permission, 임의 URL을
보내거나 저장하면 안 됩니다. 즐겨찾기는 접근 권한을 부여하지 않습니다.
상세 header에서는 compact control을 NxPageFrame의 breadcrumbActions slot에
배치하세요. 목록 primary cell에서는 서버 metadata를 favoriteTarget으로 전달하거나,
host가 먼저 인가할 수 있도록 owner record identity를 favoriteCandidate로 전달하세요.
cell link가 행 owner가 아닌 관련 record를 가리키면 favoriteable={false}를 설정합니다.
이 입력으로 App 소유의 별도 즐겨찾기 state나 custom paging 계약을 만들지 않습니다.
계층형 선택
계층 구조가 페이지 콘텐츠의 일부라면 NxColumnBrowser를 사용하세요. leaf 하나나
여러 개를 선택하는 과정이 차단형 모달 단계라면 NxColumnPicker를 사용합니다. 두
컴포넌트는 같은 NxColumnNode[]를 받고 동일한 leaf 라벨과 keyword를 검색합니다.
Browser는 overlay chrome을 소유하지 않습니다. Picker는
Browser를 호스트의 가운데 정렬 NxModalDialog 안에 조합하고 Select 동작으로
대기 중인 leaf를 확정합니다.
이식 가능한 node와 props 타입은 package root에서, 호스트 바인딩 renderer는
/host에서 가져옵니다.
import type { NxColumnNode } from "@nexia/sdk";
import { NxColumnBrowser, NxColumnPicker } from "@nexia/sdk/host";
const roots: NxColumnNode[] = [
{
value: "platform",
label: t("apps.platform"),
children: [{ value: "tenant.user", label: t("resources.users") }],
},
];
<NxColumnBrowser
roots={roots}
value={resourceKey}
onSelect={setResourceKey}
ariaLabel={t("resources.choose")}
/>;
<NxColumnPicker
open={pickerOpen}
onClose={() => setPickerOpen(false)}
title={t("resources.choose")}
roots={roots}
value={resourceKey}
onSelect={setResourceKey}
/>;번역된 라벨, node 값, disabled 상태, 선택 값과 선택 뒤 실행할 데이터 조회·변경은
App이 소유합니다. 인라인 Browser의 onSelect는 leaf를 선택했을 때 실행되고,
branch 행은 column 이동만 수행합니다. onActivate로 Enter나 double click 동작을
별도로 추가할 수 있습니다. 마지막 column에는 renderPreview를 사용하고, branch를
바꿀 때 기존 leaf 선택을 해제해야 한다면 onClearSelection을 연결하세요.
다중 선택에서는 multiple을 설정하고 values를 전달하며 확정된 leaf 목록을
onSelectMany(values, nodes)로 받습니다. renderSelectionPreview가 제공하는
onRemove(value)로 대기 중인 선택을 제거할 수 있습니다. 단일 mode는 대신
value, onSelect, renderPreview를 사용합니다.
테이블과 목록
| 심볼 | 종류 | 목적 |
|---|---|---|
ResourceTable | 컴포넌트 | 표준 Resource 목록 테이블 |
ResourceTableColumn, ResourceTableSort | 타입 | 컬럼과 정렬 선언 |
ResourcePrimaryCell, ResourceTextCell | 컴포넌트 | 관례적 셀 렌더러 |
ResourceStatusBadge, NxStatusBadge | 컴포넌트 | 상태 표시 |
ResourceRowActionsMenu | 컴포넌트 | 행별 동작 메뉴 |
NxPagination | 컴포넌트 | 페이지네이션 컨트롤 |
useResourceListParams | 훅 | 테넌트·컨텍스트·URL 범위 목록 상태 |
readResourceListParams, serializeResourceTableSort | 함수 | URL 직렬화 |
keepPreviousResourceListData | 함수 | 매끄러운 페이지네이션을 위한 쿼리 옵션 |
컬럼 정렬과 표시
ResourceTable은 대시보드·다이얼로그·상세 화면의 보조 행에도 쓰는 단일 테이블입니다.
이 경우 toolbar와 footer를 생략해 chrome 없이 사용합니다. 테이블별 표시 방식이나
컬럼 구성을 저장할 논리 뷰에만 안정적인 tableId를 주세요. 호스트는 저장할 때 현재
테넌트와 사용자를 이 값에 결합하므로 번역된 문구에서 만들지 마세요.
목록 Surface 안의 발췌입니다. 생성된 Note 목록의 data, 검색 상태, refetch, 번역 함수 t와 useDateTimeFormatter()의 formatDateTime을 사용하며 로딩·오류 처리 뒤에 배치합니다.
import type { ResourceTableColumn } from "@nexia/sdk";
import { ResourceTable, ResourcePrimaryCell, NxSearchField } from "@nexia/sdk/host";
import { type NoteRecord, noteResourceRef } from "#app/resources/notes/note-resource-contract.ts";
const columns: ResourceTableColumn<NoteRecord>[] = [
{
key: "name",
header: t("workshop.note.name.label"),
size: "primary",
cell: (row) => (
<ResourcePrimaryCell
resource={noteResourceRef(row)}
label={row.name}
/>
),
},
{
key: "created",
sortKey: "created_at",
header: t("workshop.note.created_at.label"),
cell: (row) => formatDateTime(row.created_at),
},
{
key: "updated_at",
defaultVisible: false,
header: t("workshop.note.updated_at.label"),
cell: (row) => formatDateTime(row.updated_at),
},
];
<ResourceTable
tableId="workshop.notes"
label={t("workshop.note.list.title")}
columns={columns}
rows={data.data}
schema={data.meta.list_schema}
toolbar={{ leading: <NxSearchField value={search} onChange={setSearch} /> }}
footer
onRetry={() => void refetch()}
getKey={(row) => row.public_id}
renderCard={(row, index) => (
<article className="space-y-2">
{columns[0].cell(row, index)}
<p>{columns[1].cell(row, index)}</p>
</article>
)}
/>;toolbar와 footer chrome은 opt-in입니다. toolbar를 켜면 표준 Filter, 해당 쿼리만
다시 읽는 Refresh, 마지막 More 유틸리티가 구성됩니다. 명시적인 refresh 설정이
없으면 onRetry를 소유 쿼리 refresh 콜백으로 재사용합니다. leading 슬롯에는 검색,
날짜 선택기 등 임의의 조회 컨트롤을 둘 수 있습니다. showLabel은 의미론적 caption을
보이게 하고 labelTooltip은 그 옆에 설명을 붙입니다. 테이블별 스타일·행 높이 설정은
선택적인 UserContext.table_style, UserContext.table_density 전역 기본값보다 우선하며
“전역 설정 사용”으로 되돌릴 수 있습니다. 선언한 column key에 맞춘 집계 행은
summaryRow.cells를 사용하고, 선택적 label이 접근 가능한 행 라벨을 제공합니다.
테이블별 refresh callback은 오류 retry 동작을 바꾸지 않고 onRetry 기반 refresh
fallback만 대체합니다.
백엔드 schema.sortable 목록이 정렬 가능 여부의 정본입니다. 시각 컬럼에
sortable: true를 다시 선언하지 마세요. 시각 key가 백엔드 쿼리 key와 다를
때만 sortKey를 쓰고, schema가 허용한 정렬을 일부러 막을 때만
sortable: false를 씁니다.
카드로 요약해 보여줄 수 있는 Resource라면 앞 예시처럼 renderCard를 추가하세요.
toolbar를 활성화하면(빈 객체도 가능) 표준 더 보기 메뉴에서 테이블과 카드를
전환할 수 있으며, 기본값은 테이블입니다.
검색·필터·페이지·정렬·새로고침·선택·항목 열기는 두 보기에서 같은 상태와 처리를
사용합니다. 보기와 카드 열 수(자동·2열·3열·4열)는 tableId·테넌트·사용자별로
저장됩니다. 정렬은 호출 화면의 기존 URL 또는 쿼리 상태로 유지하며, 카드 정렬
메뉴도 같은 onSortChange를 호출합니다. renderCard는 배치를 구성하며, 기존
컬럼 셀이나 권한 필터가 적용된 공통 액션 renderer를 재사용합니다. 표와 카드의
콜백·권한 판단을 따로 구현하지 마세요.
트리나 summaryRow가 있는 목록은 테이블 전용입니다. 카드 안의 버튼과 링크를
누르면 해당 컨트롤만 동작하고 카드 자체의 열기 동작은 실행되지 않습니다.
목록을 카드로 시작하려면 renderCard와 함께 defaultView="cards"를 지정하세요.
저장된 보기 설정이 있으면 그 설정을 우선하며, renderCard가 없거나 summaryRow가
있는 목록은 표로 표시합니다. Resource 표 실행 예제에서 표와 카드로 시작하는
두 보기를 확인할 수 있으며, 셀과 액션은 같은 구현을 사용합니다.
구조 컬럼이 아닌 모든 컬럼은 더 보기 > 열 구성에 나타납니다.
size: "primary"인 identity 컬럼은 항상 표시되고, 꼭 필요한 다른 표현 전용
컬럼은 hideable: false로 잠급니다. defaultVisible: false는 사용자가 선택을
바꾸기 전까지 숨길 컬럼의 초기값입니다. 저장된 선택은 테넌트·사용자·tableId
단위로 격리되며, 새로 추가된 컬럼은 선언된 기본값을 따릅니다. 현재 정렬 중인
시각 컬럼을 숨기면 그 정렬도 지워져 보이지 않는 컬럼으로 정렬된 상태가 남지
않습니다. 이 표시 플래그는 프런트엔드 전용이며 meta.list_schema에 넣지 않습니다.
Resource 액션의 의미와 표현
같은 동작을 행 메뉴와 상세 화면 또는 Inspector 액션 레일에 함께 보여 줄 때는 유효 권한으로 한 번 필터링한 액션 집합에서 두 표현을 파생합니다. 노출되는 Resource 액션은 보기, 편집, 관리, 전송 또는 재설정, 수명 주기, 삭제 순서를 동일하게 유지합니다. 새 탭에서 열기와 닫기 같은 Host 소유 chrome은 이 순서의 바깥에 놓을 수 있지만 Resource 액션의 순서를 바꾸면 안 됩니다. 프런트엔드의 권한 검사는 노출만 제어하며, 모든 동작은 백엔드에서 다시 인가합니다.
ResourceRowActionsMenu의 각 항목은 동작과 표현의 책임을 나눕니다.
id는 번역하지 않는 안정적인 동작 식별자입니다. Host는show,view,edit,archive,retire,deactivate,delete처럼 알고 있는 ID에 표준 선행 아이콘을 적용합니다.label은 운영자에게 보이는 현지화 문구입니다.- 실제 이동이나 명령은 App이 소유한
onSelect가 수행합니다. Host는 ID나 라벨로 동작을 추론하지 않습니다. destructive는 파괴적 표현을 선언하고,disabled는 사용할 수 없거나 처리 중인 명령의 상태를 유지합니다.
새 동작을 추가하기 위해 SDK iconKey나 App의 아이콘 라이브러리 의존성을
늘릴 필요가 없습니다. ResourceRowActionsMenu가 모르는 ID에는 중립적인 선행
아이콘을 넣습니다. 반복 사용되는 플랫폼 동작으로 자리 잡으면 App 계약을
바꾸지 않고 Host 표준 매핑만 추가할 수 있습니다. App이 icon을 명시했다면
그 아이콘이 우선합니다.
destructive와 dangerGhost는 서로 다른 계층을 설명합니다.
NxDropdownMenuItem.destructive: true는 메뉴 항목이 파괴적 동작이라는 의미입니다.ResourceRowActionsMenu는 이 값으로 파괴적 메뉴 표현을 적용합니다.NxButton variant="dangerGhost"는 같은 의미를 Inspector 또는 상세 화면의 보조 버튼으로 렌더링할 때 사용하는 시각적 표현입니다.
공유 도메인 액션에 버튼 variant를 저장하거나 번역된 라벨을 보고 파괴적
동작인지 추론하지 마세요. 동작의 의미를 한 번만 선언한 뒤 메뉴 항목에서는
destructive: true, 버튼에서는 dangerGhost로 변환합니다. 보기, 편집, 관리
동작은 중립 표현을 유지합니다.
목록 상태에는 useResourceListParams를 사용하세요. 필터, 정렬, 검색어, 페이지를 URL에 저장하므로 링크를 공유하거나 새로고침해도 같은 목록을 볼 수 있습니다.
Resource Projection
Board와 Calendar, Resource Scheduler, Schedule은 Host에 바인딩된 React 컴포넌트입니다. 순수
projection·owner-action 타입은 package root에서, renderer는 /host에서
import합니다.
import type {
NxCalendarOccurrence,
NxCalendarSelection,
NxResourceSchedulerOccurrence,
NxResourceSchedulerSelection,
ResourceBoardProjection,
ResourceScheduleProjection,
} from "@nexia/sdk";
import {
NxCalendar,
NxResourceScheduler,
ResourceBoard,
ResourceSchedule,
} from "@nexia/sdk/host";Host가 facade를 각각 lazy binding하므로 브라우저는 해당 화면을 실제로 그릴 때만 EventCalendar, drag-and-drop, Gantt chunk를 불러옵니다.
| 컴포넌트 | 쓰는 곳 | App이 공급하는 것 |
|---|---|---|
ResourceBoard | Kanban lane과 card | ResourceBoardProjection, localized label, 선택적 typed onMove action |
NxCalendar | 월·주·일·목록 occurrence | NxCalendarOccurrence[], timezone, locale, 상태 label, 선택적 reschedule/resize action과 빈 범위 선택 |
NxResourceScheduler | resource timeline·resource time-grid occurrence | resource lane, NxResourceSchedulerOccurrence[], timezone, locale, 상태 label, 선택적 reschedule/resize/reassign action과 빈 범위 선택 |
ResourceSchedule | Gantt 또는 schedule-timeline 작업 | ResourceScheduleProjection, 선택한 view, 선택적 typed owner action과 호출자 소유 생성 callback |
NxCalendar는 eventAppearance로 채운 band 또는 text-only occurrence를 렌더링하고,
showEmptyState: false로 별도 안내 없이 grid를 유지하며, Resource link가 없는
occurrence에는 onOpenOccurrence를 사용할 수 있습니다. Calendar와 Scheduler의
onOpenOccurrence callback은 호출자 소유 상세 흐름을 열 뿐 레코드 접근 권한을
부여하지 않습니다.
생성과 선택 계약은 다음과 같습니다.
| 심볼 | 정확한 계약 | 소유권 동작 |
|---|---|---|
NxCalendarSelection | NxCalendarTemporalRange: start, end, all_day, timezone | 사용자가 선택한 빈 Calendar 범위를 전달하며 저장된 occurrence를 뜻하지 않음 |
NxResourceSchedulerSelection | NxCalendarSelection과 resource_ids: string[] | 빈 범위와 선택한 resource lane을 전달하며 App 레코드를 생성하지 않음 |
NxCalendarProps.onSelectRange | (selection: NxCalendarSelection) => void | 호출자가 소유한 occurrence 생성 흐름을 열거나 초기값을 채움 |
NxResourceSchedulerProps.onSelectRange | (selection: NxResourceSchedulerSelection) => void | 선택한 resource context를 넣어 호출자 소유 생성 흐름을 열거나 초기값을 채움 |
ResourceScheduleProps.onCreateResource | (parent?: ResourceProjectionReference) => void | 선택적 parent 아래에서 호출자 소유 Resource 생성 흐름을 엶 |
이 컴포넌트들은 App 레코드를 스스로 조회하거나 수정하지 않습니다. App이 자기 API에서 actor-authorized projection을 읽고, 타입 있는 callback을 App 소유 업무 동작에 연결합니다. 빈 범위 선택과 Schedule의 native add affordance는 위 callback만 호출하며, 공용 renderer는 domain record를 생성하거나 저장하지 않습니다. Core는 rendering과 되돌릴 수 있는 optimistic feedback을 제공합니다. 전체 소유 경계는 Resource Projection을 보세요.
useResourceProjectionChange({ tenantId, resource, onChanged, enabled })는 정확한
app_key / resource_key / resource_id identity를 구독합니다. 소유 App은
projection commit 뒤에만 PII-free ResourceProjectionChanged event를 내보냅니다.
Callback은 refetch 힌트일 뿐 데이터나 권한이 아닙니다.
Import와 Export
컴포넌트는 /host에서, 아래 props 타입은 패키지 root에서 import합니다.
| 컴포넌트 / props | 필수 입력과 동작 |
|---|---|
NxResourceTransferActions / NxResourceTransferActionsProps | transferMeta, onImported; 사용 가능한 transfer 화면으로 이동. 현재 Core는 화면 이동을 처리하므로 이 컴포넌트에서 onImported를 호출하지 않음 |
NxResourceImportDialog / NxResourceImportDialogProps | open, onClose, previewUrl, importUrl, templateUrl, onImported; 일반 Resource 업로드·preview·실행·polling |
NxDatasetImportWorkspace / NxDatasetImportWorkspaceProps | resourceKey; 선택적으로 onCompleted, onQueued, 참조 생성 동작. 조직 문맥은 호스트에서 읽으며 조직 ID props를 받지 않음 |
NxDatasetExportWorkspace / NxDatasetExportWorkspaceProps | resourceKey; 선택적으로 legalEntityPublicId, operatingUnitPublicId, asOf, triggerVariant, overflowItems |
Core는 공통 UI와 파일 처리를 제공하고 App은 schema·권한·scope filtering·export rows·검증·업무 저장·retry를 정의합니다. Resource Transfer에서 export source와 import recipe/pipeline 연결 방법을 확인하세요.
데이터 마이그레이션
호스트가 조합하는 migration 항목은 dataMigrationRegistry로 등록합니다.
import { dataMigrationRegistry } from "@nexia/sdk/host";
dataMigrationRegistry.registerProvider(provider);
dataMigrationRegistry.registerTarget(target);
dataMigrationRegistry.registerStage({
...stage,
loader: () => import("./migration/OrdersStage"),
});App은 provider, product target, lazy App 소유 stage를 등록할 수 있지만 host registry를
검사하거나 조합할 수는 없습니다. Stage는 provider와 target, 선택적
requiredPermissions와 permissionMode, 선택적 prerequisiteStageIds를 선언하고
onCompleted({ runPublicId })로 서버가 소유한 migration run의 public id만
보고합니다. Callback은 GET /api/data-migrations/runs 재조회 힌트일 뿐이며 브라우저
상태는 완료 근거가 아닙니다. NxDatasetImportWorkspace를 쓰는 stage는 App backend가
ResourceImportPipelineDefinition에 DataMigrationStageIdentity를 선언합니다. Host는
서버 소유 identity를 queued execution에 붙여 실제 terminal import 결과를 기록하며,
브라우저는 이 identity를 선택하거나 위조할 수 없습니다.
Migration stage는 표준 accepted operation(ticket, status, 선택적 status_url)을
취소 가능하게 polling할 때 useDataMigrationBackgroundOperation()을 사용하고, 서버
metadata의 format으로 file picker 계약을 만들 때 dataMigrationUploadAccept(formats)을
사용합니다. createDataMigrationSessionCheckpointStore()는 참조 Resource를 만들기 위해
stage를 잠시 벗어날 때 쓰는 크기 제한 memory-only checkpoint이며, 영속 실행 근거가
아닙니다. 다른 App의 route를 import하거나 hard-code하지 말고 stable Resource Key로
resourceCreateDestinationRegistry에서 create route를 해석합니다. Boot 중 동일한 registry
선언의 반복은 허용하지만 충돌하는 duplicate는 즉시 실패합니다.
데이터 접근
| 심볼 | 목적 |
|---|---|
api | 설정된 HTTP 클라이언트. 테넌트와 Legal Entity 컨텍스트를 담고 있음 |
can | 프런트엔드 코드에서의 권한 확인 |
canForPopulation, canForAnyPopulation | 하나 또는 여러 SubjectPopulation 값에 대한 권한 확인 |
useContextQuery | 현재 테넌트와 사용자, Legal Entity, 워크스페이스 컨텍스트 |
usePermissionsQuery | tenant와 선택적 Legal Entity·Operating Unit 범위에서 행위자의 권한 |
useLegalEntitiesQuery, useOperatingUnitsQuery | 조직 조회 |
useApprovalRoutePoliciesQuery | App의 참조 picker를 위한 Core 소유 Approval 경로 정책 메타데이터 |
useOperatingUnitScope, useAssignableOperatingUnits, useAffiliatedOperatingUnits | Operating Unit 범위 지정 |
useParties | Party 조회 |
useAttachments, useUploadAttachment, useDeleteAttachment | 첨부 라이프사이클 |
useUploadFile | 전송 방식에 중립적인 호스트 발급 upload intent. 취소·재시도 가능한 UploadTask를 반환 |
useUploadMedia | 독립 Media 업로드. MediaItem mutation 결과를 반환 |
sendUserInvitation | 호스트 소유 로그인 초대. UserInvitationPayload를 받아 UserInvitationResponse를 반환 |
App 코드에서는 다음 컨텍스트 타입을 사용할 수 있습니다. TenantContext,
UserContext, LegalEntityContext, WorkspaceContext, PermissionsResponse입니다.
UserInvitationPayload는 이메일 또는 Person Party public id와 선택적 Legal
Entity·Operating Unit 역할 배정을 받습니다. 초대 요청의 검증과 인가는 계속
Core가 책임집니다.
usePermissionsQuery(tenantId, legalEntityPublicId, operatingUnitPublicId?, enabled?)는 Legal Entity 인자가 nullish이면 현재 context를 사용하고, 선택적
Operating Unit을 cache key와 요청에 모두 포함합니다. enabled가 true이고
tenant id가 있을 때만 실행합니다. 여전히 UI affordance용 snapshot이며,
backend는 보호된 요청마다 다시 인가합니다.
useApprovalRoutePoliciesQuery(tenantId, legalEntityPublicId, enabled?)는
활성화되어 있고 두 식별자가 모두 있을 때만 현재 context의 정책 catalog를
읽습니다. ApprovalRoutePoliciesQueryResult의 각 항목에는 key, version,
name, status, scope, legal_entity_public_id가 있습니다. App은 이
메타데이터를 참조 picker에 보여 주고 안정적인 정책 key를 저장할 수 있지만,
App 명령이 실행될 때 선택한 정책을 검증하는 책임은 계속 backend에 있습니다.
useUploadFile(): UploadFileClient는 저장소나 전송 정책을 App에 노출하지 않고
호스트가 발급한 upload intent를 시작합니다. uploadFile()에는 필수 purpose와
브라우저 File, 선택적 checksum, onProgress({ loaded, total, percent })를 담은
UploadFileInput을 전달합니다. 반환되는 UploadTask에는 result promise,
cancel(), retry()가 있습니다.
Promise는 UploadIntentResponse로 resolve되며 그 upload는 공개
UploadIntent projection입니다. 필드는 id, purpose, state, 선택적
transport(direct 또는 proxy), nullable expires_at, 선택적 opaque
media입니다. Direct object-storage 전송과 proxy 전송 중 무엇을 쓸지는 Core가
정합니다. App 코드는 transport를 선택하지 않고 호스트 전용 upload URL이나 storage
field에 의존하지 않습니다. 관련 공개 타입은 UploadFileInput, UploadProgress,
UploadIntent, UploadIntentResponse, UploadTask, UploadFileClient입니다.
can은 무엇을 렌더링할지 결정하고, 무엇이 허용되는지는 결코 결정하지 않습니다. 그것이 지키는 요청은 전부 백엔드에서 다시 승인됩니다. 프런트엔드 권한 확인은 UX 어포던스입니다.
Inspector와 작업 탭
| 심볼 | 목적 |
|---|---|
ResourceInspectorPanel, ResourceInspectorStackHost, ResourceInspectorStackProvider | Inspector 셸 |
resourceInspectorAdapterRegistry | Resource의 inspector 어댑터 등록 |
autoRegisterPackageResourceInspectors | App 엔트리에서 파일 시스템이 소유한 inspector 등록 집합 선언 |
ResourceInspectorAdapter, ResourceInspectorAdapterProps | 어댑터 타입 |
InspectorAction | 편집, 삭제, 수명 주기, App 소유 명령을 모두 표현하는 단일 권한 확인 Inspector 액션 계약 |
ResourceActionDescriptor, InspectorResourceActions | 행 메뉴와 Inspector 표현이 함께 소비하는 하나의 개방형 액션 집합 |
ResourceLink | 활성 Inspector stack에서 참조 Resource를 열고, stack 밖 route 이동과 작업 탭 메뉴를 지원하는 링크 |
useOpenResourceWorkTab | 작업 탭으로 레코드 열기 |
useMarkTabDirty, useWorkTabLabel, useRegisterTabActions | 작업 탭 상태와 동작 |
useActiveWorkTabId | 둘러싼 App surface를 소유한 작업 탭의 안정된 ID |
useIsActiveWorkTabVisible | 둘러싼 작업 탭이 현재 보이는 pane일 때만 true. 직접 만든 renderer의 지속 polling을 제한할 때 사용 |
useRegisterListLocateAction | 백엔드 locate 동작을 목록에 바인딩 |
한 작업 탭 안에서 route tree가 remount되어도 유지해야 하는 session-only draft의 key로
useActiveWorkTabId()를 사용합니다. 작업 탭 밖의 surface에서는 null입니다. 이 ID를
tenant, actor, 인가, 영속 browser-storage key로 사용하지 마세요.
ResourceInspectorStackHost가 보기와 닫기를 소유합니다. 어댑터는 직렬화된
백엔드 액션 결정이 허용할 때만 편집, 삭제, 활성화·비활성화를 추가하고 행 메뉴와
같은 route 또는 명령을 InspectorAction에 연결합니다. 삭제는 파괴적 표현을
유지하고 수명 주기 라벨은 현재 전이를 설명하며, Host가 표준 아이콘과 헤더
배치를 제공합니다. 안정적인 액션 id는 ResourceRowActionsMenu와 같은 관례를
따릅니다. 알려진 ID에는 표준 아이콘이 적용됩니다. App은 SDK나 Host를 수정하지
않고 리소스 전용 ID를 자유롭게 사용할 수 있으며, 명시적인 icon이 없으면
Host가 중립 폴백 아이콘을 표시합니다.
리소스 로컬 factory 하나가 ResourceActionDescriptor[]를 반환하게 하고, 그
결과를 행 메뉴와 InspectorResourceActions에 전달하세요. 액션 포함 여부, 순서,
라벨, 권한, 파괴적 상태, 처리 중 상태는 factory가 소유하고 각 호출자는 자신의
화면 맥락에 맞는 callback을 전달합니다. Inspector 표현은 Host가 이미 보기
액션을 소유하므로 view와 show를 제외합니다. 이것은 닫힌 액션 enum이 아니라
데이터 동기화 계약입니다. 한 리소스에서만 쓰는 ID도 SDK 변경 없이 유효합니다.
행 표현이 정말 없는 액션에만 단수 InspectorAction을 직접 사용하세요.
Primary Section과 Inspector host는 하나의 ResourceInspectorStackProvider로
감싸세요. Provider의 root가 null이어도 ResourceInspectorStackHost는 계속
마운트해야 합니다. 그래야 Primary Section의 ResourceLink가 첫 stack entry를
만들 수 있습니다. 클릭 가능한 표 행에서는 행 배경과 행 자체의 대표값이 그 행의
Resource를 선택하고, 다른 참조값은 onActivate를 덮어쓰지 않은
ResourceLink로 참조 Resource를 push합니다. More 메뉴, 링크, 버튼, 폼
컨트롤을 누를 때는 행까지 함께 활성화되지 않습니다. resource.route가 내부
경로라면 ResourceLink를 우클릭했을 때도 새 작업 탭에서 열기를 제공합니다.
ResourceLink의 대상으로 노출되는 Inspector adapter는 data가 없는 id-only
ResourceRef도 처리해야 합니다. resource.data가 있으면 즉시 사용하되, 없으면
resource.id로 Resource detail을 조회하세요. 그래야 다른 Resource에서
drill-in했을 때 비어 있는 Inspector가 표시되지 않습니다.
Dashboard renderer 훅
Dashboard renderer는 별도의 viewport 또는 transport 정책을 만들지 말고 board의 공용 lifecycle을 따라야 합니다.
| 심볼 | 목적 |
|---|---|
useAgentToolDataFreshness({ enabled, refreshMs, preview? }) | TanStack Query의 refetchInterval, refetchOnWindowFocus, staleTime을 반환. 호스트가 polling을 최소 10분으로 제한하고 더 느린 hint는 존중하며 parked tab·preview·비활성 query에서는 멈춤 |
useDashboardWidgetAutoHeight(widgetId) | widget scroll container에 붙일 ref를 반환. board 안에서는 새 widget이 자리 잡도록 자연 콘텐츠 높이를 보고하고 board 밖에서는 아무 동작도 하지 않음 |
useAgentToolDataInvalidation(enabled?) | 호환용 no-op. tool-data 무효화는 Shell 전체 Reverb ingress가 소유하므로 widget이 transport subscription을 열지 않음 |
freshness 결과를 useQuery에 펼치세요. spec의 queryRef.refresh는 요청 cadence이지 호스트 정책보다 빠르게 polling할 권한이 아닙니다.
오류와 메시지, 서식
| 심볼 | 목적 |
|---|---|
parseMutationFormError, firstFieldError, withoutFieldError | API 검증 오류를 필드에 매핑 |
MutationFormError, FormFieldErrors | 오류 타입 |
useMessage, MessageApi, ToastOptions | 토스트와 메시지 |
useFeedbackAction, FeedbackUndoOptions | 선택적인 1회 server-backed 역작업을 포함하는 표준 operation feedback |
useDateTimeFormatter | 로케일·타임존이 올바른 서식. fallback 옵션을 받고 locale과 timezone을 반환 |
formatNumber, NumberLike | 로케일 인식 숫자 서식. 없거나 잘못된 값은 —로 표시 |
useDebouncedValue | 디바운스된 입력 |
NxStatCard, NxLineChart, NxBarChart, NxDonutChart | 지표와 일반 비교 표시 |
NxHeatmap, NxGauge, NxSankey | 행렬 강도, 목표 진행률, 흐름 분석. App은 ECharts option이 아니라 의미 데이터만 전달 |
날짜는 직접 포맷하지 말고 useDateTimeFormatter를 쓰세요. 테넌트의 업무 타임존을 적용해줍니다.
요약에서 표준 경로를 열어야 한다면 NxStatCard 또는 NxOverviewBand 카드에
href를 설정합니다. 정적 요약이면 생략합니다. 기존 카드의 onOpen callback은
제거되었으며, 경로 카드는 네이티브 링크와 Shell 작업 탭 동작을 유지합니다.
되돌릴 수 있는 mutation에는 useFeedbackAction의 undo에 번역된 label, 역작업,
성공·실패 feedback을 전달합니다. 역작업은 원래 성공 결과와 argument tuple을 받고 한
번만 실행됩니다. Custom success Toast action이 있으면 단일 action slot을 유지합니다.
클라이언트 화면만 되돌리는 동작을 Undo로 표시하지 마세요.
Approval, Signature, 에이전트
| 심볼 | 목적 |
|---|---|
approvalComposerBusinessFormRegistry | Approval 업무 폼 위젯 등록 |
ApprovalBusinessFormSlotPropsV2 | slot API 버전 2에서 composer 위젯이 받는 props |
ApprovalBusinessFormController, ApprovalBusinessFormSubmitEnvelope | 제출 계약 |
ApprovalCase, ApprovalDocument, ApprovalStep, ApprovalState | Approval 읽기 모델 |
ApprovalResourceSummary, ApprovalResourceSummaryField | Approval case에 붙는 권한 검사 Resource summary projection |
SignatureRequestPreparationEditor, SignatureRequestPreparationExactPreview | Core 소유 요청 로컬 편집과 보호된 exact-PDF 검토 화면 |
SignatureRequestPreparationPlacementEditor, SignatureSingleRequestDialog | placement-only 편집과 App 중립 단일 요청 흐름 |
registerAgentComponent | 에이전트가 렌더링할 수 있는 컴포넌트 등록 |
registerLazyAgentComponent | 같은 renderer를 동적 import() loader 뒤에 등록 |
useAgentToolDataInvalidation | 기존 renderer 호환 유지. 실제 무효화는 Shell 전체 Reverb ingress가 수행 |
ApprovalBusinessFormSlotPropsV2가 SlotWidgetDescriptor의 slotApiVersion: 2가 지목하는 prop 계약입니다. 이 props만 소비하세요.
ApprovalResourceSummaryField는 의미상 subtitle, meta, status 중 하나를
role로 실을 수 있습니다. Compact presentation 힌트로만 사용하고 field 순서와
layout은 호스트 소유로 둡니다.
테스트
테스트 헬퍼는 @nexia/sdk/testing에서 가져옵니다. 호스트 설정 방법은 App 테스트하기를 따르세요.
| 심볼 | 목적 |
|---|---|
configureNexiaAppFrontendTestHost | 테스트 호스트 바인딩 |
appTestServer | 요청 모킹 |
appTestI18n | 테스트용 로케일 카탈로그 |
NexiaAppFrontendTestHostBindings | 바인딩 타입 |
configureNexiaAppFrontendHost와 NexiaAppFrontendHostBindings가 런타임 대응물이고, 호스트가 공급합니다.
오류
| 증상 | 원인 | 해결 |
|---|---|---|
Module not found: @/..., @core/..., @shell/... | App이 플랫폼 별칭을 import함 | @nexia/sdk에서 import. coupling ratchet이 새 플랫폼 참조를 거부합니다 |
| 화면 Route가 빈 화면을 렌더링 | pageElements()가 게시한 컴포넌트 이름에 등록이 없음 | 파생 경로 아래에 화면을 두거나 overrides 항목 추가 |
Invalid hook call 또는 컨텍스트 오류 | App 번들에 React나 React Query 중복 | 그 라이브러리를 peerDependencies로 옮기기 |
SDK에서 appComponentRegistry를 import할 수 없음 | SDK export가 아니라 @shell/ 뒤의 호스트 심볼임 | autoRegisterPackageSurfaces 쓰기 |
| 좁은 작업 탭에서도 2열 화면이 눌린 채 유지됨 | md: 또는 lg: 클래스가 브라우저 뷰포트를 측정함 | 직접 만든 레이아웃은 NxResponsiveRegion으로 감싸고, 주 콘텐츠와 보조 영역 구조라면 NxAdaptiveSplit 사용 |
| App 컴포넌트가 렌더링 중 예외를 던짐 | Route 경계가 App 실패를 격리함 | 콘솔 오류를 읽어 컴포넌트를 고치거나 화면의 재시도 동작 사용 |
App에 적용
autoRegisterPackageSurfaces로 진입점을 등록하고 호스트에서 경로를 여세요. 화면 작성은 App 화면 만들기, 테스트 환경은 App 테스트하기에서 이어집니다.
관련 문서
- SDK 계약 — 같은 경계의 PHP 쪽
- App 화면 만들기 — 이 프리미티브로 화면 작성
- Slot Widget 추가 — composer 위젯이 필요한 descriptor 선언
- 경계 위반 해결 — 거부된 import 고치기