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

# BlueskyManager와 HasShortHand

> Bluesky Facade의 실체인 BlueskyManager 아키텍처와 HasShortHand 트레이트가 제공하는 단축 메서드를 설명합니다.

## 개요

`Bluesky` Facade는 `BlueskyManager`의 얇은 래퍼입니다. `BlueskyManager`는 여러 Agent를 관리하며, `HasShortHand` 트레이트가 자주 사용하는 메서드의 단축키를 한데 모아 제공합니다.

## 아키텍처

```mermaid theme={null}
graph TD
    A["Bluesky Facade<br>(Revolution\\Bluesky\\Facades\\Bluesky)"] --> B["BlueskyManager<br>(Factory 인터페이스)"]
    B --> C["HasShortHand 트레이트<br>단축 메서드군"]
    B --> D["Conditionable 트레이트<br>when() / unless()"]
    B --> E["Macroable 트레이트<br>macro() / mixin()"]
    B --> F{"Agent"}
    F --> G["LegacyAgent<br>(App Password)"]
    F --> H["OAuthAgent<br>(OAuth)"]
    G --> I["LegacySession"]
    H --> J["OAuthSession"]
```

`BlueskyServiceProvider`는 `Factory::class`를 `BlueskyManager`로서 스코프 싱글톤(요청당 1개 인스턴스)으로 등록합니다.

```php theme={null}
// BlueskyServiceProvider::register()
$this->app->scoped(Factory::class, BlueskyManager::class);
```

Facade의 `getFacadeAccessor()`가 이 바인딩을 해결합니다.

```php theme={null}
// Facades/Bluesky.php
protected static function getFacadeAccessor(): string
{
    return Factory::class;
}
```

## BlueskyManager의 코어 메서드

`BlueskyManager` 자체가 정의하는 코어 메서드는 다음과 같습니다. `HasShortHand` 트레이트의 메서드와 구분해서 파악해 두면 내부 구조 이해에 도움이 됩니다.

### 인증

| 메서드                                                                    | 설명                                                |
| ---------------------------------------------------------------------- | ------------------------------------------------- |
| `login(string $identifier, string $password, ?string $service = null)` | App Password로 인증하고 `LegacyAgent`를 설정              |
| `withToken(?AbstractSession $token)`                                   | `OAuthSession` 또는 `LegacySession`을 전달하여 Agent를 설정 |
| `check(): bool`                                                        | 인증되었는지 확인(토큰 유효 기간도 검증)                           |
| `refreshSession()`                                                     | 토큰을 리프레시                                          |
| `logout()`                                                             | Agent를 초기화                                        |

### Agent 조작

| 메서드                        | 설명                           |
| -------------------------- | ---------------------------- |
| `agent(): ?Agent`          | 현재의 Agent를 취득                |
| `withAgent(?Agent $agent)` | Agent를 직접 설정                 |
| `assertDid(): string`      | 인증된 DID를 반환. 미인증이면 예외를 throw |

### HTTP 클라이언트

| 메서드                                                                                              | 설명                            |
| ------------------------------------------------------------------------------------------------ | ----------------------------- |
| `client(bool $auth = true): AtpClient`                                                           | 인증된(또는 익명의) XRPC 클라이언트를 반환    |
| `public(): BskyClient`                                                                           | 인증이 필요 없는 퍼블릭 엔드포인트 클라이언트를 반환 |
| `send(BackedEnum\|string $api, string $method, bool $auth, ?array $params, ?callable $callback)` | 임의의 AT Protocol API를 직접 호출    |

### 유틸리티

| 메서드                               | 설명                           |
| --------------------------------- | ---------------------------- |
| `identity(): Identity`            | DID 해결 등 아이덴티티 서비스를 취득       |
| `pds(): PDS`                      | PDS 정보 서비스를 취득               |
| `entryway(?string $path): string` | 서비스 URL(`bsky.social` 등)을 반환 |
| `publicEndpoint(): string`        | 퍼블릭 엔드포인트 URL을 반환            |

## HasShortHand 트레이트

`HasShortHand` 트레이트는 AT Protocol의 저수준 API를 PHP에 알맞은 이해하기 쉬운 메서드로 래핑합니다. `BlueskyManager`에 `use HasShortHand;`라고 기재하기만 하면, 이 메서드들을 `Bluesky::`에서 직접 호출할 수 있습니다.

```php theme={null}
// BlueskyManager.php
class BlueskyManager implements Factory
{
    use Conditionable;
    use HasShortHand;
    use Macroable;
    // ...
}
```

<Info>
  `HasShortHand`를 독립된 트레이트로 만든 이유는 테스트·커스터마이즈의 용이성 때문입니다. 이 트레이트를 사용하면 `BlueskyManager`의 코드를 단순하게 유지하면서도 풍부한 API 단축키를 제공할 수 있습니다.
</Info>

### 게시·피드

| 메서드                                                             | 설명             |
| --------------------------------------------------------------- | -------------- |
| `post(Post\|string\|array $text)`                               | 신규 게시를 작성      |
| `getPost(string $uri)`                                          | AT-URI로 게시를 취득 |
| `getPosts(array $uris)`                                         | 여러 게시를 한꺼번에 취득 |
| `deletePost(string $uri)`                                       | 게시를 삭제         |
| `getTimeline(?string $algorithm, ?int $limit, ?string $cursor)` | 홈 타임라인을 취득     |
| `getAuthorFeed(?string $actor, ...)`                            | 지정 계정의 피드를 취득  |
| `searchPosts(string $q, ...)`                                   | 게시를 검색         |

### 인게이지먼트

| 메서드                                  | 설명             |
| ------------------------------------ | -------------- |
| `like(Like\|StrongRef $subject)`     | 좋아요를 작성        |
| `deleteLike(string $uri)`            | 좋아요를 취소        |
| `repost(Repost\|StrongRef $subject)` | 리포스트를 작성       |
| `deleteRepost(string $uri)`          | 리포스트를 취소       |
| `getActorLikes(?string $actor, ...)` | 계정의 좋아요 목록을 취득 |

### 팔로우

| 메서드                                 | 설명            |
| ----------------------------------- | ------------- |
| `follow(Follow\|string $did)`       | 팔로우           |
| `deleteFollow(string $uri)`         | 팔로우 해제        |
| `getFollowers(?string $actor, ...)` | 팔로워 목록을 취득    |
| `getFollows(?string $actor, ...)`   | 팔로우 중인 목록을 취득 |

### 프로필·계정

| 메서드                                 | 설명          |
| ----------------------------------- | ----------- |
| `getProfile(?string $actor)`        | 프로필을 취득     |
| `upsertProfile(callable $callback)` | 프로필을 갱신     |
| `resolveHandle(string $handle)`     | 핸들을 DID로 해결 |

### 미디어

| 메서드                                                        | 설명                |
| ---------------------------------------------------------- | ----------------- |
| `uploadBlob(StreamInterface\|string $data, string $type)`  | 이미지 등의 블롭을 업로드    |
| `uploadVideo(StreamInterface\|string $data, string $type)` | 동영상을 업로드          |
| `getJobStatus(string $jobId)`                              | 동영상 업로드 잡의 상태를 확인 |
| `getUploadLimits()`                                        | 업로드 제한을 취득        |

### 알림

| 메서드                                       | 설명            |
| ----------------------------------------- | ------------- |
| `listNotifications(...)`                  | 알림 목록을 취득     |
| `countUnreadNotifications(...)`           | 미읽음 알림 건수를 취득 |
| `updateSeenNotifications(string $seenAt)` | 읽음 처리를 갱신     |

### AT Protocol 레코드 조작

| 메서드                                                                 | 설명         |
| ------------------------------------------------------------------- | ---------- |
| `createRecord(string $repo, string $collection, ...)`               | 레코드를 작성    |
| `getRecord(string $repo, string $collection, string $rkey, ...)`    | 레코드를 취득    |
| `listRecords(string $repo, string $collection, ...)`                | 레코드 목록을 취득 |
| `putRecord(string $repo, string $collection, string $rkey, ...)`    | 레코드를 갱신·작성 |
| `deleteRecord(string $repo, string $collection, string $rkey, ...)` | 레코드를 삭제    |

### 피드 제너레이터·라벨러

| 메서드                                                                    | 설명           |
| ---------------------------------------------------------------------- | ------------ |
| `publishFeedGenerator(BackedEnum\|string $name, Generator $generator)` | 피드 제너레이터를 공개 |
| `createThreadGate(string $post, ?array $allow)`                        | 스레드 게이트를 작성  |
| `upsertLabelDefinitions(callable $callback)`                           | 라벨 정의를 갱신    |
| `deleteLabelDefinitions()`                                             | 라벨 정의를 삭제    |
| `createLabels(RepoRef\|StrongRef\|array $subject, array $labels)`      | 라벨을 부여       |
| `deleteLabels(RepoRef\|StrongRef\|array $subject, array $labels)`      | 라벨을 삭제       |

## 자주 사용하는 단축키 예시

### 게시하기

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

$response = Bluesky::login(
    identifier: config('bluesky.identifier'),
    password: config('bluesky.password'),
)->post('Hello Bluesky');
```

### 리플라이하기

리플라이는 `Post::build()`로 부모 게시의 `StrongRef`를 설정합니다. `HasShortHand`에는 전용 `reply()` 메서드가 없고, `post()`에 `Post` 객체를 전달합니다.

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\Types\StrongRef;

$parent = StrongRef::to(uri: 'at://did:plc:.../app.bsky.feed.post/...', cid: 'bafyrei...');

$reply = Post::create('리플라이 텍스트')
    ->reply(root: $parent, parent: $parent);

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->post($reply);
```

### 좋아요·리포스트

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Types\StrongRef;

$ref = StrongRef::to(uri: 'at://did:plc:.../app.bsky.feed.post/...', cid: 'bafyrei...');

// 좋아요
Bluesky::withToken($session)->like($ref);

// 리포스트
Bluesky::withToken($session)->repost($ref);
```

### 프로필 갱신하기

`upsertProfile()`은 현재 프로필을 취득하고, 클로저 내에서 변경한 내용을 저장합니다. `Profile` 객체의 메서드를 사용해 displayName·description 등을 설정할 수 있습니다.

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Profile;

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->upsertProfile(function (Profile $profile) {
        $profile->displayName('새로운 표시 이름')
                ->description('프로필 설명문');
    });
```

#### 셀프 라벨 설정하기

`SelfLabels`를 사용해 계정 전체에 대해 셀프 라벨을 설정할 수 있습니다.

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Profile;
use Revolution\Bluesky\Types\SelfLabels;

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->upsertProfile(function (Profile $profile) {
        $profile->labels(SelfLabels::make(['!no-unauthenticated']));
    });
```

### 임의의 API를 직접 호출하기

`HasShortHand`에 없는 메서드는 `send()` 또는 `client()`를 사용합니다.

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

// send()로 임의의 XRPC 메서드를 호출
$response = Bluesky::withToken($session)
    ->send(
        api: 'app.bsky.actor.getProfiles',
        method: 'get',
        params: ['actors' => ['did:plc:...', 'did:plc:...']],
    );

// client()로 더 세밀한 조작
$response = Bluesky::withToken($session)
    ->client()
    ->bsky()
    ->getProfiles(actors: ['did:plc:...']);
```

## Facade 경유와 직접 이용의 차이

`Bluesky::post()`와 `app(Factory::class)->post()`는 동일한 `BlueskyManager` 인스턴스에 대한 조작입니다.

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Contracts\Factory;

// Facade 경유(일반적인 사용법)
Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->post('Hello');

// 컨테이너에서 직접 취득
$manager = app(Factory::class);
$manager->login(config('bluesky.identifier'), config('bluesky.password'))
        ->post('Hello');

// 타입 힌트로 DI
class MyService
{
    public function __construct(private Factory $bluesky) {}

    public function doPost(): void
    {
        $this->bluesky->login(
            config('bluesky.identifier'),
            config('bluesky.password'),
        )->post('Hello from DI');
    }
}
```

`scoped` 등록이므로 하나의 요청 내에서는 `login()` 이후의 세션 상태가 유지됩니다.

## Conditionable과 Macroable

`BlueskyManager`는 `Conditionable`과 `Macroable` 트레이트도 사용합니다.

### Conditionable: when() / unless()

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

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->when(config('app.env') === 'production', function ($bluesky) {
        $bluesky->post('프로덕션 환경에서의 게시');
    });
```

### Macroable: 커스텀 메서드 추가

`macro()`를 사용하면 애플리케이션 측에서 `BlueskyManager`에 메서드를 추가할 수 있습니다. `AppServiceProvider`의 `boot()` 안에서 정의하는 것이 일반적입니다.

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

Bluesky::macro('postWithHashtag', function (string $text, string $tag) {
    /** @var \Revolution\Bluesky\BlueskyManager $this */
    return $this->post("{$text} #{$tag}");
});

// 사용 예시
Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->postWithHashtag('Hello', 'laravel');
```

## 커스텀 Agent 삽입

`withAgent()`를 사용하면 임의의 `Agent` 구현을 직접 설정할 수 있습니다.

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Contracts\Agent;

// 커스텀 Agent를 구현하는 경우 Agent 인터페이스를 구현
class MyCustomAgent implements Agent
{
    // ...
}

Bluesky::withAgent(new MyCustomAgent());
```

<Info>
  일반적인 인증(`login()` / `withToken()`)을 사용하면 자동으로 에이전트가 설정되므로, `withAgent()`를 직접 사용하는 경우는 주로 테스트 시입니다.
</Info>

## 참고 링크

* [인증 방식 비교](/ko/packages/laravel-bluesky/authentication) — App Password와 OAuth의 상세
* [Basic client](/ko/packages/laravel-bluesky/basic-client) — 인증 후 API 조작 예시
* [테스트](/ko/packages/laravel-bluesky/testing) — Fake를 사용한 테스트 방법
* Source: [src/BlueskyManager.php](https://github.com/invokable/laravel-bluesky/blob/main/src/BlueskyManager.php)
* Source: [src/HasShortHand.php](https://github.com/invokable/laravel-bluesky/blob/main/src/HasShortHand.php)


## Related topics

- [봇 튜토리얼 - Laravel Bluesky](/ko/packages/laravel-bluesky/bot-tutorial.md)
- [인증 방식 비교 - Laravel Bluesky](/ko/packages/laravel-bluesky/authentication.md)
- [Illuminate\Support\Manager — 드라이버 시스템의 해부](/ko/advanced/manager.md)
- [Homebrew로 PHP와 Composer와 Node.js 설치하기 (macOS)](/ko/blog/mac-setup.md)
- [WSL로 PHP와 Composer와 Node.js 설치하기 (Windows)](/ko/blog/windows-setup.md)
