TextBuilder란
TextBuilder는 Bluesky의 AT Protocol이 정의하는 facets(리치 텍스트 주석)를 메서드 체인으로 조립하기 위한 클래스입니다.
Bluesky의 게시 본문은 플레인 텍스트이지만, 멘션·링크·해시태그를 표시하기 위해서는 텍스트 내의 위치(바이트 오프셋)와 종류를 나타내는 facets 배열을 함께 전송해야 합니다. TextBuilder는 이 오프셋 계산과 배열 구축을 자동으로 수행합니다.
기본적인 텍스트 생성
TextBuilder::make()
TextBuilder::make()로 초기 텍스트를 지정해 인스턴스를 생성합니다. 초기 텍스트는 생략할 수 있습니다.
use Revolution\Bluesky\RichText\TextBuilder;
$builder = TextBuilder::make(text: 'Hello Bluesky');
text()
text()로 텍스트를 끝에 추가합니다.
$builder = TextBuilder::make()
->text('Hello ')
->text('Bluesky');
// $builder->text === 'Hello Bluesky'
newLine()
줄바꿈을 추가합니다. count로 줄 수를 지정할 수 있습니다(기본값: 1).
$builder = TextBuilder::make('1행')
->newLine()
->text('2행')
->newLine(count: 2)
->text('4행');
toPost()
TextBuilder 인스턴스를 Post 레코드로 변환합니다. Bluesky::post()에 바로 전달할 수 있습니다.
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\RichText\TextBuilder;
$post = TextBuilder::make('단순한 게시')->toPost();
$response = Bluesky::withToken()->post($post);
Post::build()
Post::build()에 클로저를 전달하는 방법도 사용할 수 있습니다. 반환값은 Post입니다.
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을 추가합니다.
$builder->mention(text: '@alice.bsky.social');
DID의 자동 해결
did를 생략하면 핸들에서 DID를 자동으로 해결합니다(Bluesky::resolveHandle()이 호출됩니다).
// DID를 자동 해결(API 호출이 발생합니다)
$builder->mention('@alice.bsky.social');
DID를 명시적으로 지정
DID를 이미 알고 있는 경우에는 명시적으로 전달하여 API 호출을 회피할 수 있습니다.
// DID를 직접 지정(권장: API 호출 없음)
$builder->mention(text: '@alice.bsky.social', did: 'did:plc:xxxxxxxxxxxxxxxxxxxx');
프로덕션 환경에서 멘션을 많이 사용하는 경우에는 DID를 캐시해 두면 API 호출을 줄일 수 있습니다.
링크(URL) 임베드
link()로 링크 facet을 추가합니다.
// URL 자체를 표시 텍스트로 사용
$builder->link('https://laravel.com');
// 표시 텍스트와 URL을 각각 지정
$builder->link(text: 'Laravel 공식 사이트', uri: 'https://laravel.com');
uri를 생략하면 text가 그대로 URI로 사용됩니다.
해시태그 추가
tag()로 해시태그 facet을 추가합니다.
// #로 시작하는 텍스트를 전달하면 태그가 자동 추출됨
$builder->tag('#Laravel');
// 태그 문자열을 명시적으로 지정(# 없는 문자열)
$builder->tag(text: '#Laravel', tag: 'Laravel');
복합 텍스트 구축
여러 facet을 조합해 리치한 게시 텍스트를 만들 수 있습니다.
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()를 사용한 작성
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()와의 조합
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()를 사용할 수 있습니다.
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을 설정할 수도 있습니다.
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();
detectFacets()는 정규 표현식 기반의 검출입니다. 확실히 링크하고 싶은 경우에는 link()·mention()·tag()를 명시적으로 사용하는 편이 확실합니다.
커스텀 facet 추가
facet() 메서드로 임의의 facet 배열을 직접 추가할 수 있습니다.
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()을 사용해 바이트 수를 계산합니다.
한국어·이모지 등 멀티바이트 문자는 한 글자라도 여러 바이트를 소비하므로, 글자 수가 아니라 바이트 수로 오프셋이 결정됩니다.
// 'Hello'는 UTF-8에서 5바이트(1글자 = 1바이트)
// '한국어'는 UTF-8에서 9바이트(1글자 = 3바이트)
$builder = TextBuilder::make('한국어');
// strlen($builder->text) === 9
게시의 문자 수 제한
Bluesky의 게시는 grapheme(표시상의 글자 수)로 최대 300자입니다. 바이트 수가 아닌 grapheme로 제한되므로, 한국어에서도 300자를 쓸 수 있습니다.
use Illuminate\Support\Str;
$text = '이것이 게시 텍스트입니다.';
// grapheme로 문자 수 확인
$length = Str::length($text); // mb_strlen()과 동등
TextBuilder 자체는 문자 수 체크를 하지 않습니다. 300 grapheme를 초과하는 게시를 보내면 AT Protocol API가 에러를 반환합니다.
메서드 목록
| 메서드 | 설명 |
|---|
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' => ...] 배열로 변환 |