App 알림 발행
App 소유의 결과 알림을 호스트 계약으로 발행하고 전달 권한을 확인합니다.
App이 durable 업무 이벤트를 소비하고 자신의 트랜잭션에서 결과를 확정한 뒤에는 NotificationContribution과 NotificationPublisher를 사용합니다. Core 내부 알림 클래스를 import하거나 알림 행을 직접 만들지 마세요.
알림 기여 등록
기존 package manifest의 contributionLocations()에 contribution 디렉터리를 등록합니다. App manifest에서 사용하는 형식은 다음과 같습니다.
use Nexia\AppRuntime\AbstractPackageAppManifest;
final class WorkshopAppManifest extends AbstractPackageAppManifest
{
public function contributionLocations(): array
{
return [[
'namespace' => 'Workshop\\Contribution',
'directory' => __DIR__.'/Contribution',
]];
}
}NotificationContribution으로 App 소유 category와 intent를 등록합니다. intent key와 category는 App key로 시작해야 합니다. App 자신의 결과 이벤트를 Inbox에서 소비해 결과 저장이 성공한 뒤 publish($event, new NotificationPublication(...))을 호출합니다. event name과 producer key도 App key로 시작해야 하며 법인 값은 publication과 같아야 합니다. owner에는 canonical ResourceRef, recipient에는 사용자 ID, dedupeKey에는 재시도에도 변하지 않는 결과 식별자를 넣습니다.
use Nexia\Events\EventEnvelope;
use Nexia\Notification\Contracts\NotificationContribution;
use Nexia\Notification\Contracts\NotificationIntent;
use Nexia\Notification\Contracts\NotificationPublisher;
use Nexia\Notification\NotificationCategory;
use Nexia\Notification\NotificationPublication;
use Nexia\ResourceReference\ResourceRef;
final class NoteCompletedIntent implements NotificationIntent
{
public function key(): string { return 'workshop.note.completed'; }
public function category(): string { return 'workshop.work'; }
public function policySnapshot(): array { return ['preference_mode' => 'default_on', 'email_policy' => 'in_app_only', 'realtime_attention' => 'center_only', 'realtime_variant' => 'success']; }
public function deepLink(array $params): ?string { return '/apps/workshop/notes/'.$params['note_id']; }
public function render(array $params, string $locale): array { return ['title' => $locale === 'ko' ? '노트 완료' : 'Note complete', 'body' => null]; }
}
final class WorkshopNotifications implements NotificationContribution
{
public function notificationCategories(): iterable { return [new NotificationCategory('workshop.work', 'workshop.notifications.work', 60)]; }
public function notificationIntents(): iterable { return [new NoteCompletedIntent]; }
}
// App source 결과 이벤트를 소비한 Inbox handler 안에서 결과 저장 후 호출합니다.
/** @var NotificationPublisher $notifications */
/** @var EventEnvelope $event */
$notifications->publish($event, new NotificationPublication(
intentKey: 'workshop.note.completed',
owner: new ResourceRef('workshop', 'workshop.note', $note->public_id, $note->displayLabel()),
legalEntityId: $note->legal_entity_id,
recipientIds: [$requesterId],
params: ['note_id' => $note->public_id],
dedupeKey: 'note:'.$note->public_id.':completed',
));전달과 접근 권한 확인
반드시 source App Inbox handler 안에서 호출하고 발행 실패를 잡아 넘기지 마세요. 실패하면 source handler 트랜잭션도 롤백되고 기존 Inbox 작업이 failed가 되며, 기존 Inbox 재시도가 source 이벤트를 다시 전달합니다. Core는 비활성 App을 건너뛰고 같은 Inbox 트랜잭션에서 contribution 소유권을 확인하며, 각 수신자의 현재 읽기 권한과 법인 범위로 owner Resource를 다시 해석합니다. 기존 수신 설정과 이메일 delivery를 그대로 적용하며 수신자별 중복을 막습니다. 화면은 수신자의 현재 locale로 intent를 렌더링하고, 나중에 권한이 회수되면 해당 행을 숨기고 아직 보내지 않은 이메일을 막습니다.