Community생산성 & 협업github.com

offload-project/laravel-waitlist

다수의 대기자 명단, 항목 상태 추적, 선택적 이메일 확인, 수명 주기 이벤트, 메일링 리스트 동기화(Mailchimp/Kit/Audienceful) 및 laravel-invite-only와의 연결을 위한 패키지 규칙 및 API.

laravel-waitlist란 무엇인가요?

이 스킬은 `offload-project/laravel-waitlist` 패키지를 사용하는 Laravel 애플리케이션에서 개발자가 대기자 명단을 효율적으로 관리할 수 있도록 돕습니다. 이 패키지는 여러 대기자 명단 생성, 항목 추가/초대/거부, 이메일 인증, 수명 주기 이벤트 처리, Mailchimp, Kit(ConvertKit), Audienceful과 같은 외부 서비스와의 메일링 리스트 동기화를 지원합니다. 개발자는 `Waitlist` 파사드를 통해 `WaitlistService`에 접근하며, 특정 대기자 명단을 `for()` 메서드로 지정하고 `add()`, `invite()`, `verify()` 등의 메서드를 사용하여 항목을 관리할 수 있습니다. 또한 `laravel-invite-only` 패키지와의 연결을 통해 초대 토큰 생성 및 관리를 자동화하고, `MailingList` 파사드를 통해 구독/구독 취소를 큐 작업으로 처리하여 성능을 최적화합니다. 이 스킬은 대기자 명단 관련 기능 구현 시 올바른 파사드 사용, 이벤트 리스닝, 메일링 리스트 설정, 테스트 전략 등 모범 사례를 제공합니다.

지원 대상✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/offload-project/laravel-waitlist/tree/HEAD/skills

Installed? Explore more 생산성 & 협업 skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

컨텍스트

offload-project/laravel-waitlist은 Laravel 11/12/13 패키지(PHP 8.3+)로, 하나 이상의 대기자 명단을 관리합니다. 제공 기능:

  • Waitlist Eloquent 모델(이름 및 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 파사드, 또는 이 패키지의 대기자 명단 흐름에 대한 도움을 요청할 때 적용하세요.

규칙

파사드 사용

  1. Waitlist 파사드(OffloadProject\Waitlist\Facades\Waitlist)를 사용하세요. WaitlistService를 직접 인스턴스화하지 마십시오. 파사드가 지원되는 진입점입니다.
  2. 특정 대기자 명단을 대상으로 하려면 Waitlist::for($slugOrIdOrModel)->...를 체이닝하세요. for(...) 없이 호출하면 기본 대기자 명단에서 작동합니다(처음 사용 시 getDefault()를 통해 자동 생성됨).
  3. Waitlist::for(...)는 싱글톤 서비스의 내부 상태를 변경합니다. 장기 실행 프로세스(큐 워커, Octane)의 경우, 이전 컨텍스트 호출이 유지된다고 가정하지 말고 모든 작업에 대해 for(...)를 호출하세요.

대기자 명단 vs. 항목

  1. Waitlist::create(string $name, string $slug, ?string $description = null, bool $isActive = true)로 대기자 명단을 생성하세요. slug는 정식 식별자이며, for(...) 및 find(...)에서 기대하는 값입니다.
  2. Waitlist::for($slug)->add($name, $email, $metadata = [])로 항목을 추가하세요. 확인 절차를 실행하려면 WaitlistEntry::create([...])를 직접 호출하지 마세요. add()는 waitlist.verification.enabled가 true일 때 자동으로 sendVerification()을 트리거합니다.
  3. waitlist_entries 테이블의 고유 제약 조건은 ['waitlist_id', 'email']이며, email만이 아닙니다. 동일한 사람이 여러 대기자 명단에 가입할 수 있습니다.

초대

  1. Waitlist::invite($entryOrId, $options = [])를 통해 초대하세요. 알림 및 Invitation 레코드가 필요할 때 $entry->markAsInvited()를 단독으로 호출하지 마세요. invite()는 laravel-invite-only Invitation을 생성하고 invitation_id를 통해 연결하며 항목을 초대됨으로 표시합니다.
  2. $options는 InviteOnly::invite(...)로 전달됩니다. 일반적인 키: 'invited_by' (모델 또는 int, 기본값 auth()->user()), 'role', 'metadata', 'expires_at'. waitlist.invitable.metadata_mapper에서 설정한 키를 중복하지 마세요. 명시적인 $options가 array_merge를 통해 우선합니다.
  3. waitlist.verification.enabled가 true이고 waitlist.verification.require_before_invite가 true인 경우, 확인되지 않은 항목에 대해 invite()를 호출하면 UnverifiedEntryException이 발생합니다. 사용자 대상 흐름에서는 명시적으로 포착하고, 일반 \Throwable 아래에 숨기지 마세요.
  4. WaitlistInvited 알림은 옵트인입니다(waitlist.auto_send_invitation의 기본값은 false). laravel-invite-only의 초대 알림은 관계없이 전송됩니다. 대기자 명단 브랜드의 두 번째 이메일을 추가로 보내려는 경우에만 auto_send_invitation을 활성화하세요.

확인

  1. Waitlist::sendVerification($entry)를 통해 확인을 트리거하세요. 새 토큰을 생성하고(generateVerificationToken()는 기존 토큰을 덮어씀) VerifyWaitlistEmail을 보냅니다. 자체 토큰 생성 로직을 만들지 말고, 패키지의 확인 경로가 계속 작동하도록 패키지를 사용하세요.
  2. Waitlist::verify($token)를 통해 토큰을 확인하세요. 성공 시 WaitlistEntry를 반환하고, 알 수 없는 토큰이면 null을 반환합니다. 확인 후 토큰은 지워집니다(일회용).
  3. waitlist.verification.notification 구성을 통해 확인 알림을 사용자 정의하세요. 생성자에서 WaitlistEntry를 받습니다. $entry->verification_token에서 토큰을 읽고 route('waitlist.verify', ['token' => $entry->verification_token])을 호출하세요.
  4. 패키지의 확인 경로는 waitlist.routes.prefix(기본값 waitlist)와 waitlist.routes.middleware(기본값 ['web'])로 마운트됩니다. 자체 컨트롤러를 사용하려면 waitlist.routes.enabled => false로 설정하고 작업에서 Waitlist::verify($token)을 호출하세요.

상태 및 확인

  1. 항목 상태는 문자열 리터럴 pending, invited, rejected입니다. 원시 문자열 비교보다는 $entry->isPending(), isInvited(), isRejected()를 선호하세요.
  2. 확인 상태는 verified_at 및 verification_token에 있습니다. isVerified() 및 isPendingVerification()으로 확인하고, 원시 타임스탬프를 비교하지 마세요.
  3. UI 게이팅을 위해 "확인될 때까지 차단"하려면 isPendingVerification()(토큰 설정, 아직 확인되지 않음)을 사용하세요. isVerified() 단독으로는 확인을 시작하지 않은 항목에 대해 false를 반환합니다. 이 두 상태는 다릅니다.

Invitable 연결

  1. 호스트 앱이 특정 엔터티(팀, 조직, 프로젝트)에 사람을 초대하는 경우 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']).
  2. 호출 사이트마다 invitable을 하드코딩하지 마세요. 다른 흐름에 다른 invitable이 필요한 경우 metadata에 구별자를 가진 resolver 클로저를 사용하세요.

이벤트

  1. 파사드를 래핑하거나 테이블을 폴링하는 대신 패키지 자체 이벤트로 수명 주기에 연결하세요. OffloadProject\Waitlist\Events에 있으며 각 이벤트는 읽기 전용 속성($event->entry, $event->waitlist)으로 모델을 전달합니다.
  2. 수명 주기 이벤트는 모델에서 발생합니다(WaitlistEntryAdded는 생성 시 $dispatchesEvents를 통해; 나머지는 markAsInvited() / markAsRejected() / markAsVerified()에서). 따라서 파사드를 우회하는 코드에도 리스너가 실행됩니다.
  3. 테스트에서는 빈 Event::fake() 대신 Event::fake([SpecificEvent::class])를 사용하세요. 빈 페이크는 WaitlistEntryAdded도 삼켜서 메일링 리스트 리스너가 실행되지 않습니다.

메일링 리스트 동기화

  1. 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로 폴백됩니다.
  2. "가입 시 구독" 리스너를 직접 구축하지 마세요. 구독은 자동이며 확인 설정을 따릅니다. waitlist.verification.enabled가 꺼져 있으면 add() 시 항목이 동기화되고, 켜져 있으면 Waitlist::verify() 후에 동기화됩니다. 이 순서는 의도적입니다. 확인되지 않은 주소가 뉴스레터에 도달해서는 안 됩니다.
  3. 초대 시 태깅, 거부 시 제거, 목록 간 이동 등 구독 이상의 작업을 위해서는 수명 주기 이벤트를 수신하고 MailingList::tagEntry($entry, [...]) 또는 Waitlist::unsubscribeFromMailingList($entry)를 호출하세요. 호스트 앱의 컨트롤러 내부에 이러한 부작용을 추가하지 마세요.
  4. 동기화는 큐 작업(SyncEntryToMailingList, UnsubscribeEntryFromMailingList)을 통해 실행됩니다. 프로덕션에서는 mailing_list.queue.enabled를 켜서 가입이 공급자의 API를 차단하지 않도록 하세요. php artisan waitlist:sync-mailing-list [slug] [--all] [--force] 또는 Waitlist::for($slug)->syncMailingList()로 기존 행을 백필하세요.
  5. 패키지가 제공하지 않는 서비스를 추가하려면 OffloadProject\Waitlist\Contracts\MailingListDriver를 구현하고 서비스 공급자에서 MailingList::extend('name', fn (array $config) => new YourDriver(...))로 등록하세요. 제공된 드라이버를 포크하지 마세요.
  6. 테스트에서는 MailingList::fake()를 사용하세요. 인메모리 ArrayDriver로 교체하고 동기화를 인라인으로 실행하며 hasSubscriber(), subscribers(), tagsFor()를 노출합니다. 실제 드라이버가 보내는 정확한 요청을 주장해야 할 때만 Http::fake()를 사용하세요.

금지 사항

  1. 직접 update() 호출($entry->update(['status' => 'invited']))을 통해 수명 주기 변경을 실행하지 마세요. markAsInvited() / markAsRejected() / markAsVerified()를 사용하여 캐스트, 부작용(타임스탬프, 토큰 지우기) 및 이벤트가 일관되게 유지되도록 하세요. 더 나은 방법은 모든 것을 파사드를 통해 구동하는 것입니다.
  2. 게시된 마이그레이션을 편집하여 열을 추가하지 마세요. 호스트 앱에서 후속 마이그레이션을 작성하세요. 패키지는 향후 릴리스에서 열을 추가할 수 있으며 게시된 스키마를 가정합니다.
  3. 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]');

관련 스킬