# 작곡가 버튼 대박

**URL:** https://meta.discourse.org/t/composer-button-bonanza/352525
**Category:** Theme component
**Tags:** composer
**Created:** [2월 16, 2025, 5:07오전 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525 "2025-02-16T05:07:12Z")
**Posts on this page:** 12
**Page:** 2

<div class="post-metadata">

### Author: ![denvergeeks](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/denvergeeks/32/327671_2.png) [@denvergeeks](https://meta.discourse.org/u/denvergeeks)
#### Post date: [1월 21, 2026, 4:34오전 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/28 "2026-01-21T04:34:00Z")

</div>

[#1 - Update javascripts/discourse/api-initializers/api-initializer.js - centertap/DiscourseComposerButtonBonanza - Codeberg.org](https://codeberg.org/centertap/DiscourseComposerButtonBonanza/pulls/1) 에서 확인하실 수 있는 PR을 제출했습니다.

[편집: [Codeberg.org](http://Codeberg.org) 사이트는 제가 이전에 포크하고 PR을 제출하려고 했을 때 오프라인 상태였지만, 현재는 다시 온라인 상태입니다.]

---

<div class="post-metadata">

### Author: ![mdoggydog](https://avatars.discourse-cdn.com/v4/letter/m/82dd89/32.png) [@mdoggydog](https://meta.discourse.org/u/mdoggydog)
#### Post date: [1월 21, 2026, 5:19오전 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/29 "2026-01-21T05:19:27Z")

</div>

DCBB와 관련하여 오류를 경험하셨거나, 심지어 그 오류에 대한 수정 방법까지 알고 계신다면, 모든 내용을 보고하기 위해 업스트림 이슈를 열어주시면 정말 좋겠습니다. 그래야 제가 직접 수정할 수 있으니까요 — 다른 곳에 저장소를 클론해서 사람들에게 당신의 클론을 사용하라고 권하는 것보다는요. (어떤 이유로 Codeberg에서 저장소를 포크할 수 없는 경우라면, 토론 주제에서 우연히 언급되는 것보다는 — 제가 항상 이메일 업데이트를 받는 것도 아닌데 — 직접 알려주시는 것도 좋겠습니다.)

_아, 잠깐… 답장을 누르려는 바로 그 순간, [PR](https://codeberg.org/centertap/DiscourseComposerButtonBonanza/pulls/1) 하나가 나타났네요! 감사합니다!_

이 PR은 WYSIWYG-Composer 환경에서 `<div>`와 `<span>` 차이로 인해 발생하던 문제들(@shauny가 오랫동안 고생해온 문제)을 다루는 건가요 — 아니면 다른 오류/기능 장애를 다루는 건가요, 아니면 단순히 비권장(deprecation) 경고만 다루는 건가요(즉, 언제든 깨질 수 있지만 아직은 깨지지 않은 것들)?

백그라운드에서 무엇이 변경되었는지 이해하고 싶습니다. 관련 주제나 Discourse 문서에 대한 안내가 있다면 정말 감사하겠습니다.

---

<div class="post-metadata">

### Author: ![Shauny](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/shauny/32/362012_2.png) [@Shauny](https://meta.discourse.org/u/Shauny)
#### Post date: [1월 21, 2026, 2:59오후 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/30 "2026-01-21T14:59:49Z")

</div>

ChatGPT와 함께 제 JS 파일에 그대로 삽입할 수 있는 수정 코드를 만들었습니다.

이 코드는 “Spoiler” 버튼을 가로채고 대신 기본 버튼을 트리거합니다.

다른 방법이 없다면 이 기능이 플러그인의 일부로 포함되었으면 좋겠습니다(이deal적으로는 메뉴를 호출하고 버튼을 클릭하는 대신, 버튼이 트리거하는 동일한 동작을 직접 실행하는 것이 좋습니다).

제 테마의 JS 파일에 추가한 코드입니다:

```plaintext
import { apiInitializer } from "discourse/lib/api";

export default apiInitializer((api) => {
  /**
   * === Composer Button Bonanza용 RTE Spoiler 수정 ===
   *
   * 데스크톱: Options를 클릭한 다음 "Blur spoiler"를 클릭하는 방식으로 동작합니다.
   * 모바일: 드롭다운 마크업이 다르므로, 엄격한 셀렉터 대신 텍스트로 항목을 찾아야 합니다.
   *
   */

  const BONANZA_SPOILER_SELECTOR = "button.ComposerButtonBonanza-btn-spoiler";

  const OPTIONS_TRIGGER_SELECTORS = [
    'button[aria-label="Options"]',
    'button[title="Options"]',
    'button[aria-label*="options" i]',
    'button[title*="options" i]',
  ].join(",");

  function isRichTextComposerContext(el) {
    const composer = el.closest(".d-editor, .composer, .composer-controls");
    if (!composer) return false;

    const hasProseMirror = !!composer.querySelector(".ProseMirror");
    const hasTextarea = !!composer.querySelector("textarea");
    return hasProseMirror && !hasTextarea;
  }

  function getComposerRoot(el) {
    return el.closest(".d-editor, .composer, .composer-controls") || document;
  }

  function normalizeText(s) {
    return (s || "").trim().replace(/\s+/g, " ");
  }

  function isVisible(el) {
    if (!el) return false;
    const style = window.getComputedStyle(el);
    if (!style) return false;
    if (style.display === "none" || style.visibility === "hidden") return false;
    const r = el.getBoundingClientRect?.();
    return !!r && r.width > 0 && r.height > 0;
  }

  function openOptionsMenu(composerRoot) {
    const btn = composerRoot.querySelector(OPTIONS_TRIGGER_SELECTORS);
    if (!btn) return false;
    btn.click();
    return true;
  }

  function findOpenDropdownMenuRoot() {
    // Discourse에서 드롭다운은 일반적으로 다음 컨테이너 중 하나를 사용합니다.
    // 마지막에 보이는 항목(최근에 열린 항목)을 선택합니다.
    const candidates = [
      ...document.querySelectorAll(".dropdown-menu, .dropdown-menu __items, .dropdown-menu__ content"),
    ].filter(isVisible);

    return candidates.length ? candidates[candidates.length - 1] : null;
  }

  function findBlurSpoilerButton(menuRoot) {
    if (!menuRoot) return null;

    // 먼저 데스크톱과 유사한 구조를 시도합니다(빠른 경로)
    const exact =
      menuRoot.querySelector('button[title="Blur spoiler"]') ||
      menuRoot.querySelector('button[aria-label="Blur spoiler"]');
    if (exact) return exact;

    // 그렇지 않으면 드롭다운 항목 내부 어디에나 있는 텍스트 내용으로 위치를 찾습니다
    const itemRoots = Array.from(
      menuRoot.querySelectorAll(".dropdown-menu__item, li, div, button")
    );

    for (const node of itemRoots) {
      const text = normalizeText(node.textContent).toLowerCase();
      if (text === "blur spoiler" || text.includes("blur spoiler")) {
        // 가장 가까운 실제 버튼을 클릭합니다
        const btn = node.closest("button") || node.querySelector?.("button");
        if (btn) return btn;
        // 이미 버튼과 유사한 요소인 경우 해당 요소를 반환합니다
        if (node.tagName === "BUTTON") return node;
      }
    }

    // 마지막 수단: 라벨 텍스트로 메뉴의 모든 버튼을 스캔합니다
    const buttons = Array.from(menuRoot.querySelectorAll("button"));
    for (const btn of buttons) {
      const t = normalizeText(btn.textContent).toLowerCase();
      const title = normalizeText(btn.getAttribute("title") || "").toLowerCase();
      const aria = normalizeText(btn.getAttribute("aria-label") || "").toLowerCase();

      if (t.includes("blur spoiler") || title === "blur spoiler" || aria === "blur spoiler") {
        return btn;
      }
    }

    return null;
  }

  function clickBlurSpoilerFromOpenMenu() {
    const menuRoot = findOpenDropdownMenuRoot();
    const btn = findBlurSpoilerButton(menuRoot);
    if (!btn) return false;

    // 일부 모바일 드롭다운은 위치 지정/핸들러 연결을 완료하는 데 틱이 필요합니다
    requestAnimationFrame(() => {
      requestAnimationFrame(() => btn.click());
    });

    return true;
  }

  function openOptionsAndClickBlurSpoiler(composerRoot) {
    if (!openOptionsMenu(composerRoot)) return false;

    // 메뉴와 항목이 존재할 때까지 재시도합니다
    let tries = 0;
    const maxTries = 30;

    const attempt = () => {
      tries++;
      if (clickBlurSpoilerFromOpenMenu()) return;
      if (tries < maxTries) setTimeout(attempt, 35);
    };

    setTimeout(attempt, 0);
    return true;
  }

  document.addEventListener(
    "click",
    (e) => {
      const bonanzaBtn = e.target.closest(BONANZA_SPOILER_SELECTOR);
      if (!bonanzaBtn) return;

      // RTE에서만 오버라이드합니다. 마크다운 모드에서는 Bonanza를 그대로 둡니다.
      if (!isRichTextComposerContext(bonanzaBtn)) return;

      // RTE에서 Bonanza의 BBCode 삽입을 방지합니다
      e.preventDefault();
      e.stopImmediatePropagation();

      const composerRoot = getComposerRoot(bonanzaBtn);
      openOptionsAndClickBlurSpoiler(composerRoot);
    },
    true
  );
});

```

해킹적인 방식이지만, 작동합니다. iOS/macOS Safari와 macOS Chrome에서만 테스트했으며, Android에서는 아직 테스트하지 않았습니다.

---

<div class="post-metadata">

### Author: ![mdoggydog](https://avatars.discourse-cdn.com/v4/letter/m/82dd89/32.png) [@mdoggydog](https://meta.discourse.org/u/mdoggydog)
#### Post date: [3월 21, 2026, 9:05오전 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/31 "2026-03-21T09:05:47Z")

</div>

Composer Button Bonanza 2.0.0 버전을 방금 릴리스했습니다. 유일한 변경 사항은 `site.desktopView`의 비추천(deprecated) 사용을 수정한 것입니다. 자세한 내용은 [소스 커밋](https://codeberg.org/centertap/DiscourseComposerButtonBonanza/commit/e916b5eb952886cce3fa6d7885afaccb2df89fb9)을 참고하세요.

> [@tobiaseigen](#):
>
> 리치텍스트 에디터와 호환되지 않는다는 경고를 첫 번째 게시물에 추가했습니다.

해당 경고를 업데이트했습니다. 문제를 조사해 본 결과, 사실은 그 반대인 것 같습니다. 리치텍스트 에디터가 이 테마 컴포넌트와 호환되지 않는 것이며, 그 이유는 `TextManipulation` 인터페이스의 `ProsemirrorTextManipulation` 구현이 불완전하고/또는 부정확하기 때문입니다.

구체적으로:

- `ProsemirrorTextManipulation.applyList()` 구현은 호출자가 제공하는 `head` 매개변수를 제대로 사용하지 않습니다. 대신 호출자가 제공하는 \_예시 텍스트\_의 키를 살펴 호출자의 의도를 추측하며, 불릿 리스트, 번호 매긴 리스트, 인용 블록의 내장 버튼만 인식하도록 하드코딩되어 있습니다.
- `ProsemirrorTextManipulation.applySurround()` 구현은 원래의 `TextareaTextManipulation.applySurround()` 구현의 동작과 일치하지 않으며, `<span>`을 사용해야 할 때에도 `<div>`를 무분별하게 사용하는 원인이 됩니다. Prosemirror 구현은 또한 `applySurround()`의 `opts` 인수를 무시합니다. (그리고 `applyList()`와 동일한 트릭을 사용하여, 이탤릭, 볼드, 미리 형식화된 텍스트 버튼 감지를 위해 예시 텍스트 키를 하드코딩하고 있습니다.)

@renato, 이 문제들이 누군가의 레이더에 잡혀 있나요? 수정 일정이 있을까요?

---

<div class="post-metadata">

### Author: ![renato](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/renato/32/383632_2.png) [@renato](https://meta.discourse.org/u/renato)
#### Post date: [3월 21, 2026, 4:51오후 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/32 "2026-03-21T16:51:26Z")

</div>

> [@mdoggydog](#):
>
> @renato, 이 문제들이 누군가의 시야에 들어와 있나요? 수정에 대한 일정은 있나요?

정확히 무엇에 필요하신 건지 공유해 주시겠어요?

텍스트 영역(textarea) 에디터만 존재했을 때 만들어진 일부 API는 리치 에디터에서 완전한 동등성을 가질 의도가 아닙니다. ProseMirror의 모든 기능을 중간 추상화 계층으로 가져오는 것은 우리의 의도가 아니에요.

가능하고 필요하면 해당 부분을 개선할 수 있지만, 일반적으로 복잡한 작업이 필요할 때는 등록된 리치 에디터 확장 기능의 `commands` 키를 통해 ProseMirror 의존성에 직접 접근하는 것이 일반적입니다. 예를 들어:

> <https://github.com/discourse/discourse/blob/fb8ef7ee72f062074782f676794c01dd93bf4a14/plugins/spoiler-alert/assets/javascripts/lib/rich-editor-extension.js#L86-L88>

> <https://github.com/discourse/discourse/blob/fb8ef7ee72f062074782f676794c01dd93bf4a14/plugins/spoiler-alert/assets/javascripts/initializers/spoiler-alert.js#L25-L35>

이 예시에서 `applySurround`는 선택된 텍스트에 스포일러 bbcode를 무조건적으로 적용하는 반면, `toggleSpoiler`는 이미 스포일러 노드 안에 있는지, 인라인 스포일러인지 블록 스포일러인지 등을 결정하기 위해 ProseMirror의 모든 기능을 갖추고 있습니다.

---

<div class="post-metadata">

### Author: ![mdoggydog](https://avatars.discourse-cdn.com/v4/letter/m/82dd89/32.png) [@mdoggydog](https://meta.discourse.org/u/mdoggydog)
#### Post date: [3월 22, 2026, 3:57오전 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/33 "2026-03-22T03:57:21Z")

</div>

> [@renato](#):
>
> 정확히 어떤 용도로 필요한지 공유해 주실 수 있을까요?

이 두 메서드가 속한 인터페이스에 더 충실하게 구현되었다면, Composer Button Bonanza가 새로운 RTE에서 거의 "그냥 작동"할 것이라 생각합니다(최소한 마크다운 모드에서 작동하는 정도로는). 다른 테마 구성 요소나 플러그인 개발자들이 아직 관련 문제를 제기하지 않은 것이 놀랍습니다. (물론 제기했을 수도 있습니다. 비슷한 불만 사항을 검색해 보지는 않았지만요.)

> [@renato](#):
>
> 텍스트 영역 에디터만 있을 때 구축된 일부 API는 리치 에디터에서 완전한 호환성을 갖추도록 의도된 것이 아닙니다. ProseMirror의 모든 기능을 중간 추상화 계층으로 가져오려는 의도는 없었습니다.

"ProseMirror의 모든 기능"이 무엇을 포함하는지 정확히 알지는 못하지만, 그것이 필요할 것 같지는 않습니다. `applyList()`와 `applySurround()` 인터페이스는 그렇게 복잡하지 않습니다 — 다만 지금까지 구현된 것보다 더 많은 것이 포함되어 있습니다.

(지금까지 구현된 내용은 원칙적인 "선택 영역에 목록 마크업을 적용"하거나 "선택 영역을 둘러싼 텍스트 마크업을 적용"하는 것이기보다는, "알려진 내장 툴바 버튼의 호출을 특정 prosemirror 함수로 전달하는 것"에 더 가깝습니다.)

---

<div class="post-metadata">

### Author: ![Canapin](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/canapin/32/119591_2.png) [@Canapin](https://meta.discourse.org/u/Canapin)
#### Post date: [4월 20, 2026, 2:58오후 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/34 "2026-04-20T14:58:25Z")

</div>

안녕하세요!

제가 놓치고 있던 훌륭한 컴포넌트네요.

저는 유사한 테마 컴포넌트의 자체 버전을 작업 중이었는데, 이 컴포넌트는 `[wrap]` 계열이거나 허용되는 HTML / 마크다운 / 디스코urs 전용 콘텐츠 등 커스텀 랩(wrap)에 대해 사용자 정의 키보드 단축키를 추가할 수 있도록 허용합니다.

예를 들어, `<kbd>` `</kbd>`, 또는 `<hr>`, 또는 `[wrap=announcement]` `[/wrap]` (인라인 또는 블록 모두 가능) 등 원하는任何东西를 삽입하거나 선택된 텍스트를 감싸기 위해 Ctrl+Shift+K를 정의할 수 있습니다. 또한 기어 메뉴에 선택적인 버튼도 추가했습니다.

삽입하는 콘텐츠에서 `\n`을 사용하여 줄바꿈을 지원합니다.

리치 에디터와도 호환됩니다. 참고용 코드: [https://github.com/Canapin/discourse-custom-shortcuts](https://github.com/Canapin/discourse-custom-shortcuts), 단 경고가 있습니다: 이 코드는 Claude가 작성한 것이며 아직 제가 코드를 검토하지 않았습니다. 또한, 여러분의 것을 사용하는 대신 제 솔루션을 계속 만들었다면, 여러분의 일부 기능을 사용하기 위해 기능을 변경하거나 추가했을 가능성이 높습니다.

제 컴포넌트의 모습은 다음과 같습니다:

 ![image](https://global.discourse-cdn.com/meta/original/4X/1/2/e/12ebc119780ad4268b8e10200aa6e77998eccca5.png)  
 ![image](https://global.discourse-cdn.com/meta/original/4X/7/2/c/72c412d9ace7a52fdf5de41577d7529de14dfb93.png)  
 ![image](https://global.discourse-cdn.com/meta/original/4X/2/e/6/2e65933ae2e4b6506e96af14101870432a759feb.png)

* * *

다시 여러분의 컴포넌트로 돌아가면, 저는 그 다재다능함을 좋아하지만, 저에게는 두 가지 근본적인 것이 부족합니다:

- 리치 에디터 지원
- 커스텀 콘텐츠(커스텀 랩, 태그 또는 기타)를 추가하는 쉬운 방법

저는 제 것들을 '바이브 코딩(vibe code)'할 수 있지만, 여러분의 컴포넌트에 대한 풀 리퀘스트(예: 리치 에디터 지원)를 제안하도록 AI에 의존하는 것에는 아직 충분히 편안하지 않으며, 저는 프로그래머도 아닙니다.

리치 에디터 문제 외에도, 여러분의 컴포넌트에서 새로운 커스텀 콘텐츠를 추가하는 것의 실현 가능성에 대해 어떻게 생각하시나요(기본적으로 제가 제 컴포넌트에 대해 설명한 그 기능)? 그것이 받아들여지거나, 아니면 그 범위를 벗어난 것일까요?

---

<div class="post-metadata">

### Author: ![mdoggydog](https://avatars.discourse-cdn.com/v4/letter/m/82dd89/32.png) [@mdoggydog](https://meta.discourse.org/u/mdoggydog)
#### Post date: [4월 20, 2026, 4:23오후 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/35 "2026-04-20T16:23:08Z")

</div>

> [@Canapin](#):
>
> 자신의 컴포넌트에서 새로운 커스텀 콘텐츠를 추가하는 것의 실현 가능성에 대해 어떻게 생각하시나요? (제가 자신의 컴포넌트에 대해 설명한 것과 기본적으로 같은 내용입니다)

질문을 정확히 이해하지 못하겠습니다 — \_\_Composer Button Bonanza\_\_가 이미 이 기능을 지원하지 않나요?

컴포넌트 설정, 특히 “Buttons” 설정을 커스터마이징해 보셨나요?

- "Buttons"은 버튼(실제로는 기능)을 정의하는 데 사용됩니다.
- "Layout"은 UI에서 버튼이 어디에/언제 표시될지를 지정하는 데 사용됩니다(또는 단축키를 지정합니다).
- "Translations"은 버튼 정의에 번역을 추가하는 데 사용됩니다.

---

<div class="post-metadata">

### Author: ![Canapin](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/canapin/32/119591_2.png) [@Canapin](https://meta.discourse.org/u/Canapin)
#### Post date: [4월 20, 2026, 4:33오후 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/36 "2026-04-20T16:33:56Z")

</div>

오오, 정말 그렇네요! 이 설정을 완전히 놓치고 있었어요! 더 자세히 살펴보겠습니다. 제 필요에 부합할 수도 있겠네요 🤝

---

<div class="post-metadata">

### Author: ![Canapin](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/canapin/32/119591_2.png) [@Canapin](https://meta.discourse.org/u/Canapin)
#### Post date: [4월 20, 2026, 5:44오후 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/37 "2026-04-20T17:44:02Z")

</div>

키보드 단축키가 작동하지 않아서 애를 먹고 있습니다.

![image](https://global.discourse-cdn.com/meta/original/4X/4/6/4/464edd63a82db91f0f02be599e58d669add973bd.png)

도구 모드의 버튼은 작동하는데, Ctrl+Shift+k를 누르면 아무 반응이 없습니다. 혹시 원인을 아시는 분 계신가요?

---

<div class="post-metadata">

### Author: ![mdoggydog](https://avatars.discourse-cdn.com/v4/letter/m/82dd89/32.png) [@mdoggydog](https://meta.discourse.org/u/mdoggydog)
#### Post date: [4월 20, 2026, 6:06오후 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/38 "2026-04-20T18:06:54Z")

</div>

> [@Canapin](#):
>
> 키보드 단축키가 작동하지 않아서 고생하고 있습니다.

아, 죄송합니다! 레이아웃 설정 문서에 실수가 있었습니다: 단축키 지정자는 빼기(-) 기호가 아니라 더하기(+) 기호를 사용해야 합니다.

예를 들어: `shift+k` (_`shift-k`가 아닙니다_).

(앞으로 1~2일 안에 설정 페이지 문서를 업데이트하겠습니다.)

---

<div class="post-metadata">

### Author: ![mdoggydog](https://avatars.discourse-cdn.com/v4/letter/m/82dd89/32.png) [@mdoggydog](https://meta.discourse.org/u/mdoggydog)
#### Post date: [4월 27, 2026, 9:02오후 UTC](https://meta.discourse.org/t/composer-button-bonanza/352525/39 "2026-04-27T21:02:14Z")

</div>

> [@mdoggydog](#):
>
> (하루 이틀 내에 설정 페이지 문서를 업데이트하겠습니다.)

완료되었습니다. 버전 2.0.1이 출시되었습니다. (이 업데이트를 통해 전반적인 문서가 추가되고, 새 버튼의 CSS 스타일링 관련 버그가 수정되었습니다.)

[Previous page](https://meta.discourse.org/t/composer-button-bonanza/352525.md?page=1)
