본문으로 건너뛰기

예제

업무 프로세스 기여하기

Workshop 동작을 BPMN Service Task catalog에 게시하고 App 소유 handler를 구현해 양쪽 등록을 검증합니다.

예제 유형 레시피

개요

업무 프로세스 기여하기 예제

이 예제는 Workshop App의 Note를 보관하는 동작을 Nexia Business Process의 BPMN Service Task에 공개하고, Process 런타임이 호출할 App handler까지 연결합니다. 앞의 Resource 생성하기에서 Workshop Note가 만들어지고 Contributor 만들기에서 workshop.note.archive 권한이 추가됐다고 가정합니다.

최종 결과

구성 요소역할확인 결과
ProcessWorkActionDescriptorworkshop.note.archive를 BPMN 동작 catalog에 게시Service Task 설정에서 노트 보관 선택 가능
NotePolicy::archive()App의 보관 권한과 레코드 범위를 판정시작 사용자를 평범한 App Policy로 재인가
ArchiveNoteProcessWorkActionHandler복원된 사용자와 Legal Entity 문맥에서 Note를 보관Note 상태가 archived로 변경
ProcessWorkActionRegistrar 등록descriptor identity를 handler factory와 연결external task가 incident 없이 완료
ProcessWorkActionConformance 테스트모든 Workshop serviceTask에 handler가 있는지 검사배포 전에 descriptor/handler 누락 발견

BPMN 자동 작업의 실행할 작업 선택기에 추가된 Workshop 노트 보관

위 화면은 Work Action descriptor와 locale를 적용하고 runtime catalog를 갱신한 실제 결과입니다. 업무 프로세스 편집기에서 자동 작업 → 실행할 작업을 열면 기타 앱 → Workshop → 노트 보관이 추가됩니다. 이 선택지가 보인다는 것은 Core가 정적 descriptor를 발견했다는 뜻이며, handler 등록과 실제 Note 보관 성공까지 보장한다는 뜻은 아닙니다. 실행 가능 여부는 이 예제의 2·3·6단계에서 완성하고 검증합니다. 오른쪽의 필드가 없습니다는 작성자가 입력할 payloadSchema가 없다는 뜻입니다. 실행 후 나오는 status 등의 outputContract는 5단계의 출력 매핑에서 사용합니다.

BPMN Start Event
  → Service Task: workshop.note.archive
     → Core가 Process instance의 Tenant·Legal Entity·시작 사용자를 복원
     → Workshop handler가 Note를 인가하고 보관
     → status = archived 출력
  → End Event

이 예제는 BPMN 정의를 생성하거나 Process를 시작하는 방법이 아니라, BPMN이 호출할 App 소유 작업 동작을 제공하는 방법에 집중합니다. 실행되는 Process instance는 workshop.note Resource에 연결돼 있어야 합니다. 연결되지 않았거나 다른 App Resource에 연결된 instance에서 실행하면 Core와 handler가 실패로 닫습니다.

1. Work Action descriptor 게시

descriptor contributor를 Workshop의 기존 discovery 위치에 복사합니다.

코드 예시
Shell
cp docs/developers/examples/business-process-contribution/WorkshopProcessActions.php \
  packages/workshop/src/Descriptors/WorkshopProcessActions.php

핵심 선언은 다음과 같습니다.

코드 예시
PHP
new ProcessWorkActionDescriptor(
    kind: 'serviceTask',
    appKey: 'workshop',
    actionKey: 'note.archive',
    labelKey: 'workshop.process_actions.note.archive.label',
    topic: 'workshop.note.archive',
    outputContract: [
        'note_public_id' => ['type' => 'string'],
        'status' => [
            'type' => 'string',
            'enum' => NoteStatus::values(),
        ],
    ],
)

kind: 'serviceTask'는 Process가 App 작업 완료를 기다린다는 뜻입니다. actionKey는 App 안의 영구 identity이고 topic은 worker routing identity입니다. BPMN 작성자는 raw topic을 입력하지 않고 descriptor가 제공한 현지화된 동작을 선택합니다. outputContract의 필드만 Activity IO를 통해 이후 Process 변수로 매핑할 수 있습니다.

이 단계가 끝나면 descriptor validator는 통과할 수 있지만 아직 실행은 안 됩니다. Descriptor는 catalog 항목을 선언할 뿐 handler를 자동으로 등록하지 않습니다.

2. 보관 권한과 App handler 구현

먼저 packages/workshop/src/Policies/NotePolicy.php에 보관 능력을 추가합니다.

코드 예시
PHP
public function archive(Actor $user, Note $note): bool
{
    return $this->allowsPermission($user, 'workshop.note.archive')
        && $this->matchesAuthorizationContract($note);
}

workshop.note.archive라는 Legal Entity 범위 권한과 Note의 레코드 소유 범위가 모두 맞아야 허용됩니다. Process에서 시작했다는 사실은 권한을 추가로 주지 않습니다.

handler 파일을 App 소스에 복사합니다.

코드 예시
Shell
mkdir -p packages/workshop/src/Process
cp docs/developers/examples/business-process-contribution/ArchiveNoteProcessWorkActionHandler.php \
  packages/workshop/src/Process/ArchiveNoteProcessWorkActionHandler.php

handler는 ProcessWorkActionInvocation에서 Core가 복원한 문맥을 받고 App 모델만 변경합니다. 예제 구현은 다음 안전 속성을 함께 지킵니다.

  • ResourceRef가 workshop.note인지 먼저 확인합니다.
  • Note를 현재 Legal Entity 안에서만 조회하고 row lock을 잡습니다.
  • Process 시작 사용자를 Gate::forUser()에 전달해 NotePolicy::archive()로 다시 인가합니다.
  • 이미 보관된 Note에도 같은 성공 결과를 반환해 재시도를 멱등하게 처리합니다.
  • 복구 가능한 업무 실패에는 안정적인 ProcessWorkActionException 코드를 제공합니다.

성공 시 handler는 다음 계약으로 출력을 반환합니다.

코드 예시
PHP
return new ProcessWorkActionResult(output: [
    'note_public_id' => (string) $note->public_id,
    'status' => NoteStatus::Archived->value,
]);

App은 Core의 Process instance, token, external-task 모델을 import하거나 직접 갱신하지 않습니다. handler 결과를 받은 Core가 external task 완료와 token 전진을 소유합니다.

3. 서비스 프로바이더에 handler 등록

packages/workshop/src/WorkshopServiceProvider.php에 세 import를 추가합니다.

코드 예시
PHP
use Amuzcorp\Nexia\Workshop\Descriptors\WorkshopProcessActions;
use Amuzcorp\Nexia\Workshop\Process\ArchiveNoteProcessWorkActionHandler;
use Nexia\Process\Contracts\ProcessWorkActionRegistrar;

같은 파일의 boot()에서 App 등록 뒤 handler factory를 등록합니다.

코드 예시
PHP
$this->app->make(ProcessWorkActionRegistrar::class)->register(
    'workshop',
    WorkshopProcessActions::ARCHIVE_NOTE,
    static fn (): ArchiveNoteProcessWorkActionHandler => app(
        ArchiveNoteProcessWorkActionHandler::class,
    ),
);

appKey와 actionKey 쌍은 descriptor와 정확히 같아야 합니다. topic만 같게 만들어서는 연결되지 않습니다. 이 단계가 끝나면 실행 중인 Core worker가 workshop.note.archive external task를 Workshop handler로 전달할 수 있습니다. registrar는 정확한 appKey/actionKey 쌍으로 handler를 찾으며 descriptor version이나 topic으로 선택하지 않습니다.

4. 현지화 라벨 추가

Workshop이 지원하는 모든 resources/lang/{locale}.json에 같은 key를 추가합니다. 한국어판은 다음 값입니다.

코드 예시
JSON
{
  "workshop.process_actions.note.archive.label": "노트 보관"
}

영문판에는 Archive note처럼 해당 locale의 자연스러운 값을 넣습니다. key가 빠지면 BPMN 선택기에 raw key 대신 일반화된 업무 라벨이 표시될 수 있습니다.

5. BPMN Service Task에 연결

runtime cache를 갱신한 뒤 Business Process 정의 편집기에서 다음 순서로 연결합니다.

  1. Start Event, Service Task, End Event를 차례로 연결합니다.
  2. Service Task의 Work Action에서 Workshop의 노트 보관을 선택합니다.
  3. 출력 매핑에서 status를 Process 변수 archive_status에 연결합니다.
  4. 정의를 저장하고 게시합니다.
  5. workshop.note에 연결된 Process instance를 시작합니다. 시작 사용자는 해당 Note의 workshop.note.archive 권한과 레코드 범위를 가져야 합니다.

실행 후 Note 상태는 archived, Process 변수 archive_status는 archived가 되고 instance는 End Event까지 진행해야 합니다. Work Action version과 topic은 external task 생성 시 고정됩니다. 게시된 note.archive action key의 의미를 바꾸지 마세요. 동작 의미가 달라지면 새 action key를 게시하고, 대기 task가 완료될 때까지 기존 descriptor version과 handler를 유지해야 합니다.

6. 등록 계약 검증

계약 테스트를 Workshop 패키지에 복사합니다.

코드 예시
Shell
cp docs/developers/examples/business-process-contribution/WorkshopProcessWorkActionTest.php \
  packages/workshop/tests/Feature/WorkshopProcessWorkActionTest.php
sh docs/developers/examples/business-process-contribution/verify.sh

validator는 App descriptor contracts are valid.로 끝나고 package doctor에는 FAIL이 없어야 합니다. 마지막 테스트는 catalog의 모든 Workshop serviceTask에 등록된 handler가 있는지 검사하며, 성공하면 스크립트가 다음 문장을 출력합니다.

Workshop Business Process work action is catalogued and paired with a registered handler.

자주 발생하는 오류

증상원인해결다시 확인
BPMN 선택기에 노트 보관이 없음descriptor 미발견, App 미설치 또는 cache 미갱신discovery 위치와 설치 상태를 확인하고 runtime cache 갱신선택기의 기타 앱 → Workshop 아래에 노트 보관이 표시됨
테스트가 “no handler is registered”로 실패서비스 프로바이더 등록 누락 또는 key 불일치workshop과 WorkshopProcessActions::ARCHIVE_NOTE로 등록verify.sh가 등록 정합성 완료 문구를 출력
instance가 external task incident에서 멈춤Resource 연결이 없거나 다른 App Resource임instance를 정규 workshop.note ResourceRef로 시작Service Task가 완료되고 instance가 End Event까지 진행
handler에서 인가 실패시작 사용자가 Note 보관 권한 또는 레코드 범위를 갖지 않음현재 Legal Entity의 Role과 Access Grant 확인같은 사용자가 대상 Note를 보관하고 상태가 archived로 바뀜
Note는 보관됐지만 다음 Gateway가 값을 못 읽음Activity IO 출력 매핑 누락status를 archive_status 같은 Process 변수에 매핑완료된 instance 변수에 archive_status=archived가 존재

다음 단계

App에서 Process를 직접 시작하려면 업무 프로세스의 App 소유 Process 시작과 업무 프로세스 계약의 ProcessStarter·BoundProcessStart 계약을 이어서 사용하세요. 사람 입력 단계가 필요하면 Work Action에 userTask kind를 추가하지 말고 ProcessUserTaskFormDescriptor를 사용합니다.

설정별 동작까지 확인하기

이 Workshop 예제는 최소 기여 연결을 검증합니다. 결재 필수 정책이나 기본 절차 설치를 자동으로 추가하지는 않습니다. 전자 결재에서 개발자의 정책 적용 조건과 관리자의 양식·결재선·필수 설정 순서를 확인한 뒤, 업무 프로세스의 실제 구매 흐름을 따라가세요. 자동 입력과 사람 검토, 결재 필수·불필요, 결재선 준비, 결과 분기, 기본안 업데이트를 하나의 흐름에서 비교할 수 있습니다.

포함된 파일

BPMN Work Action Descriptor

docs/developers/examples/business-process-contribution/WorkshopProcessActions.php
코드 예시
PHP
<?php

declare(strict_types=1);

namespace Amuzcorp\Nexia\Workshop\Descriptors;

use Amuzcorp\Nexia\Workshop\Enums\NoteStatus;
use Nexia\AppDescriptors\AppDescriptorSet;
use Nexia\AppDescriptors\Contracts\AppDescriptorContribution;
use Nexia\AppDescriptors\ProcessWorkActionDescriptor;

final class WorkshopProcessActions implements AppDescriptorContribution
{
    public const ARCHIVE_NOTE = 'note.archive';

    public static function appDescriptors(): AppDescriptorSet
    {
        return AppDescriptorSet::of(
            new ProcessWorkActionDescriptor(
                kind: 'serviceTask',
                appKey: 'workshop',
                actionKey: self::ARCHIVE_NOTE,
                labelKey: 'workshop.process_actions.note.archive.label',
                topic: 'workshop.note.archive',
                outputContract: [
                    'note_public_id' => ['type' => 'string'],
                    'status' => [
                        'type' => 'string',
                        'enum' => NoteStatus::values(),
                    ],
                ],
            ),
        );
    }
}

App 소유 Work Action Handler

docs/developers/examples/business-process-contribution/ArchiveNoteProcessWorkActionHandler.php
코드 예시
PHP
<?php

declare(strict_types=1);

namespace Amuzcorp\Nexia\Workshop\Process;

use Amuzcorp\Nexia\Workshop\Enums\NoteStatus;
use Amuzcorp\Nexia\Workshop\Models\Note;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Nexia\Process\Contracts\ProcessWorkActionHandler;
use Nexia\Process\Domain\ProcessWorkActionException;
use Nexia\Process\ProcessWorkActionInvocation;
use Nexia\Process\ProcessWorkActionResult;

final readonly class ArchiveNoteProcessWorkActionHandler implements ProcessWorkActionHandler
{
    public function handle(ProcessWorkActionInvocation $invocation): ProcessWorkActionResult
    {
        if ($invocation->resourceRef->appKey !== 'workshop'
            || $invocation->resourceRef->resourceKey !== 'workshop.note') {
            throw new ProcessWorkActionException(
                'The Process instance is not anchored to a Workshop note.',
                'workshop_note_resource_mismatch',
            );
        }

        return DB::transaction(function () use ($invocation): ProcessWorkActionResult {
            $note = Note::query()
                ->where('public_id', $invocation->resourceRef->resourceId)
                ->where('legal_entity_id', $invocation->legalEntity->key())
                ->lockForUpdate()
                ->first();

            if (! $note instanceof Note) {
                throw new ProcessWorkActionException(
                    'The Workshop note is not available in this Legal Entity.',
                    'workshop_note_unavailable',
                );
            }

            Gate::forUser($invocation->actor)->authorize('archive', $note);

            if ($note->status !== NoteStatus::Archived) {
                $note->forceFill(['status' => NoteStatus::Archived])->save();
            }

            return new ProcessWorkActionResult(output: [
                'note_public_id' => (string) $note->public_id,
                'status' => NoteStatus::Archived->value,
            ]);
        });
    }
}

Descriptor와 Handler 정합성 테스트

docs/developers/examples/business-process-contribution/WorkshopProcessWorkActionTest.php
코드 예시
PHP
<?php

declare(strict_types=1);

use Amuzcorp\Nexia\Workshop\Descriptors\WorkshopProcessActions;
use Nexia\AppDescriptors\ProcessWorkActionDescriptor;
use Nexia\Process\Contracts\ProcessWorkActionRegistrar;
use Nexia\Testing\ProcessWorkActionConformance;

test('every Workshop Process service task has a registered handler', function (): void {
    ProcessWorkActionConformance::assert(
        'workshop',
        WorkshopProcessActions::appDescriptors()->ofType(ProcessWorkActionDescriptor::class),
        app(ProcessWorkActionRegistrar::class),
    );
});

업무 프로세스 Contribution 갱신과 검증

docs/developers/examples/business-process-contribution/verify.sh
코드 예시
Shell
#!/usr/bin/env sh
set -eu

for required in \
  packages/workshop/src/Descriptors/WorkshopProcessActions.php \
  packages/workshop/src/Process/ArchiveNoteProcessWorkActionHandler.php \
  packages/workshop/tests/Feature/WorkshopProcessWorkActionTest.php
do
  test -f "$required" || {
    printf 'Missing Workshop Process source: %s\n' "$required" >&2
    exit 1
  }
done

grep -Fq 'WorkshopProcessActions::ARCHIVE_NOTE' \
  packages/workshop/src/WorkshopServiceProvider.php || {
    printf '%s\n' 'WorkshopServiceProvider has not registered the Process work-action handler.' >&2
    exit 1
  }

grep -Fq 'function archive' packages/workshop/src/Policies/NotePolicy.php || {
  printf '%s\n' 'NotePolicy does not authorize the archive business action.' >&2
  exit 1
}

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
task test -- packages/workshop/tests/Feature/WorkshopProcessWorkActionTest.php --compact

printf '%s\n' 'Workshop Business Process work action is catalogued and paired with a registered handler.'