> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# TextBuilder

> Bluesky의 리치 텍스트(facets)를 PHP로 구축하기 위한 TextBuilder 클래스의 상세한 사용법.

## TextBuilder란

`TextBuilder`는 Bluesky의 AT Protocol이 정의하는 **facets**(리치 텍스트 주석)를 메서드 체인으로 조립하기 위한 클래스입니다.

Bluesky의 게시 본문은 플레인 텍스트이지만, 멘션·링크·해시태그를 표시하기 위해서는 텍스트 내의 위치(바이트 오프셋)와 종류를 나타내는 `facets` 배열을 함께 전송해야 합니다. `TextBuilder`는 이 오프셋 계산과 배열 구축을 자동으로 수행합니다.

```mermaid theme={null}
sequenceDiagram
    participant App as Laravel 앱
    participant TB as TextBuilder
    participant Post as Post 레코드
    participant AT as AT Protocol

    App->>TB: TextBuilder::make('Hello')
    App->>TB: ->mention('@alice.bsky.social')
    App->>TB: ->link('https://example.com')
    App->>TB: ->tag('#Laravel')
    TB->>TB: 바이트 오프셋을 계산하여<br>facets 배열을 구축
    TB->>Post: toPost()
    Post->>AT: Bluesky::post($post)
    AT-->>App: 응답
```

## 기본적인 텍스트 생성

### TextBuilder::make()

`TextBuilder::make()`로 초기 텍스트를 지정해 인스턴스를 생성합니다. 초기 텍스트는 생략할 수 있습니다.

```php theme={null}
use Revolution\Bluesky\RichText\TextBuilder;

$builder = TextBuilder::make(text: 'Hello Bluesky');
```

### text()

`text()`로 텍스트를 끝에 추가합니다.

```php theme={null}
$builder = TextBuilder::make()
    ->text('Hello ')
    ->text('Bluesky');

// $builder->text === 'Hello Bluesky'
```

### newLine()

줄바꿈을 추가합니다. `count`로 줄 수를 지정할 수 있습니다(기본값: 1).

```php theme={null}
$builder = TextBuilder::make('1행')
    ->newLine()
    ->text('2행')
    ->newLine(count: 2)
    ->text('4행');
```

### toPost()

`TextBuilder` 인스턴스를 `Post` 레코드로 변환합니다. `Bluesky::post()`에 바로 전달할 수 있습니다.

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\RichText\TextBuilder;

$post = TextBuilder::make('단순한 게시')->toPost();

$response = Bluesky::withToken()->post($post);
```

### Post::build()

`Post::build()`에 클로저를 전달하는 방법도 사용할 수 있습니다. 반환값은 `Post`입니다.

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\RichText\TextBuilder;

$post = Post::build(function (TextBuilder $builder) {
    $builder->text('Hello Bluesky');
});

$response = Bluesky::withToken()->post($post);
```

## 멘션(`@mention`) 추가

`mention()`으로 멘션 facet을 추가합니다.

```php theme={null}
$builder->mention(text: '@alice.bsky.social');
```

### DID의 자동 해결

`did`를 생략하면 핸들에서 DID를 자동으로 해결합니다(`Bluesky::resolveHandle()`이 호출됩니다).

```php theme={null}
// DID를 자동 해결(API 호출이 발생합니다)
$builder->mention('@alice.bsky.social');
```

### DID를 명시적으로 지정

DID를 이미 알고 있는 경우에는 명시적으로 전달하여 API 호출을 회피할 수 있습니다.

```php theme={null}
// DID를 직접 지정(권장: API 호출 없음)
$builder->mention(text: '@alice.bsky.social', did: 'did:plc:xxxxxxxxxxxxxxxxxxxx');
```

<Tip>
  프로덕션 환경에서 멘션을 많이 사용하는 경우에는 DID를 캐시해 두면 API 호출을 줄일 수 있습니다.
</Tip>

## 링크(URL) 임베드

`link()`로 링크 facet을 추가합니다.

```php theme={null}
// URL 자체를 표시 텍스트로 사용
$builder->link('https://laravel.com');

// 표시 텍스트와 URL을 각각 지정
$builder->link(text: 'Laravel 공식 사이트', uri: 'https://laravel.com');
```

`uri`를 생략하면 `text`가 그대로 URI로 사용됩니다.

## 해시태그 추가

`tag()`로 해시태그 facet을 추가합니다.

```php theme={null}
// #로 시작하는 텍스트를 전달하면 태그가 자동 추출됨
$builder->tag('#Laravel');

// 태그 문자열을 명시적으로 지정(# 없는 문자열)
$builder->tag(text: '#Laravel', tag: 'Laravel');
```

## 복합 텍스트 구축

여러 facet을 조합해 리치한 게시 텍스트를 만들 수 있습니다.

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\RichText\TextBuilder;

$post = TextBuilder::make('새로운 기사를 공개했습니다!')
    ->newLine(count: 2)
    ->mention('@alice.bsky.social', did: 'did:plc:xxxx')
    ->text(' 님도 꼭 읽어봐 주세요.')
    ->newLine()
    ->link(text: '기사 읽기', uri: 'https://example.com/article/1')
    ->newLine()
    ->tag('#Laravel')
    ->text(' ')
    ->tag('#PHP')
    ->toPost();

$response = Bluesky::withToken()->post($post);
```

### Post::build()를 사용한 작성

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\RichText\TextBuilder;

$post = Post::build(function (TextBuilder $builder) {
    $builder->text('새로운 기사를 공개했습니다!')
            ->newLine(count: 2)
            ->mention('@alice.bsky.social', did: 'did:plc:xxxx')
            ->text(' 님도 꼭 읽어봐 주세요.')
            ->newLine()
            ->link(text: '기사 읽기', uri: 'https://example.com/article/1')
            ->newLine()
            ->tag('#Laravel')
            ->text(' ')
            ->tag('#PHP');
});

$response = Bluesky::withToken()->post($post);
```

## 게시와의 통합

### Bluesky::post()와의 조합

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\RichText\TextBuilder;

$post = TextBuilder::make('test')
    ->newLine()
    ->link('https://bsky.app/')
    ->toPost();

$response = Bluesky::withToken()->post($post);
```

### Notification 채널과의 조합

`BlueskyChannel`의 `toBluesky()` 메서드에서 `Post::build()`를 사용할 수 있습니다.

```php theme={null}
use Illuminate\Notifications\Notification;
use Revolution\Bluesky\Notifications\BlueskyChannel;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\RichText\TextBuilder;

class DeployedNotification extends Notification
{
    public function __construct(
        private string $url,
    ) {}

    public function via(object $notifiable): array
    {
        return [BlueskyChannel::class];
    }

    public function toBluesky(object $notifiable): Post
    {
        return Post::build(function (TextBuilder $builder) {
            $builder->text('배포가 완료되었습니다')
                    ->newLine()
                    ->link(text: '확인하기', uri: $this->url)
                    ->newLine()
                    ->tag('#Laravel');
        });
    }
}
```

## facets의 자동 검출

텍스트 내의 `@mention`·URL·`#hashtag`를 자동 검출해 facets을 설정할 수도 있습니다.

```php theme={null}
use Revolution\Bluesky\RichText\TextBuilder;

$builder = TextBuilder::make('@alice.bsky.social test https://example.com #alice')
    ->detectFacets();

// detectFacets() 이후에 facets을 추가하는 것도 가능
$builder->newLine()->tag('#bob');

$post = $builder->toPost();
```

<Warning>
  `detectFacets()`는 정규 표현식 기반의 검출입니다. 확실히 링크하고 싶은 경우에는 `link()`·`mention()`·`tag()`를 명시적으로 사용하는 편이 확실합니다.
</Warning>

## 커스텀 facet 추가

`facet()` 메서드로 임의의 facet 배열을 직접 추가할 수 있습니다.

```php theme={null}
use Revolution\Bluesky\RichText\TextBuilder;

$builder = TextBuilder::make();

$builder->facet([
    'index' => [
        'byteStart' => 0,
        'byteEnd' => 5,
    ],
    'features' => [
        [
            '$type' => 'app.bsky.richtext.facet#link',
            'uri' => 'https://example.com',
        ],
    ],
]);
```

## 문자 수 제한과 주의 사항

### 바이트 오프셋과 grapheme

AT Protocol의 facet 인덱스는 **UTF-8 바이트 오프셋**으로 지정합니다. `TextBuilder`는 내부에서 `strlen()`을 사용해 바이트 수를 계산합니다.

한국어·이모지 등 멀티바이트 문자는 한 글자라도 여러 바이트를 소비하므로, **글자 수가 아니라 바이트 수**로 오프셋이 결정됩니다.

```php theme={null}
// 'Hello'는 UTF-8에서 5바이트(1글자 = 1바이트)
// '한국어'는 UTF-8에서 9바이트(1글자 = 3바이트)
$builder = TextBuilder::make('한국어');
// strlen($builder->text) === 9
```

### 게시의 문자 수 제한

Bluesky의 게시는 **grapheme(표시상의 글자 수)로 최대 300자**입니다. 바이트 수가 아닌 grapheme로 제한되므로, 한국어에서도 300자를 쓸 수 있습니다.

```php theme={null}
use Illuminate\Support\Str;

$text = '이것이 게시 텍스트입니다.';

// grapheme로 문자 수 확인
$length = Str::length($text); // mb_strlen()과 동등
```

<Info>
  `TextBuilder` 자체는 문자 수 체크를 하지 않습니다. 300 grapheme를 초과하는 게시를 보내면 AT Protocol API가 에러를 반환합니다.
</Info>

## 메서드 목록

| 메서드                                          | 설명                                        |
| -------------------------------------------- | ----------------------------------------- |
| `TextBuilder::make(string $text = '')`       | 인스턴스를 생성                                  |
| `text(string $text)`                         | 텍스트를 끝에 추가                                |
| `newLine(int $count = 1)`                    | 줄바꿈을 추가                                   |
| `mention(string $text, ?string $did = null)` | 멘션 facet을 추가                              |
| `link(string $text, ?string $uri = null)`    | 링크 facet을 추가                              |
| `tag(string $text, ?string $tag = null)`     | 해시태그 facet을 추가                            |
| `detectFacets()`                             | 텍스트로부터 facets을 자동 검출                      |
| `facet(array $facet)`                        | 커스텀 facet을 추가                             |
| `resetFacets()`                              | 모든 facets을 리셋                             |
| `toPost()`                                   | `Post` 레코드로 변환                            |
| `toArray()`                                  | `['text' => ..., 'facets' => ...]` 배열로 변환 |

<Info>
  Source: [src/RichText/TextBuilder.php](https://github.com/invokable/laravel-bluesky/blob/main/src/RichText/TextBuilder.php)
</Info>


## Related topics

- [Basic client - Laravel Bluesky](/ko/packages/laravel-bluesky/basic-client.md)
- [Laravel Bluesky](/ko/packages/laravel-bluesky/index.md)
- [알림 채널 - Laravel Bluesky](/ko/packages/laravel-bluesky/notification.md)
- [Eloquent의 스코프](/ko/advanced/eloquent-scopes.md)
- [Get accent phrases from text](/ko/packages/laravel-voicevox/engine-api/query-editing/get-accent-phrases-from-text.md)
