컨텍스트
offload-project/laravel-waitlist은 Laravel 11/12/13 패키지(PHP 8.3+)로, 하나 이상의 대기자 명단을 관리합니다. 제공 기능:
WaitlistEloquent 모델(이름 및slug를 가진 대기자 명단) 및WaitlistEntry모델(대기자).WaitlistService(Waitlist파사드를 통해 해결)는for(),create(),add(),invite(),reject(),sendVerification(),verify()및 쿼리/카운트 헬퍼를 제공합니다.- 선택적 이메일 확인 절차: 게시된
/waitlist/verify/{token}경로. - 선택적
offload-project/laravel-invite-only연결:Waitlist::invite()를 호출하면 실제Invitation(토큰, 만료, 이벤트)이 생성되고WaitlistEntry::$invitation_id에 FK가 유지됩니다. - 두 개의 알림:
WaitlistInvited(옵트인,auto_send_invitation으로 제어) 및VerifyWaitlistEmail. - 수명 주기 이벤트(
OffloadProject\Waitlist\Events):WaitlistCreated,WaitlistEntryAdded,WaitlistEntryVerified,WaitlistEntryInvited,WaitlistEntryRejected,WaitlistEntrySubscribed,WaitlistEntryUnsubscribed,MailingListSyncFailed. - 메일링 리스트 통합(
MailingList파사드,MailingListManager): 항목을 Mailchimp, Kit(ConvertKit) 또는 Audienceful로 큐 작업으로 동기화.log및array드라이버와extend()훅 제공. - 타입화된 예외:
UnverifiedEntryException(인증 게이트가 켜져 있을 때 미인증 항목 초대 시도) 및MailingListException(드라이버/자격 증명/목록 ID/API 오류).
이 스킬은 composer.json에 offload-project/laravel-waitlist가 있는 Laravel 앱에서 작업하거나, 사용자가 Waitlist, WaitlistEntry, Waitlist 또는 MailingList 파사드, 또는 이 패키지의 대기자 명단 흐름에 대한 도움을 요청할 때 적용하세요.
규칙
파사드 사용
Waitlist파사드(OffloadProject\Waitlist\Facades\Waitlist)를 사용하세요.WaitlistService를 직접 인스턴스화하지 마십시오. 파사드가 지원되는 진입점입니다.- 특정 대기자 명단을 대상으로 하려면
Waitlist::for($slugOrIdOrModel)->...를 체이닝하세요.for(...)없이 호출하면 기본 대기자 명단에서 작동합니다(처음 사용 시getDefault()를 통해 자동 생성됨). Waitlist::for(...)는 싱글톤 서비스의 내부 상태를 변경합니다. 장기 실행 프로세스(큐 워커, Octane)의 경우, 이전 컨텍스트 호출이 유지된다고 가정하지 말고 모든 작업에 대해for(...)를 호출하세요.
대기자 명단 vs. 항목
Waitlist::create(string $name, string $slug, ?string $description = null, bool $isActive = true)로 대기자 명단을 생성하세요.slug는 정식 식별자이며,for(...)및find(...)에서 기대하는 값입니다.Waitlist::for($slug)->add($name, $email, $metadata = [])로 항목을 추가하세요. 확인 절차를 실행하려면WaitlistEntry::create([...])를 직접 호출하지 마세요.add()는waitlist.verification.enabled가 true일 때 자동으로sendVerification()을 트리거합니다.waitlist_entries테이블의 고유 제약 조건은['waitlist_id', 'email']이며,email만이 아닙니다. 동일한 사람이 여러 대기자 명단에 가입할 수 있습니다.
초대
Waitlist::invite($entryOrId, $options = [])를 통해 초대하세요. 알림 및Invitation레코드가 필요할 때$entry->markAsInvited()를 단독으로 호출하지 마세요.invite()는laravel-invite-onlyInvitation을 생성하고invitation_id를 통해 연결하며 항목을 초대됨으로 표시합니다.$options는InviteOnly::invite(...)로 전달됩니다. 일반적인 키:'invited_by'(모델 또는 int, 기본값auth()->user()),'role','metadata','expires_at'.waitlist.invitable.metadata_mapper에서 설정한 키를 중복하지 마세요. 명시적인$options가array_merge를 통해 우선합니다.waitlist.verification.enabled가 true이고waitlist.verification.require_before_invite가 true인 경우, 확인되지 않은 항목에 대해invite()를 호출하면UnverifiedEntryException이 발생합니다. 사용자 대상 흐름에서는 명시적으로 포착하고, 일반\Throwable아래에 숨기지 마세요.WaitlistInvited알림은 옵트인입니다(waitlist.auto_send_invitation의 기본값은false).laravel-invite-only의 초대 알림은 관계없이 전송됩니다. 대기자 명단 브랜드의 두 번째 이메일을 추가로 보내려는 경우에만auto_send_invitation을 활성화하세요.
확인
Waitlist::sendVerification($entry)를 통해 확인을 트리거하세요. 새 토큰을 생성하고(generateVerificationToken()는 기존 토큰을 덮어씀)VerifyWaitlistEmail을 보냅니다. 자체 토큰 생성 로직을 만들지 말고, 패키지의 확인 경로가 계속 작동하도록 패키지를 사용하세요.Waitlist::verify($token)를 통해 토큰을 확인하세요. 성공 시WaitlistEntry를 반환하고, 알 수 없는 토큰이면null을 반환합니다. 확인 후 토큰은 지워집니다(일회용).waitlist.verification.notification구성을 통해 확인 알림을 사용자 정의하세요. 생성자에서WaitlistEntry를 받습니다.$entry->verification_token에서 토큰을 읽고route('waitlist.verify', ['token' => $entry->verification_token])을 호출하세요.- 패키지의 확인 경로는
waitlist.routes.prefix(기본값waitlist)와waitlist.routes.middleware(기본값['web'])로 마운트됩니다. 자체 컨트롤러를 사용하려면waitlist.routes.enabled => false로 설정하고 작업에서Waitlist::verify($token)을 호출하세요.
상태 및 확인
- 항목 상태는 문자열 리터럴
pending,invited,rejected입니다. 원시 문자열 비교보다는$entry->isPending(),isInvited(),isRejected()를 선호하세요. - 확인 상태는
verified_at및verification_token에 있습니다.isVerified()및isPendingVerification()으로 확인하고, 원시 타임스탬프를 비교하지 마세요. - UI 게이팅을 위해 "확인될 때까지 차단"하려면
isPendingVerification()(토큰 설정, 아직 확인되지 않음)을 사용하세요.isVerified()단독으로는 확인을 시작하지 않은 항목에 대해false를 반환합니다. 이 두 상태는 다릅니다.
Invitable 연결
- 호스트 앱이 특정 엔터티(팀, 조직, 프로젝트)에 사람을 초대하는 경우
config/waitlist.php의invitable아래에 한 번 구성하세요:invitable.model— 클래스 문자열; 패키지는 이에 대해::first()를 호출합니다. 단일 테넌트 앱에서만 사용하세요.invitable.resolver— 다중 테넌트 경우를 위한 클로저fn(WaitlistEntry $entry) => Model|null.$entry->metadata또는 다른 열에서 테넌트 ID를 가져옵니다.invitable.metadata_mapper— 클로저fn(WaitlistEntry $entry) => array로 항목 메타데이터를 초대 메타데이터로 변환합니다(예:['role' => 'beta-tester']).
- 호출 사이트마다 invitable을 하드코딩하지 마세요. 다른 흐름에 다른 invitable이 필요한 경우
metadata에 구별자를 가진resolver클로저를 사용하세요.
이벤트
- 파사드를 래핑하거나 테이블을 폴링하는 대신 패키지 자체 이벤트로 수명 주기에 연결하세요.
OffloadProject\Waitlist\Events에 있으며 각 이벤트는 읽기 전용 속성($event->entry,$event->waitlist)으로 모델을 전달합니다. - 수명 주기 이벤트는 모델에서 발생합니다(
WaitlistEntryAdded는 생성 시$dispatchesEvents를 통해; 나머지는markAsInvited()/markAsRejected()/markAsVerified()에서). 따라서 파사드를 우회하는 코드에도 리스너가 실행됩니다. - 테스트에서는 빈
Event::fake()대신Event::fake([SpecificEvent::class])를 사용하세요. 빈 페이크는WaitlistEntryAdded도 삼켜서 메일링 리스트 리스너가 실행되지 않습니다.
메일링 리스트 동기화
waitlist.mailing_list.enabled로 켜고 드라이버(mailchimp,kit,audienceful,log,array)를 선택하세요.Waitlist::for($slug)->connectMailingList($listId, $driver = null)로 대기자 명단을 목록에 연결하세요. 목록 ID는 Mailchimp 오디언스 ID, Kit 양식 ID(list_type이tag인 경우 태그 ID), 또는 Audienceful publication ID(list_type이tag인 경우 태그 이름)입니다. 자체 목록이 없는 대기자 명단은 드라이버에 구성된list_id로 폴백됩니다.- "가입 시 구독" 리스너를 직접 구축하지 마세요. 구독은 자동이며 확인 설정을 따릅니다.
waitlist.verification.enabled가 꺼져 있으면add()시 항목이 동기화되고, 켜져 있으면Waitlist::verify()후에 동기화됩니다. 이 순서는 의도적입니다. 확인되지 않은 주소가 뉴스레터에 도달해서는 안 됩니다. - 초대 시 태깅, 거부 시 제거, 목록 간 이동 등 구독 이상의 작업을 위해서는 수명 주기 이벤트를 수신하고
MailingList::tagEntry($entry, [...])또는Waitlist::unsubscribeFromMailingList($entry)를 호출하세요. 호스트 앱의 컨트롤러 내부에 이러한 부작용을 추가하지 마세요. - 동기화는 큐 작업(
SyncEntryToMailingList,UnsubscribeEntryFromMailingList)을 통해 실행됩니다. 프로덕션에서는mailing_list.queue.enabled를 켜서 가입이 공급자의 API를 차단하지 않도록 하세요.php artisan waitlist:sync-mailing-list [slug] [--all] [--force]또는Waitlist::for($slug)->syncMailingList()로 기존 행을 백필하세요. - 패키지가 제공하지 않는 서비스를 추가하려면
OffloadProject\Waitlist\Contracts\MailingListDriver를 구현하고 서비스 공급자에서MailingList::extend('name', fn (array $config) => new YourDriver(...))로 등록하세요. 제공된 드라이버를 포크하지 마세요. - 테스트에서는
MailingList::fake()를 사용하세요. 인메모리ArrayDriver로 교체하고 동기화를 인라인으로 실행하며hasSubscriber(),subscribers(),tagsFor()를 노출합니다. 실제 드라이버가 보내는 정확한 요청을 주장해야 할 때만Http::fake()를 사용하세요.
금지 사항
- 직접
update()호출($entry->update(['status' => 'invited']))을 통해 수명 주기 변경을 실행하지 마세요.markAsInvited()/markAsRejected()/markAsVerified()를 사용하여 캐스트, 부작용(타임스탬프, 토큰 지우기) 및 이벤트가 일관되게 유지되도록 하세요. 더 나은 방법은 모든 것을 파사드를 통해 구동하는 것입니다. - 게시된 마이그레이션을 편집하여 열을 추가하지 마세요. 호스트 앱에서 후속 마이그레이션을 작성하세요. 패키지는 향후 릴리스에서 열을 추가할 수 있으며 게시된 스키마를 가정합니다.
Waitlist또는WaitlistEntry를 서브클래싱하지 마세요. 둘 다final입니다. 패키지 이벤트에 대한 리스너를 통해 또는 앱의 컨테이너에서 사용자 정의 바인딩을 통해 서비스를 확장하여 호스트 앱 측에서 동작을 추가하세요.
예제
단일 대기자 명단 (구성 없음)
use OffloadProject\Waitlist\Facades\Waitlist;
$entry = Waitlist::add('John Doe', '[email protected]', ['source' => 'landing-page']);
Waitlist::invite($entry, [
'invited_by' => auth()->user(),
'expires_at' => now()->addDays(14),
]);
여러 대기자 명단
Waitlist::create('Beta Program', 'beta');
Waitlist::create('VIP Access', 'vip');
Waitlist::for('beta')->add('Jane Smith', '[email protected]');
Waitlist::for('vip')->add('Bob Wilson', '[email protected]');
$pendingBeta = Waitlist::for('beta')->getPending();
$vipCount = Waitlist::for('vip')->count();
확인 절차
// config/waitlist.php
'verification' => [
'enabled' => true,
'require_before_invite' => true,
'notification' => \OffloadProject\Waitlist\Notifications\VerifyWaitlistEmail::class,
],
use OffloadProject\Waitlist\Exceptions\UnverifiedEntryException;
use OffloadProject\Waitlist\Facades\Waitlist;
$entry = Waitlist::add('John Doe', '[email protected]');
// 확인 이메일이 자동으로 전송됩니다.
try {
Waitlist::invite($entry);
} catch (UnverifiedEntryException) {
return back()->withErrors(['email' => 'Please verify your email first.']);
}
Invitable 모델 연결 (팀 초대)
// config/waitlist.php
'invitable' => [
'model' => null,
'resolver' => fn (\OffloadProject\Waitlist\Models\WaitlistEntry $entry) =>
\App\Models\Team::find($entry->metadata['team_id'] ?? null),
'metadata_mapper' => fn (\OffloadProject\Waitlist\Models\WaitlistEntry $entry) => [
'role' => $entry->metadata['role'] ?? 'member',
],
],
그 다음:
Waitlist::for('beta')->add('Jane', '[email protected]', [
'team_id' => $team->id,
'role' => 'admin',
]);
나중에 Waitlist::invite($entry)를 호출하면 결과 laravel-invite-only 초대가 role=admin으로 해당 팀에 범위가 지정됩니다.
사용자 정의 확인 알림
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use OffloadProject\Waitlist\Models\WaitlistEntry;
class CustomVerifyWaitlistEmail extends Notification
{
public function __construct(public WaitlistEntry $entry) {}
public function via($notifiable): array
{
return ['mail'];
}
public function toMail($notifiable): MailMessage
{
$url = route('waitlist.verify', ['token' => $this->entry->verification_token]);
return (new MailMessage)
->subject('대기자 명단 자리 확인하기')
->greeting("{$this->entry->name}님, 안녕하세요!")
->action('이메일 확인', $url);
}
}
// config/waitlist.php
'verification' => [
'enabled' => true,
'notification' => \App\Notifications\CustomVerifyWaitlistEmail::class,
],
가입을 Mailchimp에 동기화
// config/waitlist.php
'mailing_list' => [
'enabled' => true,
'default' => 'mailchimp',
'drivers' => [
'mailchimp' => [
'key' => env('MAILCHIMP_API_KEY'), // 접미사는 데이터 센터를 포함합니다 (예: -us14)
'list_id' => env('MAILCHIMP_LIST_ID'),
],
],
],
// 대기자 명단당 하나의 오디언스 (선택 사항, 그렇지 않으면 구성된 list_id가 사용됨)
Waitlist::for('beta')->connectMailingList('a1b2c3d4e5');
// 구성에 따라 추가 시 또는 확인 시 자동으로 구독
Waitlist::for('beta')->add('Jane Smith', '[email protected]');
수명 주기에 반응
use Illuminate\Support\Facades\Event;
use OffloadProject\Waitlist\Events\WaitlistEntryInvited;
use OffloadProject\Waitlist\Events\WaitlistEntryRejected;
use OffloadProject\Waitlist\Facades\MailingList;
use OffloadProject\Waitlist\Facades\Waitlist;
Event::listen(fn (WaitlistEntryInvited $event) => MailingList::tagEntry($event->entry, ['invited']));
Event::listen(fn (WaitlistEntryRejected $event) => Waitlist::unsubscribeFromMailingList($event->entry));
메일링 리스트 흐름 테스트
use OffloadProject\Waitlist\Facades\MailingList;
MailingList::fake();
Waitlist::for('beta')->add('Test User', '[email protected]');
MailingList::assertHasSubscriber('[email protected]');