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

# 다른 앱의 번들 라인 그룹화하기

> setInternalProperties를 사용해 Aftersell Cart에 어떤 라인들이 같은 번들에 속하는지 알려주면, 여러 개의 무관한 라인 대신 하나의 항목으로 렌더링돼요.

대부분의 번들 앱은 **각 구성품을 별도의 카트 라인으로 추가**한 다음, 자체 설계한 라인 아이템 속성으로 서로 연결하는 방식으로 번들을 만들어요. Shopify의 Ajax API는 이 라인들이 서로 관련되어 있다는 표시 없이 카트에 전달하므로, 기본적으로 드로어는 3개 구성품 번들을 각각 고유한 가격과 수량 스테퍼가 있는 3개의 무관한 항목으로 표시해요.

`setInternalProperties`가 카트에 이것들이 하나라고 알려주는 방법이에요.

<div id="how-grouping-works">
  ## 그룹화 작동 방식
</div>

카트는 두 가지 **표준(canonical) 속성**을 기준으로 라인을 그룹화해요. 카트는 번들 앱의 속성 이름을 알지 못하므로, 여러분이 번역해 줘야 해요: 앱이 작성한 내용을 읽고, [라인 트랜스폼](/ko/aftersell/cart/sdk-hooks#registerlinetransform)으로 각 라인에 표준 속성 쌍을 찍어 주세요.

| 속성                            | 필수 여부 | 값                                    |
| ----------------------------- | ----- | ------------------------------------ |
| `_aftersell_cart_bundle_id`   | 예     | 공유 ID예요. 같은 ID를 가진 모든 라인이 하나의 번들이에요. |
| `_aftersell_cart_bundle_role` | 아니요   | 번들이 표시될 라인에 `parent`로 설정하세요.         |

이 속성들은 Shopify가 아닌 `setInternalProperties`를 통해 전달돼요. **렌더링 전용 오버레이**라서 `properties`에 도달하지 않고, Shopify에 저장되지 않으며, 주문에도 나타나지 않아요.

<div id="step-1-find-out-what-your-app-writes">
  ## 1단계: 앱이 무엇을 작성하는지 확인하기
</div>

번들 앱마다 속성 이름이 다르므로, 실제 카트를 살펴보는 것부터 시작하세요. 스토어프런트에서 번들을 추가한 다음, 브라우저 콘솔에서 다음을 실행하세요:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.actions.getCart().items.forEach((line) => {
  console.log(line.title, line.properties);
});
```

번들의 라인들에 공유되는 속성을 찾으면 돼요. 보통 ID, 참조 또는 번들 이름을 담은 숨겨진 속성(이름이 `_`로 시작)이에요. `_bundle_id`, `_bundle_ref`, `_parent_id` 같은 것이 일반적이에요. 정확한 키를 기록하고, 한 라인이 메인 상품으로 표시되어 있는지 확인하세요.

<div id="step-2-map-it-onto-the-canonical-properties">
  ## 2단계: 표준 속성에 매핑하기
</div>

**Cart settings → Custom script → Initialization**에 붙여넣고, 속성 이름을 여러분이 찾은 이름으로 바꾸세요:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerLineTransform((line) => {
  const props = line.properties;
  if (!props) return;

  const bundleId = props._bundle_id;
  if (!bundleId) return;

  line.setInternalProperties({
    _aftersell_cart_bundle_id: bundleId,
    // Mark the main product so the bundle renders under it.
    _aftersell_cart_bundle_role: props._bundle_role === 'main' ? 'parent' : 'child',
  });
});
```

이게 통합의 전부예요. 두 개 이상의 라인이 같은 ID를 공유하면 카트가 하나의 번들로 접어서 표시해요.

<Note>
  앱이 메인 상품을 표시하지 않는다면 `_aftersell_cart_bundle_role`을 아예 생략하세요. 카트가 앵커를 대신 선택해 줘요.
</Note>

<div id="what-you-get">
  ## 얻을 수 있는 것
</div>

라인이 그룹화되면 앵커 라인에 [`bundle` 객체](/ko/aftersell/cart/sdk-cart-object#bundles)가 담기고, 드로어는 번들을 하나의 항목으로 렌더링해요:

* **하위 항목이 앵커 아래에 중첩**되어 별도의 행으로 표시되지 않아요.
* **수량이 원자적으로 처리돼요.** 번들의 수량을 변경하면 각 하위 항목의 `perAnchorQty` 비율에 따라 모든 구성원이 함께 확장되므로, 한 구성품이 2개인 번들은 그 2:1 관계를 유지해요.
* **제거도 원자적으로 처리돼요.** 번들을 제거하면 모든 구성원 라인이 한 번의 요청으로 제거되어, 구성품이 고아처럼 남지 않아요.
* **가격 행이 하나예요.** 표시되는 내용은 [Cart items](/ko/aftersell/cart/cart-items-block) 블록의 **bundle price** 설정을 따라요: 모든 구성원의 합계 또는 메인 상품 가격만 표시해요.

<div id="how-the-anchor-is-chosen">
  ## 앵커 선택 방식
</div>

앵커는 번들이 표시되는 라인이에요. 카트는 다음 순서로 선택해요:

1. `_aftersell_cart_bundle_role`이 `parent`로 설정된 라인.
2. 없으면 **가격이 가장 높은** 구성원.
3. 그것도 없으면 카트의 첫 번째 구성원.

번들 앱은 보통 메인 상품에 할인을 적용하기 때문에 가격 기반 폴백이 대체로 맞아요. 메인 상품이 가장 저렴하거나 무료인 경우처럼 그렇지 않을 때는 역할을 명시적으로 설정하세요.

<div id="rules-worth-knowing">
  ## 알아 두면 좋은 규칙
</div>

* **번들에는 최소 두 개의 라인이 필요해요.** 번들 ID를 가진 단일 라인은 그대로 두고 정상적으로 렌더링돼요.
* **Shopify 네이티브 번들은 이미 처리돼요.** Shopify 자체가 componentized로 표시한 라인은 이 그룹화에서 건너뛰고 자동으로 적용돼요. 별도의 라인을 추가하는 앱에만 이 방법이 필요해요.
* **트랜스폼은 렌더링마다 실행돼요.** 가볍게, 부작용 없이 유지하세요. 내부에서 액션을 호출하거나 fetch하지 마세요.
* **병합은 누적 방식이에요.** 여러분의 속성은 다른 트랜스폼이 설정한 속성과 병합돼요. 같은 키에 대해 실제 충돌이 발생하면 마지막에 등록된 트랜스폼이 이겨요.
* **그룹화는 숨기기와 이름 변경 이후, 정렬 이전에 실행돼요.** 따라서 `setHidden`으로 숨긴 라인은 번들의 일부가 되지 않고, [comparator](/ko/aftersell/cart/sdk-hooks#registerlinecomparator)는 하위 항목이 아닌 앵커를 봐요.

<Warning>
  **그룹화된 하위 항목은 `state.items`에서 사라져요.** 라인이 번들로 접히면 `getCart().items`와 이벤트 페이로드에는 앵커만 나타나고, 하위 항목은 `anchor.bundle.children`으로 이동해요. `itemCount`에도 더 이상 집계되지 않아요.

  **카트 총액은 영향을 받지 않아요.** 총액은 Shopify에서 직접 가져오기 때문이에요. 그룹화는 표시 방식만 변경하며, 쇼핑객이 지불하는 금액은 절대 변경하지 않아요.
</Warning>

<div id="reading-a-bundle-back">
  ## 번들 다시 읽기
</div>

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_updated', (state) => {
  state.items.forEach((line) => {
    if (!line.bundle) return;
    console.log(line.title, 'is a bundle of', line.bundle.children.length, 'items:');
    line.bundle.children.forEach((child) => {
      console.log('  ', child.quantity, 'x', child.title);
    });
  });
});
```

번들의 라인에 작업을 수행하려면, 앵커를 포함한 모든 구성원의 `key`를 담고 있는 `bundle.memberKeys`를 사용하세요.

<div id="using-it-for-other-things">
  ## 다른 용도로 사용하기
</div>

번들 그룹화가 `setInternalProperties`가 만들어진 목적이지만, 이 오버레이는 **라인에서 파생된 렌더링 전용 데이터**를 위한 범용 채널이에요. 여기에 넣은 것은 실제 카트를 건드리지 않고 `line.internalProperties`와 [커스텀 코드 블록](/ko/aftersell/cart/custom-code-blocks)에서 읽을 수 있어요:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerLineTransform((line) => {
  if (line.properties?._preorder_ship_date) {
    line.setInternalProperties({ _badge: `Ships ${line.properties._preorder_ship_date}` });
  }
});
```

값이 **파생된** 표시 전용 데이터일 때 사용하세요. 데이터가 주문까지 살아남아야 한다면, 상품 폼의 숨겨진 `properties[...]` 입력으로 설정한 실제 라인 아이템 속성이 적합해요. 그러면 누가 추가를 수행하든 값이 Shopify에 도달해요.

<div id="where-to-go-next">
  ## 다음 단계
</div>

* **[`registerLineTransform`](/ko/aftersell/cart/sdk-hooks#registerlinetransform)**: 이 작업이 거치는 훅.
* **[Cart 객체](/ko/aftersell/cart/sdk-cart-object#bundles)**: `bundle`과 하위 항목의 구조.
* **[Cart items 블록](/ko/aftersell/cart/cart-items-block)**: 번들 가격 설정.
