
# 반복

Queuety 워크플로는 순서가 있는 단계 체인입니다. 반복 제어 단계를 사용하면 `_next_step` 로직을 직접 작성하지 않고 이전에 이름을 지정한 단계로 돌아갈 수 있습니다.

워크플로에 맞는 형식을 선택하세요:

- 조건이 간단한 경우 공개 `state_key`
- 반복에 더 복잡한 로직이 필요한 경우 `condition_class`
- 반복 자체에 명확한 중단 조건이 필요한 경우 `max_iterations`

두 방법 모두 이전에 이름을 지정한 단계를 대상으로 하므로, 반복은 직렬화 및 재생이 가능하며 다른 모든 단계와 동일한 워크플로 런타임의 보호를 받습니다.

반복 조건은 앞선 단계가 완료된 뒤 저장된 공개 워크플로 상태를 기준으로 평가되므로, 작업자 프로세스가 다시 시작되어도 모든 반복이 지속됩니다.

## 상태 키를 사용하는 `repeat_until()`

```php
Queuety::workflow( 'approval_revision_loop' )
    ->max_transitions( 12 )
    ->then( DraftBriefStep::class, 'draft' )
    ->wait_for_decision( result_key: 'review' )
    ->then( NormalizeReviewOutcomeStep::class, 'review_outcome' )
    ->repeat_until( 'draft', 'review_approved', true, 'repeat_draft_until_approved' )
    ->then( PublishBriefStep::class )
    ->dispatch( [ 'brief_id' => 42 ] );
```

`NormalizeReviewOutcomeStep`은 판단 페이로드를 간단한 공개 키로 변환할 수 있습니다:

```php
class NormalizeReviewOutcomeStep implements \Queuety\Step {
    public function handle( array $state ): array {
        return [
            'review_approved' => 'approved' === ( $state['review']['outcome'] ?? null ),
        ];
    }

    public function config(): array {
        return [];
    }
}
```

`review_approved`이 여전히 `true`이 아니면 반복 단계는 `draft`로 돌아갑니다. `true`이 되면 워크플로는 `PublishBriefStep`로 넘어갑니다.

신호는 소비되지 않고 저장됩니다. `wait_for_decision()`을 두 번째로 통과할 때 이전 `rejected` 신호가 여전히 일치하므로 대기가 즉시 끝나고, 그 결과 루프가 `max_transitions()`이 멈출 때까지 계속 돌 수 있습니다. 각 반복에 고유한 키를 부여하세요. `DraftBriefStep`이 증가하는 `review_round`를 반환하게 하고, `correlation_key: 'review_round'`를 `wait_for_decision()`에 전달한 뒤 검토자가 신호 페이로드에 동일한 `review_round`을 보내게 하세요. 그러면 `review_round`이 현재 상태 값과 일치하는 신호만 대기를 끝냅니다.

## 조건 클래스를 사용하는 `repeat_until()`

```php
use App\Workflows\ReviewApprovedCondition;

Queuety::workflow( 'approval_revision_loop' )
    ->then( DraftBriefStep::class, 'draft' )
    ->wait_for_decision( result_key: 'review' )
    ->repeat_until(
        target_step: 'draft',
        condition_class: ReviewApprovedCondition::class,
        max_iterations: 5,
        name: 'repeat_draft_until_approved',
    )
    ->then( PublishBriefStep::class )
    ->dispatch( [ 'brief_id' => 42 ] );
```

```php
namespace App\Workflows;

use Queuety\Contracts\RepeatCondition;

final class ReviewApprovedCondition implements RepeatCondition {
    public function matches( array $state ): bool {
        return 'approved' === ( $state['review']['outcome'] ?? null );
    }
}
```

조건 클래스는 공개 워크플로 상태를 받아 클로저에 의존하지 않고도 조건식을 직렬화하고 검사할 수 있게 합니다.

## `repeat_while()`

```php
Queuety::workflow( 'poll_remote_status' )
    ->max_transitions( 20 )
    ->then( PollRemoteStatusStep::class, 'poll' )
    ->delay( seconds: 30 )
    ->repeat_while( 'poll', 'should_poll_again', true, 'repeat_poll' )
    ->then( FinalizeImportStep::class )
    ->dispatch( [ 'import_id' => 99 ] );
```

이 패턴은 단계가 `should_poll_again`, `has_more_pages` 또는 `needs_revision` 같은 간단한 불리언 값을 기록할 때 유용합니다.

## 반복은 왜 `_next_step`과 별도로 제공되나요

일반 단계에서 `_next_step`을 반환해 수동으로 분기할 수도 있습니다. 반복 도우미는 다음과 같은 제한된 경우를 위한 기능입니다:

- 대상이 이전 단계인 경우
- 조건이 공개 워크플로 상태에서 오는 경우
- 반복 로직을 워크플로 정의 자체에 드러내고 싶은 경우

따라서 반복을 임의의 단계 코드 안에 숨기는 것보다 내보내기 파일, 이벤트 로그, 문서에서 검사하기 쉽습니다.

## 반복 보호

특정 반복 하나에만 중단 조건을 두려면 `max_iterations`을 사용하세요:

```php
Queuety::workflow( 'poll_remote_status' )
    ->then( PollRemoteStatusStep::class, 'poll' )
    ->repeat_while( 'poll', 'should_poll_again', true, max_iterations: 8 )
    ->dispatch();
```

워크플로 전체에 더 넓은 범위의 비상 중단 장치가 필요하면 `max_transitions()`을 사용하세요:

```php
Queuety::workflow( 'safe_revision_loop' )
    ->max_transitions( 10 )
    ->then( DraftBriefStep::class, 'draft' )
    ->wait_for_decision( result_key: 'review' )
    ->then( NormalizeReviewOutcomeStep::class )
    ->repeat_until( 'draft', 'review_approved', true )
    ->dispatch();
```

반복이 `max_iterations`을 초과하거나 워크플로가 `max_transitions()`을 초과하면, Queuety가 무한 반복을 허용하지 않고 실행을 실패 처리합니다.

## 관련 문서

- [조건 분기](https://queuety.inline0.dev/ko/docs/workflows/conditional)
- [신호](https://queuety.inline0.dev/ko/docs/workflows/signals)
- [안전장치](https://queuety.inline0.dev/ko/docs/workflows/guardrails)
- [PHP API](https://queuety.inline0.dev/ko/docs/api)
