본문으로 건너뛰기
참조

전자 결재 계약

전자 결재 업무 양식으로 나타나기 위해 App이 함께 배포해야 하는 바인딩과 문서 스키마, Slot Widget입니다.

전자 결재 계약

App 동작을 결재 작성기에 표시하려면 바인딩과 문서 스키마를 게시하고 formWidgetKey에 해당하는 widget을 등록하세요. 바인딩·스키마·widget의 식별자가 맞아야 합니다. 아래 선언부터 적용하고 제출 권한과 고정 증빙 계약을 이어서 확인하세요.

시그니처

Descriptor contribution 하나이고, 보통 클래스 하나에 둡니다.

코드 예시
PHP
use Nexia\AppDescriptors\Contracts\AppDescriptorContribution;
use Nexia\AppDescriptors\AppDescriptorSet;
use Nexia\AppDescriptors\ApprovalDocumentSchema;
use Nexia\AppDescriptors\ApprovalFormBindingDescriptor;

바인딩의 정체성은 넘기는 것이 아니라 파생됩니다.

{appKey}.{resourceKey}.{actionKey}   →   quality.inspection_decision.submit

최소 예시

설명용 Quality key를 사용하는 생성자 유효 선언 예시입니다. 해당 Quality 업무 Resource가 체크아웃에 있다는 전제는 아닙니다. 실제로 등록한 Resource와 아래 런타임 연동 계약을 함께 사용하세요.

코드 예시
PHP
<?php

declare(strict_types=1);

namespace Amuzcorp\Nexia\Quality\Descriptors;

use Nexia\AppDescriptors\Contracts\AppDescriptorContribution;
use Nexia\AppDescriptors\AppDescriptorSet;
use Nexia\AppDescriptors\ApprovalDocumentSchema;
use Nexia\AppDescriptors\ApprovalFormBindingDescriptor;

final class QualityApprovalDescriptors implements AppDescriptorContribution
{
    public static function appDescriptors(): AppDescriptorSet
    {
        return AppDescriptorSet::of(
            new ApprovalFormBindingDescriptor(
                appKey: 'quality',
                resourceKey: 'inspection_decision',
                actionKey: 'submit',
                labelKey: 'quality.approval.inspection_decision.submit.label',
                entryModes: ['link'],
                formWidgetKey: 'quality.inspection_decision.submit',
                documentSchemaKey: 'quality.inspection_decision',
                submitPermissionKey: 'quality.inspection_decision.submit',
            ),
            new ApprovalDocumentSchema(
                appKey: 'quality',
                resourceKey: 'inspection_decision',
                titleKey: 'quality.approval.inspection_decision.document.title',
                sections: [[
                    'title_key' => 'quality.approval.inspection_decision.document.summary',
                    'fields' => [
                        ['key' => 'proposed_decision', 'label_key' => 'quality.inspection_decision.proposed_decision.label'],
                        ['key' => 'reason_text', 'label_key' => 'quality.inspection_decision.reason_text.label'],
                    ],
                ]],
            ),
        );
    }
}

typed 바인딩과 스키마는 별도 값으로 유지되고, 발견만 공통 set으로 옮겼습니다. Approval business-template과 route-policy preset도 같은 게시 경계를 씁니다.

인자: ApprovalFormBindingDescriptor

인자타입기본값동작
appKeystring—소유 App, 비어 있으면 안 됨
resourceKeystring—App 안의 Resource, 비어 있으면 안 됨
actionKeystring—승인 대상 동작, 비어 있으면 안 됨
labelKeystring—composer의 라벨, 비어 있으면 안 됨
entryModesstring 목록—create, link 중 최소 하나
formWidgetKeystring—폼을 렌더링하는 Slot Widget 키, 비어 있으면 안 됨
documentSchemaKeystring—제출되는 문서 스키마, 비어 있으면 안 됨
versionstring1.0Descriptor 개정
statusDescriptorStatusActiveActive, Deprecated, Removed
descriptionKeystring, nullnull더 긴 설명
approvedLabelKeystring, nullnull승인됐을 때 결과 라벨
rejectedLabelKeystring, nullnull반려됐을 때 결과 라벨
recalledLabelKeystring, nullnull회수됐을 때 결과 라벨
cancelledLabelKeystring, nullnull취소됐을 때 결과 라벨
submitPermissionKeystring, nullnull제출에 필요한 권한

ApprovalFormBindingDescriptor::ENTRY_MODES는 ['create', 'link']입니다.

모드레코드가쓰는 경우
create제출의 일부로 생성됨승인이 레코드가 존재하게 되는 방식일 때
link이미 존재하고 건에 붙음레코드를 먼저 작성한 뒤 제출할 때

두 흐름을 모두 지원한다면 둘 다 선언하세요. 제출 전에 작성하는 레코드는 ['link']를 사용합니다.

검증

필수 필드마다 생성자에서 검증하고, 메시지에 파생된 id가 담깁니다.

ApprovalFormBindingDescriptor app_key must be a non-empty string.
ApprovalFormBindingDescriptor [quality] resource_key must be a non-empty string.
ApprovalFormBindingDescriptor [quality.inspection_decision] action_key must be a non-empty string.
ApprovalFormBindingDescriptor [quality.inspection_decision.submit] label_key must be a non-empty string.
ApprovalFormBindingDescriptor [quality.inspection_decision.submit] must declare at least one entry mode.
ApprovalFormBindingDescriptor [quality.inspection_decision.submit] entry mode [attach] must be one of: create, link
ApprovalFormBindingDescriptor [quality.inspection_decision.submit] form_widget_key must be a non-empty string.
ApprovalFormBindingDescriptor [quality.inspection_decision.submit] document_schema_key must be a non-empty string.

ApprovalDocumentSchema

인자타입기본값동작
appKeystring—소유 App
resourceKeystring—문서가 서술하는 Resource
sectionsarray—순서 있는 문서 섹션
versionstring1.0스키마 개정
statusDescriptorStatusActive라이프사이클
titleKeystring, nullnull문서 제목 키

각 섹션은 제목 키와 필드 목록을 담은 배열입니다.

코드 예시
PHP
[
    'title_key' => 'quality.approval.inspection_decision.document.summary',
    'fields' => [
        ['key' => 'decision_revision', 'label_key' => 'quality.inspection_decision.decision_revision.label'],
    ],
]

스키마는 App이 저장하는 것이 아니라 승인자가 읽는 것을 선언합니다. 판단에 필요한 필드를 넣고 내부 장부는 빼세요.

세 조각이 맞아야 한다

바인딩은 자기완결적이지 않습니다. 산출물 셋이 키로 이어지고, 하나만 어긋나도 composer가 제시할 수는 있지만 렌더링하지 못하는 바인딩이 됩니다.

산출물키일치해야 할 대상
ApprovalFormBindingDescriptorformWidgetKey어떤 SlotWidgetDescriptor.key
SlotWidgetDescriptorslotapproval.composer.business_form
SlotWidgetDescriptorcomponent등록된 프런트엔드 컴포넌트
ApprovalFormBindingDescriptordocumentSchemaKey어떤 스키마의 {appKey}.{resourceKey}

바인딩과 widget key를 동일한 Resource·동작 식별자에서 만드세요.

같은 AppDescriptorSet에 아래 값을 추가하세요. SlotWidgetDescriptor import는 contributor 파일 상단에 둡니다.

코드 예시
PHP
use Nexia\AppDescriptors\SlotWidgetDescriptor;

new SlotWidgetDescriptor(
    key: 'quality.inspection_decision.submit',
    version: '1.0',
    slot: 'approval.composer.business_form',
    component: 'quality.inspection_decision.submit',
    slotApiVersion: 2,
    permission: 'quality.inspection_decision.submit',
);

slotApiVersion: 2는 컴포넌트가 @nexia/sdk의 ApprovalBusinessFormSlotPropsV2를 소비한다는 뜻입니다.

권한

바인딩은 요청을 라우팅합니다. 쓰기를 승인하지 않습니다.

플랫폼의 Approval이 하는 일여러분 App이 해야 하는 일
건을 결재선에 따라 라우팅제출된 페이로드 검증
결과와 감사 흔적 기록최종 레코드 동작에서 Policy 강제
여러분 위젯과 문서 렌더링행위자가 수행할 수 없는 동작 거부

승인된 건은 App 판단의 입력이지 판단의 대체물이 아닙니다. submitPermissionKey는 제출을 통제하고 이후의 쓰기는 통제하지 않습니다.

App 안에 병행 승인 장치를 만들지 마세요. 전자결재를 거치는 Resource는 플랫폼 Approval에 업무 양식으로 이어집니다.

결과 또는 반환: 고정된 바인딩 증거

BoundApprovalSubmission을 만들어 ApprovalHost::submitBound()로 제출합니다. 어긋나면 안 되는 증거가 명시됩니다.

지원 파일은 전체 흐름에서 하나의 list 계약을 사용합니다. prepareSupportingAttachments()를 호출해 list<ApprovalAttachmentEvidence>를 얻고 그 list를 submitBound()의 두 번째 인자로 바로 넘기세요. ApprovalAttachmentEvidence는 Media UUID, checksum, scan verdict, 선택 scan 시각을 담습니다. prepared attachments wrapper는 없습니다.

필드 묶음고정되는 증거
TemplatetemplateKey, templateVersion
BindingbindingKey, bindingVersion
Resource정규 ResourceRef, resourceVersion
판단documentValues, 해석된 lineDefinition, 제목과 선택 라벨
재생선택 idempotencyKey, idempotencyFingerprint

호스트는 case를 만들기 전에 선택된 template과 binding 버전, 정규 Resource 정체성, 해석된 결재선을 검증합니다. 이후 ApprovalHost::caseSummary()는 고정된 binding App·Resource·동작 필드와 Resource 버전이 든 ApprovalCaseSummary를 반환합니다. Case가 없거나 예전 비정규 Resource reference라면 null을 반환하므로, 그 부재에서 부분 reference를 만들어내지 마세요.

테넌트 작성 Process가 현재 실제 설정을 선택하는 경우에는 submitCurrentBound(CurrentBoundApprovalSubmission, $attachments)를 사용하세요. 호스트가 현재 게시된 template, 활성 route policy, 설치된 binding을 해석해 원자적으로 snapshot합니다. CurrentBoundApprovalResult는 case, route, 고정된 정확한 template·binding 버전을 반환합니다.

이 경로가 자동 승인 정책을 opt-in하는 제출 경로입니다. submitBound()는 기존의 사람 전자 결재 의미를 유지하며 자동 승인 정책 사실을 평가하지 않습니다.

선택적 정책 자동승인 사실

자동승인은 바인딩 하나에 적용하는 선택적 Core 정책입니다. App은 approved boolean을 보내거나 사용자 결재 동작·서명을 만들지 않습니다. 바인딩이 이를 지원할 때만 provider를 등록하세요.

코드 예시
PHP
use Nexia\Approval\Contracts\AutomaticApprovalFactProviderRegistrar;

/** @var AutomaticApprovalFactProviderRegistrar $registrar */
$registrar->register('quality.inspection_decision.submit', fn () => new QualityApprovalFacts($records));

evaluate()는 Core가 제출 transaction을 유지하는 동안 실행됩니다. ResourceRef가 가리키는 현재 App 레코드를 잠그고 다시 읽은 뒤 AutomaticApprovalEvaluation(resourceRef, resourceVersion, documentFingerprint, facts)를 반환해야 합니다. fingerprint는 원시 값이나 정규화하지 않은 JSON이 아니라 렌더된 snapshot으로 계산합니다.

코드 예시
PHP
use Nexia\Support\CanonicalPayloadFingerprint;

$snapshot = $approvalHost->renderDocument('quality.inspection_decision', $documentValues)?->toArray();
$documentFingerprint = CanonicalPayloadFingerprint::sha256($snapshot);

facts에는 저장된 scalar bool, int, string만 넣고 금액은 정수 최소 단위 또는 canonical decimal string을 사용합니다. ResourceRef·resource version·정규화한 렌더 문서 fingerprint가 달라지면 Core가 거절하고, 활성 정책만 평가해 정책·버전·사실·시각·결과의 불변 증거를 남깁니다. 법인에 활성 정책이 있으면 같은 바인딩의 법인 정책이 우선이며, 법인 정책이 없을 때만 tenant catalog 정책을 사용합니다. approval.automatic_policy.*는 법인 정책을, approval.automatic_policy_catalog.*는 tenant catalog 정책을 관리하는 별도 권한입니다.

같은 bound 요청을 재시도할 때는 안정적인 idempotency key를 사용하세요. Host는 키를 기안자와 첨부 파일을 포함한 불변 요청 payload에 묶습니다. 24시간 idempotency 보존 기간 안의 일치하는 재시도는 정책이 retire된 뒤에도 처음 고정한 case·route·template·binding 결과를 반환합니다. 무기한 재실행을 보장하지는 않습니다. payload가 달라지거나 다른 기안자가 같은 키를 쓰면 거절됩니다. 사실이 없거나 일치하지 않거나 provider를 쓸 수 없거나 강한 서명 template이면 기존 사람 결재 경로가 유지됩니다. 소비자는 outcome_source (human 또는 policy), policy_key, policy_version, document_fingerprint를 구분해 검증할 수 있으며 정책 결과를 사람 서명으로 다루면 안 됩니다.

바인딩과 Template

자주 혼동되지만 서로 바꿔 쓸 수 없습니다.

Approval 바인딩Approval Form Template
사는 곳여러분 패키지 소스테넌트 데이터
소유자여러분 App관리자
목적App과 Approval 런타임의 연결저장되고 선택 가능한 업무 양식
바뀌는 시점릴리스를 배포할 때관리자가 편집할 때

Template은 게시될 때 바인딩을 선택할 수 있습니다. 바인딩은 플랫폼이 제출 시점에 해석하는, 설치된 App 기능으로 남습니다.

오류

증상원인해결
바인딩 id가 담긴 부팅 시 InvalidArgumentException생성자 검증메시지를 읽으세요. 누락된 필드를 알려줍니다
composer에서 바인딩이 제시되지 않음발견, 설치, status 관문nexia-apps:doctor-package-app 실행 후 테넌트 설치와 status 확인
바인딩은 제시되는데 composer가 아무것도 렌더링하지 않음formWidgetKey에 맞는 Slot Widget이 없거나 컴포넌트가 등록되지 않음키를 맞춘 뒤 composer를 직접 열어 확인. doctor는 pageElements()만 순회하고 위젯 component는 검사하지 않습니다
문서가 비었거나 라벨이 잘못됨documentSchemaKey가 기여된 스키마와 안 맞음{appKey}.{resourceKey} 맞추기
승인자에게 i18n 키 원문이 보임로케일 항목 누락resources/lang/{locale}.json에 키 추가

App에 적용

App에서는 바인딩과 문서 스키마를 게시하고 동일한 key의 composer widget을 등록한 뒤 호스트 기능으로 제출합니다. 위 Quality resource/action 이름은 이 식별자 연결을 보여 주는 예시입니다. 과거 Quality Descriptor 파일을 전제로 작업하지 마세요.

업무별 결재 설정

직접 처리와 결재 상신 경로가 모두 ApprovalRequirements::required(bindingKey, legalEntityKey, default, contextKey)를 적용한 바인딩만 supportsRequirementPolicy를 선언합니다. Core의 전자결재 → 관리 → 업무별 결재 설정에서 해당 업무를 설정합니다. App 설정은 /workbench/approvals/operation-requirements?app=<app-key>로 연결하고, 별도의 결재 필수 입력란을 만들지 않습니다.

기존 contributor에 ApprovalRequirementContextProvider를 구현하면 조건별로 안정적인 키·표시명·기존 App 기본값을 담은 ApprovalRequirementContext를 제공할 수 있습니다. Core가 바인딩의 contribution에서 발견합니다. App은 신뢰할 수 있는 업무 데이터로 조건을 판단하고 상신 시 requirementContextKey로 전달합니다. 우선순위는 법인 조건 → 법인 공통 → 기업 조건 → 기업 공통 → App 기본값입니다. 실행 시 결정한 값을 증빙에 저장하고, 설정 변경으로 진행 중인 업무를 바꾸지 않습니다.

submitPermissionKey는 양식 목록의 사용 가능 여부를 판단합니다. 위임된 업무 권한은 additionalSubmitPermissionKeys로 추가할 수 있습니다. 선택한 범위에서 하나 이상의 권한이 있어야 하며, 실제 App endpoint는 대상 레코드와 동작의 권한을 다시 검사합니다. 양식이 보인다는 사실만으로 리소스 수정 권한이 생기지 않습니다.

게시된 양식은 route_policy_key를 가질 수 있으며 변경하려면 새 양식 버전이 필요합니다. 기업 공통 양식은 법인 전용 결재선을 선택할 수 없고, 같은 키의 법인 결재선이 공통 결재선을 대신하지 않습니다. CurrentBoundApprovalSubmission에 경로 없이 수동 lineDefinition을 전달할 수 있어 결과의 route는 null일 수 있습니다. 명시적으로 선택한 경로가 사라진 경우에는 오류로 처리합니다.

Process에서 사람이 결재선을 선택해야 하면 액션에 supportsLinePreparation을 선언하고 ProcessWorkActionInvocation.approvalLine을 사용합니다. Core는 대기 중인 동일 token에 준비 User Task를 만듭니다. 준비 완료는 결재선을 검증하고 같은 업무를 재시도하며, 승인이나 다음 단계 진행을 의미하지 않습니다. App은 기존 업무 명령과 내구성 있는 callback으로 상신과 결과 반영을 처리합니다.

공통 데이터의 업무는 requirementPolicyLegalEntityScoped: false를 선언하고 결재 필요 여부 조회에 법인 키를 null로 전달합니다. 설정은 기업 공통 범위에만 표시됩니다. 특정 조건만 변경할 수 있는 업무는 requirementPolicyContextsOnly: true를 사용합니다. Core는 선언된 조건만 제공하고 업무 전체의 일괄 우회를 거부합니다. 이 속성들은 기존 업무 범위를 보존하기 위한 것이며 모든 업무의 결재를 선택 사항으로 만들지 않습니다.

업무 생성이나 Process 실행 결과를 일반 HTTP 재시도 기간인 24시간 이후에도 유지하려면 ApprovalIdempotencyKey(durable: true)를 사용합니다. 재시도에서는 동일한 실행자·대상·입력의 불변 지문을 전달합니다. 일반 키의 만료 정책은 유지되고, 영구 키는 중복 실행을 막는 업무 증거로 보관됩니다.

관련 문서

Host는 Process 결재선 준비에 사용할 실제 바인딩 대상을 ApprovalException.preparationResourceRef에 보존합니다. 이 내부 참조는 공개 오류 응답에 포함하지 않으며, 하위 문서를 상신해도 Process의 최초 대상은 유지됩니다.

원본 위치: docs/developers/content/ko/app-sdk/approval-contracts.md