예제
전자 결재에 기여하기
Workshop Note binding·업무 양식 widget·App 제출 endpoint를 결재 작성 화면에 연결합니다.
예제 유형 레시피
개요
전자 결재에 기여하기 예제
이 예제는 지금까지 만든 Workshop Note를 실제 전자결재 작성 흐름에 연결합니다.
앞의 Descriptor 예제가 관리 화면에 노트 제출 선택지를
게시했다면, 여기서는 Slot Widget, frontend controller, App 소유 제출 endpoint를
같은 identity로 묶습니다.
이 작업이 끝나면
| 기여물 | 화면 또는 기능 |
|---|---|
WorkshopApprovalSlotWidgets | Core 결재 작성기의 업무 양식 slot에 Workshop Widget을 연결 |
| frontend registry와 Widget | 기존 Note 선택, 이름·상태 미리 보기, 제출 가능 여부 제공 |
| App 제출 endpoint | 현재 actor와 Note를 재인가한 뒤 Core Approval case 생성 |
| 게시된 Legal Entity 양식 | 전자결재 → 전자결재 작성에 Workshop 업무 양식 표시 |

위 캡처는 Descriptor catalog에서 노트 제출을 골라 업무 양식에 연결한 실제
화면입니다. 선택하면 기존 노트를 전자결재에 연결합니다.라는 App 설명이
표시됩니다. 이 양식을 생성·게시한 뒤에만 사용자용 전자결재 작성 목록에
나타납니다.
세 계약이 Core의 작성 흐름에 연결되는 방식
이 예제는 하나의 클래스를 상속해 완성되는 기능이 아니라, 다음 세 경계를 같은
workshop.note.submit identity로 연결합니다.
| 경계 | Workshop이 구현·등록하는 것 | Core가 처리하는 것 |
|---|---|---|
| 정적 화면 계약 | WorkshopApprovalSlotWidgets implements AppDescriptorContribution | AppDescriptorCatalog와 SlotWidgetCompositionResolver가 설치 상태, slot API version, lifecycle, 권한을 검사 |
| 브라우저 계약 | approvalComposerBusinessFormRegistry.register(...)와 ApprovalBusinessFormSlotPropsV2 controller | 결재 작성기가 component key로 Widget을 불러오고 공용 결재선·draft·preview·submit lifecycle을 호스팅 |
| 제출 계약 | App endpoint가 주입받은 Nexia\Approval\Contracts\ApprovalHost | Core의 CoreApprovalHost implements ApprovalHost가 binding·template을 확인하고 결재 case와 감사 증거를 생성 |
게시된 Legal Entity 양식
→ Core 작성기가 binding의 formWidgetKey를 읽음
→ SlotWidgetCompositionResolver가 허용된 Slot descriptor를 선택
→ frontend registry에서 같은 key의 Workshop Widget을 로드
→ Widget이 App 제출 endpoint를 호출
→ App이 Note·Policy·revision을 재검사
→ ApprovalHost::submitBound()
→ CoreApprovalHost가 Core 결재 case를 생성
App endpoint는 CoreApprovalHost를 직접 import하거나 상속하지 않습니다. 생성자에서
SDK의 ApprovalHost 인터페이스를 주입받고, Core host가 그 인터페이스를
CoreApprovalHost 구현에 bind합니다. 이 경계 덕분에 Core는 Workshop 모델을 모르고,
Workshop도 Core Eloquent 모델을 사용하지 않습니다.
1. Slot Widget Descriptor 추가
예제 파일을 Workshop의 descriptor discovery root에 복사합니다.
cp docs/developers/examples/approval-contribution/WorkshopApprovalSlotWidgets.php \
packages/workshop/src/Descriptors/WorkshopApprovalSlotWidgets.php핵심 identity는 네 곳에서 완전히 같아야 합니다.
Approval binding formWidgetKey
= Slot descriptor key
= Slot descriptor component
= frontend registry key
= workshop.note.submit
Slot descriptor는 Core가 소유한 approval.composer.business_form slot을 사용하고,
앞에서 만든 workshop.note.publish 권한으로 가시성을 제한합니다.
2. frontend Widget 등록
packages/workshop/resources/js/index.ts에 공개 App SDK registry 등록을 추가합니다.
import { approvalComposerBusinessFormRegistry } from '@nexia/sdk/host';
approvalComposerBusinessFormRegistry.register(
'workshop.note.submit',
() => import('./approval/WorkshopNoteApprovalWidget'),
);WorkshopNoteApprovalWidget은 ApprovalBusinessFormSlotPropsV2를 받고 Note를 읽은
뒤 composer controller를 등록합니다.
registerController({
canSubmit: note !== null && note.status === 'active',
saveDraft: async () => ({ appDraftRef: note.public_id }),
buildPreview: () => ({
title: note.name,
sections: [{
title: t('workshop.approval.note.document.summary'),
fields: [
{ key: 'name', label: t('workshop.note.name.label'), value: note.name },
{ key: 'status', label: t('workshop.note.status.label'), value: note.status },
],
}],
}),
submit: async (envelope) => {
const response = await api.post(
`/legal-entities/${legalEntityPublicId}/workshop/notes/${note.public_id}/submit-approval`,
{
template_key: envelope.templateKey,
template_version: envelope.templateVersion,
line: envelope.line,
reference_user_ids: envelope.referenceUserIds,
circulation_user_ids: envelope.circulationUserIds,
},
);
return { caseId: response.data.approval_case.public_id };
},
});위 코드는 controller 연결 구조를 보여주는 축약본입니다. 실제 Widget은 loading, not-found, 권한 거부, idempotency key, 첨부 staging, 오류 상태까지 처리해야 합니다. 브라우저는 Core endpoint가 아니라 App endpoint를 호출합니다.
3. App에서 재인가하고 제출
App endpoint는 요청의 Note를 Legal Entity 범위에서 다시 찾고
workshop.note.publish Policy를 검사합니다. 현재 상태와 revision도 잠근 상태에서
확인한 뒤 App SDK의 ApprovalHost를 호출합니다.
$case = $this->approvals->submitBound(
new BoundApprovalSubmission(
legalEntity: $legalEntity,
drafter: $actor,
templateKey: $input['template_key'],
templateVersion: (int) $input['template_version'],
bindingKey: 'workshop.note.submit',
bindingVersion: '1.0',
resourceRef: new ResourceRef(
appKey: 'workshop',
resourceKey: 'workshop.note',
resourceId: (string) $note->public_id,
display: $note->name,
href: '/apps/workshop/notes/'.$note->public_id,
),
resourceVersion: (string) $note->updated_at->getTimestamp(),
documentValues: [
'name' => $note->name,
'status' => $note->status->value,
],
lineDefinition: $line,
title: $note->name,
),
);App은 Note와 Policy를 소유하고, Core는 결재선·case·감사 증거를 소유합니다. App이 Core Eloquent model을 import하거나 Core가 Workshop model을 알아서는 안 됩니다.
4. 양식을 게시하고 화면 확인
- 전자결재 → 양식 관리 → 새 전자결재 양식에서 업무 양식을 고릅니다.
- 도메인 양식 → Workshop → 노트 제출을 선택합니다.
- 이름, Legal Entity 범위, 결재 규칙을 입력해 양식을 생성하고 게시합니다.
- 전자결재 → 전자결재 작성에서 게시한 Workshop 양식을 엽니다.
- 기존 Note를 연결했을 때 이름·상태와 공용 결재선이 함께 보이는지 확인합니다.
- 제출 뒤 생성된 결재 case로 이동하는지 확인합니다.
binding만 추가했을 때 전자결재 작성에 보이지 않는 것은 정상입니다. 그 목록은 active descriptor 전체가 아니라 게시된 Legal Entity 양식을 보여줍니다.
5. 발견 확인
sh docs/developers/examples/approval-contribution/verify.sh스크립트가 Workshop Approval contribution discovered and validated.를 출력하면
정적 contribution 검증이 끝난 것입니다.
자주 발생하는 오류
| 증상 | 원인 | 해결 | 다시 확인 |
|---|---|---|---|
| 도메인 양식에 노트 제출이 없음 | Workshop 미설치, submit 권한 미배정, binding lifecycle 또는 locale 문제 | 같은 tenant의 설치 상태, workshop.note.publish, Active binding과 locale key 확인 | 도메인 양식에서 Workshop → 노트 제출을 선택할 수 있음 |
| 도메인 양식에는 있지만 작성 목록에 없음 | binding만 게시했고 Legal Entity 양식을 생성·게시하지 않음 | 해당 Legal Entity에서 Workshop 도메인 양식을 생성하고 게시 | 전자결재 작성 목록에 게시한 양식이 표시됨 |
| 작성 화면에서 Widget을 찾지 못함 | binding, Slot descriptor와 frontend registry의 component identity가 다름 | 네 곳을 모두 workshop.note.submit으로 맞추고 frontend 갱신 | 작성 화면에서 기존 Note 선택 Widget이 로드됨 |
| 제출 시 거부됨 | App Route guard, Note Policy, revision 또는 Access Grant 범위 검사 실패 | 현재 actor와 Legal Entity에서 workshop.note.publish와 Note 상태·revision을 다시 확인 | 제출 후 생성된 Approval case로 이동 |
설정별 동작까지 확인하기
이 Workshop 예제는 최소 기여 연결을 검증합니다. 결재 필수 정책이나 기본 절차 설치를 자동으로 추가하지는 않습니다. 전자 결재에서 개발자의 정책 적용 조건과 관리자의 양식·결재선·필수 설정 순서를 확인한 뒤, 업무 프로세스의 실제 구매 흐름을 따라가세요. 자동 입력과 사람 검토, 결재 필수·불필요, 결재선 준비, 결과 분기, 기본안 업데이트를 하나의 흐름에서 비교할 수 있습니다.
포함된 파일
전자결재 Slot Widget Descriptor
docs/developers/examples/approval-contribution/WorkshopApprovalSlotWidgets.php<?php
declare(strict_types=1);
namespace Amuzcorp\Nexia\Workshop\Descriptors;
use Nexia\AppDescriptors\AppDescriptorSet;
use Nexia\AppDescriptors\Contracts\AppDescriptorContribution;
use Nexia\AppDescriptors\DescriptorStatus;
use Nexia\AppDescriptors\SlotWidgetDescriptor;
final class WorkshopApprovalSlotWidgets implements AppDescriptorContribution
{
public static function appDescriptors(): AppDescriptorSet
{
return AppDescriptorSet::of(
new SlotWidgetDescriptor(
key: 'workshop.note.submit',
version: '1.0',
slot: 'approval.composer.business_form',
component: 'workshop.note.submit',
slotApiVersion: 2,
status: DescriptorStatus::Active,
permission: 'workshop.note.publish',
labelKey: 'workshop.approval.note.submit.label',
),
);
}
}Workshop contribution 갱신과 검증
docs/developers/examples/approval-contribution/verify.sh#!/usr/bin/env sh
set -eu
for required in \
packages/workshop/src/Descriptors/WorkshopApprovalDescriptors.php \
packages/workshop/src/Descriptors/WorkshopApprovalSlotWidgets.php \
packages/workshop/resources/js/approval/WorkshopNoteApprovalWidget.tsx \
packages/workshop/resources/js/index.ts
do
test -f "$required" || {
printf 'Missing Workshop Approval source: %s\n' "$required" >&2
exit 1
}
done
task artisan -- nexia-runtime:refresh-runtime-caches --if-route-cached --no-ansi
task artisan -- nexia-apps:validate-app-descriptors workshop --no-ansi
task artisan -- nexia-apps:doctor-package-app workshop --no-ansi
printf '%s\n' 'Workshop Approval contribution discovered and validated. Confirm Submit Note in Electronic Approval.'