Setup 태스크 기여
App을 Core 내부에 결합하지 않고 테넌트 Setup plan에 App 소유 준비 태스크를 추가하는 방법입니다.
테넌트의 기존 Setup 계획에 준비 상태 태스크 하나를 추가합니다. App은 완료 조건을 평가하고, 그 조건을 충족할 설정 화면을 연결합니다.
빠른 구현 순서
SetupTaskContribution에서 App 접두사의 안정적인 키, revision, evaluator 클래스, 번역 제목·설명을 가진SetupTaskDefinition을 반환합니다.SetupTaskEvaluator가 App 데이터를 변경하지 않고 준비 상태를 읽게 합니다. 상태가 다시 미완료가 될 수 있으면 Live, 명시적으로 수락한 완료를 유지해야 하면 acknowledgement가 필요한Latched를 선택합니다.SetupTaskAction에 기존 App 경로, 테넌트 범위 권한, 해당 조건을 바꾸는 좁은recheckAftermatcher를 지정합니다. Setup의 권한 판단에는 Legal Entity나 Operating Unit 컨텍스트가 없습니다.- Contribution discovery를 갱신하고 운영 가능한 테넌트에서 Setup을 엽니다. 보호된 API로 설정을 저장한 다음 계획을 다시 읽습니다.
결과 확인
운영 가능한 App의 태스크가 나타나고, 지정한 권한이 있을 때만 동작을 열 수 있으며, 실제 근거에 따라 평가 결과가 변해야 합니다. 안내 가이드를 마치는 것만으로 태스크가 완료되지는 않습니다. 검사는 App 테스트하기를 따릅니다.
아래 People 사례는 선언·근거 평가·revision·변경 관찰을 함께 보여줍니다. 설정 화면이 없다면 설정 페이지 추가부터 진행하세요.
1. 태스크 선언
Manifest contribution location 아래에서
Nexia\Setup\Contracts\SetupTaskContribution을 구현합니다. 아래는 People Core의 근로자 번호 evaluator를 연결하는 완전한 단일 태스크 예시입니다. 선택적 section 묶음은 생략했습니다.
<?php
declare(strict_types=1);
namespace Amuzcorp\Nexia\PeopleCore\Contribution;
use Amuzcorp\Nexia\PeopleCore\Setup\WorkerNumberingSetupTaskEvaluator;
use Nexia\Mutation\MutationMatcher;
use Nexia\Mutation\MutationOperation;
use Nexia\Setup\Contracts\SetupTaskContribution;
use Nexia\Setup\Data\SetupTaskAction;
use Nexia\Setup\Data\SetupTaskDefinition;
use Nexia\Setup\Enums\SetupTaskCompletionMode;
use Nexia\Setup\Enums\SetupTaskImportance;
final class PeopleSetupTasks implements SetupTaskContribution
{
public function tasks(): array
{
return [
new SetupTaskDefinition(
key: 'people.worker_numbering',
revision: 1,
priority: 100,
titleKey: 'people.setup.worker_numbering.title',
descriptionKey: 'people.setup.worker_numbering.description',
importance: SetupTaskImportance::Required,
completionMode: SetupTaskCompletionMode::Live,
skippable: false,
requiresAcknowledgement: false,
dependencies: [],
evaluatorClass: WorkerNumberingSetupTaskEvaluator::class,
defaultAction: new SetupTaskAction(
'/apps/people/worker-number-rules',
['people.worker_number_rule.read'],
recheckAfter: new MutationMatcher(
resourceKeys: ['people.worker_number_rule'],
operations: [
MutationOperation::Created,
MutationOperation::Updated,
MutationOperation::Deleted,
],
),
),
),
];
}
}MutationMatcher와 MutationOperation은 Nexia\Mutation에서 import합니다.
Matcher는 route와 독립적이며, 사용자가 저장한 뒤 안내 에이전트를 깨울 도메인
변경을 나타냅니다.
전체 선언은 packages/people/src/Contribution/PeopleSetupTasks.php에 있습니다.
호스트는 Manifest의 AppDefinition에서 App Family, App 이름, 순서를 가져옵니다.
Contributor는 이 값을 선언하지 않습니다. 자기 task 목록에 한 단계 더 묶음이 필요할
때만 $basics 같은 SetupTaskSection을 선언하고, 필요 없으면 section을 생략합니다.
한 App 안에서 반복한 section metadata는 같아야 합니다. App 내부에서는 section
priority, task priority, 마지막으로 task key가 결정적 순서를 정합니다.
제품 안내에는 Required 또는 Recommended를 사용합니다. Account Owner가 근거를
충족하지 않고도 의도적으로 terminal 처리할 수 있을 때만 skippable을 켭니다.
건너뛸 수 있는 태스크에는 언제 skip해도 안전한지 설명하는 locale key를
skipGuidanceKey에 선택적으로 지정할 수 있습니다. 호스트는 그 문구를 snapshot에
실을 뿐, skip을 인가하거나 완료를 결정하지 않습니다. Core 태스크는 Core 태스크에만
의존할 수 있습니다. App 태스크는 Core 또는 같은 App 소유 태스크에 의존할 수 있지만
다른 App에는 의존할 수 없습니다.
2. App 소유 근거 평가
Contributor는 선언만 하며 테넌트 DB를 조회하지 않습니다. 도메인 query는 App이
소유한 SetupTaskEvaluator에 둡니다. People의 근로자 번호 evaluator는 불변
평가 시각과 App 소유 model을 사용합니다.
<?php
declare(strict_types=1);
namespace Amuzcorp\Nexia\PeopleCore\Setup;
use Amuzcorp\Nexia\PeopleCore\Enums\WorkerNumberPurpose;
use Amuzcorp\Nexia\PeopleCore\Models\WorkerNumberRule;
use Nexia\Setup\Contracts\SetupTaskEvaluator;
use Nexia\Setup\Data\SetupCriterion;
use Nexia\Setup\Data\SetupEvaluationContext;
use Nexia\Setup\Data\SetupTaskAssessment;
final class WorkerNumberingSetupTaskEvaluator implements SetupTaskEvaluator
{
public function evaluate(SetupEvaluationContext $context): SetupTaskAssessment
{
$configured = WorkerNumberRule::query()
->where('purpose', WorkerNumberPurpose::Worker->value)
->whereNull('legal_entity_id')
->effectiveOn($context->evaluatedAt->format('Y-m-d'))
->exists();
return new SetupTaskAssessment(
applicable: true,
criteria: [new SetupCriterion(
'active_effective_tenant_worker_rule',
'people.setup.worker_numbering.criteria.active_effective_tenant_worker_rule',
$configured,
)],
);
}
}구현은 packages/people/src/Setup/WorkerNumberingSetupTaskEvaluator.php에 있습니다.
SetupEvaluationContext에는 불투명한 tenant·actor key, locale, 하나의 불변 평가
시각, 현재 task key, 선택적인 host 제공 subject key가 있고 Core model은 없습니다.
일반 App 선언은 subject key를 비워 둡니다. 유효일 규칙에는 evaluatedAt을 사용해
한 snapshot의 모든 criterion이 같은 시각을 보게 합니다.
평범한 설정 누락은 blocker가 아니라 충족되지 않은 required criterion으로
반환합니다. Blocker는 외부 선행 조건을 사용할 수 없는 경우처럼 태스크를 진행할
수 없다는 뜻입니다. 해당 테넌트에 태스크가 실제로 적용되지 않을 때만
applicable: false를 반환하며, 이 경우 호스트가 payload에서 제외합니다.
3. 완료와 revision 동작 선택
pending, in_progress, blocked, completed, skipped는 호스트가
파생합니다. App은 status가 아니라 근거를 반환합니다.
| 모드 | 선택하는 경우 | 저장 동작 |
|---|---|---|
Live | 완료가 현재 도메인 근거를 따라야 할 때 | Snapshot마다 다시 평가하며 근거가 바뀌면 미완료로 돌아갈 수 있음 |
Latched | 의도적으로 수락한 완료를 유지해야 할 때 | Acknowledgement가 필수이며 해당 task revision의 completion latch를 유지 |
Task identity와 revision에 따라 업데이트 결과가 달라집니다.
| 변경 | 테넌트에 보이는 결과 |
|---|---|
| 태스크 추가 | App이 operational이면 다음 조회부터 같은 get-started plan에 합류 |
| 같은 key·revision으로 evaluator 로직 변경 | Live 근거는 즉시 달라지고 revision이 맞는 기존 상호작용 사실은 계속 적용 |
| Task revision 증가 | 이전 상호작용 사실을 무시하고 다음 허용된 상호작용 때 새 revision으로 row 초기화 |
| 태스크를 applicable하지 않게 만들거나 App을 non-operational로 전환 | 태스크는 빠지지만 제외 자체가 state row를 삭제하지는 않음 |
| 같은 key·revision으로 App을 다시 operational로 전환 | 같은 plan에 태스크가 돌아오고 남아 있는 일치 상호작용 상태를 다시 적용 |
| Task key 변경 또는 제거 | 이전 row는 사용되지 않고, 바뀐 key는 새로운 task identity가 됨 |
이전 계약에서 기록한 start, skip, acknowledgement, completion latch를 더는 적용하면
안 될 때만 revision을 올립니다. 기존 상호작용이 여전히 유효한 문구, 정렬,
evaluator 변경에는 올리지 않습니다.
Plan 자체 revision은 응답 metadata입니다. Task state를 무효화하지 않으며 append-only
history도 아닙니다. 현재 get-started plan은 이 계약을 개발하는 동안 revision 1을
유지합니다. App을 설치하거나 다시 설치해도 plan record가 추가로 생기지 않습니다.
Operational App 태스크가 다음 평가 때 하나의 host plan에 합쳐집니다.
4. Plan 업데이트와 App contribution 관찰
GET /api/setup/plans/get-started는 write-free입니다. Prefetch와 retry가 task를
배정하거나 NEW 표시를 지우지 않습니다. Shell은 Setup이 실제로 열린 뒤에만
POST /api/setup/plans/get-started/open을 호출합니다. 이 transaction은
setup_plan_states의 seen revision/open time과 setup_task_states의 현재 적용
가능 태스크 assignment(first/last seen, source owner/kind, assignment plan revision)를
기록하면서 기존 상호작용 사실은 유지합니다.
Open 응답은 의도적으로 write 전 snapshot입니다. 따라서 처음 렌더된 화면에서
업데이트 안내와 NEW 태스크/source가 보입니다. 그 다음 read 또는 reopen부터는
배정된 태스크를 더 이상 새 항목으로 표시하지 않습니다. Setup을 한 번도 열지 않은
테넌트는 baseline입니다. 상호작용 row가 없는 기존 live-completed 태스크를 포함해
현재 적용 가능한 모든 태스크가 is_new: false입니다.
Baseline이 있는 테넌트에서 assignment가 없는 적용 가능 태스크만 새 항목입니다.
Core 태스크는 new_source.kind: core_contribution, App 태스크는 opaque owner key와
함께 app_contribution을 반환합니다. 이 값은 태스크를 제공한 주체를 나타낼 뿐,
정확한 설치·활성화·release·plan revision event를 단정하지 않습니다. 같은 key의 task revision이 다르면
is_updated입니다. 첫 open은 그 write 전 상태를 반환하고, 이후 해당 태스크의
오래된 상호작용 사실만 지운 뒤 새 revision을 기록합니다. 이 판단에 Core가 App
namespace나 domain model을 알 필요는 없습니다.
Core는 특정 App을 대상으로 하면서 Platform에 배치되는 태스크도 기여할 수 있습니다.
이 payload는 app: null을 유지하고 canonical App identity와 정렬 metadata를
subject_app으로 제공합니다. 여러 App을 요약하는 호스트 태스크는 대신 canonical
Family 및 launcher 순서의 subject_apps를 제공합니다. 현재
core.initial_app_access 태스크는 이 집계 형식을 사용하며, active·initialized·
code-loaded App마다 필수 initial_access_decision.<app-key> criterion 하나를
내보냅니다. Shell은 확인 수/전체 수가 있는 태스크 하나로 표시하고, 펼친 상세에서
App별 상태를 유지합니다. 두 subject field 모두 호스트 전용 composition
metadata이며 App contribution이 선언하지 않습니다.
이 집계 action도 App을 인식합니다. Shell은 canonical Family·launcher 순서에서
첫 미충족 criterion의 App을 골라
/settings/apps?step=access&app={app-key}를 엽니다. App Catalog는 해당 App
소유이면서 deprecated되지 않은 비기술 Role Preset 중 tenant와 현재 Legal Entity
scope 항목을 읽습니다. 관리자는 권한이 있는 각 scope에서 preset과 사용자를
고르며, 선택한 모든 사용자는 그 scope에서 선택한 모든 Role을 받습니다. 두
scope를 모두 고르면 tenant bootstrap은 finalize: false로 실행되고 마지막
Legal Entity bootstrap이 성공한 뒤에만 configured를 기록하므로, 뒤 호출이
실패해도 criterion은 미완료·재시도 가능으로 남습니다. 행위자가 self-approve할
수 없는 protected preset은 Grant 없이 준비되고 별도 승인 대상으로 안내됩니다.
POST /api/apps/{appKey}/initial-access-decision은 not_needed만 받으며,
configured는 성공한 Role bootstrap이 쓰는 근거입니다. 어느 결과를 완료해도
catalog는 다음 pending App으로 진행합니다.
호스트는 core.data_migration을 App·이관 대상·단계마다 반복하지 않고 테넌트
단위 온보딩 결정 하나로 기여합니다. 이 태스크는 건너뛸 수 있는 revision 2
Recommended Live 태스크입니다. data 섹션 메타데이터에 따라 조직 설정
다음, 사용자 및 접근 권한 앞에 정렬되지만 선행 태스크는
core.legal_entity_profile 하나뿐입니다. 테넌트 범위 system.data.import 권한이
있는 행위자는 이관 가능한 데이터 확인하기로 /settings/data-migrations를
열거나 이관 없이 시작으로 명시적으로 건너뜁니다. 각 실행은 안정적인 stage,
provider, target key와 함께 tenant database에 저장됩니다. 오류 없이 완료된 run은
Setup을 자동 완료하며 processing, failed, completed-with-errors 실행은 진행 중으로
남습니다. App backend도 App-neutral SDK recorder로 같은 근거를 기록하고 frontend
onCompleted는 재조회 힌트로만 사용합니다.
Payload가 결정적 정렬과 다른 consumer를 위해 Core section metadata를 유지하더라도, Shell은 Platform 태스크를 하나의 평평한 기본 설정 목록으로 표시합니다. App-owned 태스크는 일반 placement 계약에 따른 Family, App, 선택적 App section 계층을 유지합니다.
Operational Health, record별 compliance, cross-tenant projection, Account Owner delegation은 이 메커니즘의 범위가 아닙니다. Beta Setup은 Account Owner-only readiness surface로 유지합니다.
5. Action, 권한, 재확인 조건, 번역 추가
SetupTaskAction::routeTarget은 App 소유의 불투명한 navigation 값입니다. Package
path나 Core 내부 alias가 아니라 App의 공개 Shell route를 가리킵니다. 호스트는
requiredPermissionKeys를 tenant-scoped permission으로 평가합니다. Legal Entity나
Operating Unit scope permission으로는 이 Setup action을 executable로 만들 수 없습니다.
Action permission은 체크리스트가 navigation을 제공할지만 정합니다. 태스크를 완료하거나 block하지 않고, Setup endpoint를 authorize하지 않으며, 목적지 API의 권한 검사를 대체하지도 않습니다. 목적지는 자기 read·write policy를 계속 강제합니다.
SetupTaskAction::recheckAfter는 선택 사항입니다. 사용자의 실제 저장 뒤 에이전트 안내
설정이 이어서 진행해야 한다면 추가합니다. Matcher는 다음 subject
계열 중 정확히 하나만 지정합니다.
| 변경 소유자 | Matcher |
|---|---|
| Resource Catalog model | resourceKeys와 Created, Updated, Deleted, Restored 중 하나 이상. 선택적 resourceIds로 범위를 좁힘 |
| Resource root가 없는 성공한 semantic action | actionKeys와 Succeeded만 사용 |
한 matcher 안의 key와 operation은 OR 의미입니다. 가능하면 태스크가 소유한 안정된
Resource Key를 사용하세요. 호스트가 catalog에 연결된 모든 model의 commit된 Eloquent
lifecycle event를 자동 관찰하므로 일반 생성형 저장·삭제 경로에는 controller hook이
필요 없습니다. Query Builder bulk update처럼 model event를 우회하는 경로는
Nexia\Mutation\Contracts\MutationPublisher를 주입하고 업무 write 성공 뒤
resourceChanged()를 호출합니다. Resource가 아닌 action은 성공 뒤
actionSucceeded()를 호출합니다.
호스트는 알 수 없는 Resource Key, 다른 App 소유 Resource를 지정한 App task,
task owner 접두 밖의 action key를 거부합니다. Route, 활성 tab, HTTP method, 범용
"save" event를 matcher로 쓰지 마세요. Mutation은 깨우기 힌트일 뿐입니다.
core.mutation.await가 반환되면 에이전트가 plan을 다시 읽고 evaluator가 완료를 다시
판정합니다. 다른 route의 저장도 같은 선언된 Resource나 semantic action을 commit한
경우에만 이 태스크를 깨울 수 있습니다.
requiresAcknowledgement: true인 태스크는 observed wait 뒤 fresh plan read가 같은
next task, blocker 없음, 모든 required criterion 충족을 확인한 경우에만 에이전트 안내
흐름이 상호작용을 기록합니다. 그다음 AUTO tier인
core.setup.task.acknowledge를 호출합니다. 이 도구는 tenant.setup.update가
필요하고 Setup interaction state만 변경하며, 호출 뒤 plan을 다시 읽습니다.
recheck, timeout, reconnect, matcher 자체만으로는 acknowledgement를 허용하지
않습니다.
App이 소유한 section, task, criterion, blocker key를 모든 App locale catalog에 추가합니다. Shell은 plan을 그릴 때 설치된 App catalog를 불러옵니다. App Family와 App label은 canonical App definition에서 이미 오므로 Setup용으로 다시 선언하지 않습니다.
6. Package 계약 테스트
Contributor와 evaluator를 App package에서 테스트합니다. 최소한 다음을 검증하세요.
- Manifest가 contributor를 발견하고 호스트가 소유 App을 추론하는지
- Task key, revision, ordering, action route, permission, mutation matcher가 정확한지
- 실제 경계에서 모든 required criterion이 false와 true가 되는지
- 유효 종료일이 도메인의 inclusive 또는 exclusive 규칙을 정확히 따르는지
- 지원하는 모든 locale catalog에 선언한 key가 있는지
- Inactive 등 유효하지 않은 도메인 record가 실수로 태스크를 완료하지 않는지
Host test는 owner 접두 task key, 중복 key, dependency 정책과 cycle, operational App 필터, status 우선순위, 상호작용 저장, revision 무효화, tenant 격리를 이미 다룹니다. 이 host 소유 계약을 바꿀 때만 host test를 추가합니다.
검증
운영 예시의 focused package test를 실행합니다.
task test -- packages/people/tests/Feature/PeopleSetupTasksTest.php --sequential --compactContribution shape, evaluator 네 개의 경계, locale parity 검사가 모두 통과해야 합니다. 다른 App을 구현할 때는 그 package의 같은 범위 focused test를 실행하세요.
그다음 테넌트 lifecycle을 수동으로 확인합니다.
| 확인 | 기대 결과 |
|---|---|
| App이 operational이 되기 전 plan 조회 | App 태스크가 없음 |
| Setup baseline 이후 App 설치와 초기화 후 다시 열기 | 태스크가 App owner/source와 NEW로 get-started 안에 나타나며 두 번째 plan은 생기지 않음 |
| Live criterion을 충족하고 Setup 다시 열기 | 태스크가 completed로 바뀜 |
| 에이전트 안내 설정을 시작한 뒤 무관한 Resource 저장 | 대기 중인 에이전트가 계속 기다림 |
| 다른 route에서 태스크가 match하는 Resource 저장 | 에이전트가 깨어 plan을 다시 읽으며 evaluator 결과만 신뢰 |
| 근거를 제거하고 Setup 다시 열기 | Live 태스크가 미완료 상태로 돌아감 |
| App을 non-operational로 바꿨다가 다시 operational로 전환 | 태스크가 사라졌다 돌아오고 일치하는 기존 상호작용이 계속 적용되며 다시 NEW가 되지 않음 |
| Action permission이 없는 행위자로 확인 | 태스크는 보이지만 navigation action은 사용할 수 없음 |
흔한 실수
Evaluator 결과 저장. 호스트는 snapshot마다 현재 App 근거를 평가합니다. App의 도메인 사실만 저장하고 Setup 상호작용·assignment 관찰 사실은 Core가 별도로 저장하게 둡니다.
태스크 하나를 초기화하려고 plan revision 증가. Plan revision은 task state를 초기화하지 않습니다. 해당 task revision을 올리세요.
코드가 바뀔 때마다 task revision 증가. 다음 상호작용 때 유효한 skip, acknowledgement, latch 이력이 사라집니다. 상호작용 계약이 바뀔 때만 올립니다.
Action에 Legal Entity permission 사용. Setup action 결정은 tenant scope이므로 행위자가 다른 scope grant를 가지고 있어도 action은 사용할 수 없습니다.
다른 App 태스크에 의존. Registry가 App 간 dependency를 거부합니다. Core 또는 같은 owner에만 의존하세요.
열린 route나 모든 성공 요청을 저장 신호로 사용. Route는 navigation이지 도메인 identity가 아닙니다. 좁은 Resource 또는 semantic action matcher를 선언하면 호스트가 무관한 저장을 무시합니다.
Match된 mutation을 완료로 취급. Mutation은 에이전트를 깨울 뿐입니다. Evaluator가 여전히 criterion 미충족을 반환할 수 있으므로 authoritative plan을 항상 다시 읽습니다.
recheck 뒤 acknowledge. Recovery는 브라우저가 신호를 잃었을 가능성을 뜻할 뿐
사용자 저장 근거가 아닙니다. observed와 위 fresh evaluator 검사를 모두 거친 뒤에만
acknowledge합니다.
설정 누락을 blocker로 처리. 평범한 근거 누락은 incomplete criterion입니다. 진행을 막는 조건에만 blocker를 사용하세요.
구현 참고
packages/people/src/Contribution/PeopleSetupTasks.php에서 추가 태스크, 선택적 section, 건너뛰기 안내를 확인할 수 있습니다. packages/people/tests/Feature/PeopleSetupTasksTest.php는 메타데이터·근거 경계·번역 키를 검사합니다.
다음 단계
- Contribution 계약 — contributor 발견과 소유권 규칙
- SDK 계약 — 공개 namespace와 package 경계
- App Manifest 계약 — contribution location 선언
- 보호된 API 추가하기 — action 목적지 강제