사용자 정의 필드를 추가한 더 정교한 user-card-contents 테마를 만들었습니다. “새 주제 만들기” 컴포넌트와 유사한 방식으로 카드가 고정(sticky)되고 닫기 버튼이 있는 형태로 만들고 싶습니다.
요소를 닫는 호출을 방지하려면 user-card-contents.js를 오버라이드해야 한다는 제 추측이 맞나요? 이 내용을 테마 안에 패키징할 수 있을까요?
감사합니다!
사용자 정의 필드를 추가한 더 정교한 user-card-contents 테마를 만들었습니다. “새 주제 만들기” 컴포넌트와 유사한 방식으로 카드가 고정(sticky)되고 닫기 버튼이 있는 형태로 만들고 싶습니다.
요소를 닫는 호출을 방지하려면 user-card-contents.js를 오버라이드해야 한다는 제 추측이 맞나요? 이 내용을 테마 안에 패키징할 수 있을까요?
감사합니다!
Pete님, Meta에 오신 것을 환영합니다 ![]()
간단한 답은 '예’입니다. 변경 사항이 프론트엔드에만 영향을 미친다면, 테마/컴포넌트에서 이를 수행할 수 있습니다.
'sticky’라고 말씀하신 것이 구체적으로 무엇을 의미하는지 설명해 주실 수 있을까요? 콘텐츠가 스크롤되는 동안 같은 위치에 머물러 있도록 하시는 것이라면, 그것은 CSS 변경 사항이 될 것입니다. 또한, 이 변경 사항은 데스크톱과 모바일 양쪽에 포함될 예정인가요?
구체적으로 어떤 호출을 말씀하시는 것인지 설명해 주실 수 있을까요? (예: 클릭 시, 스크롤 시 등)
닫기 버튼의 마크업은 사용자 카드의 Handlebars 템플릿에 추가되어야 합니다. 버튼 클릭 시 동작을 처리하는 로직은 컴포넌트 .js 파일에 추가되어야 합니다.
전체 오버라이드는 필요하지 않을 수 있습니다. 클래스의 특정 메서드를 오버라이드하는 데 사용할 수 있는 훅이 몇 가지 있습니다. 하시는 내용을 조금 더 자세히 설명해 주시면, 이에 대해 더 자세히 공유해 드리겠습니다.
환영해줘서 고마워, Joe!
제가 하려는 것은 데스크톱에서 card-contents-base.js 믹신의 이벤트 핸들러인 clickOutsideEventName이 카드를 닫지 못하도록 막는 것입니다. 대신 사용자가 버튼을 클릭해서 닫도록 강제하고 싶습니다. 모바일에서는 아마 다른 방식으로 처리해야 할 것 같습니다.
이 handlebars 템플릿은 작동하게 만들었고, 이제 .js를 파악하는 중입니다 ![]()
TL;DR 이 글이 여러분이 찾는 내용일 것 같습니다
Theme JS
api.modifyClass("component:user-card-contents", {
didInsertElement() {
this._super(...arguments);
$("html").off(this.clickOutsideEventName);
},
actions: {
closeCard() {
this._close();
}
}
});
그리고 템플릿 어딘가에 다음 코드를 추가하세요
{{d-button
class="btn-flat"
action=(action "closeCard")
icon="times"
}}
자세한 설명
테마를 이미 다뤄보셨기 때문에 대부분 이미 알고 계실 가능성이 높지만, 더 넓은 독자를 위해 조금 더 자세히 설명해 보겠습니다.
먼저 로컬에서나 GitHub에서 검색을 해보겠습니다. GitHub에서 검색하면 이런 결과가 나옵니다. 대부분의 경우 검색어는 하나 이상의 결과를 반환하며, 더 구체적으로 검색하거나 결과를 수동으로 살펴보고 원하는 것에 가까운 것을 찾아야 합니다.
그렇게 하면 결국 이 파일에 도달하게 됩니다.
이 파일은 Mixin입니다. 왜 이걸 언급하는 걸까요? 믹스인은 여러 다른 곳에서 공유될 수 있다는 점을 인지해야 하기 때문입니다. 이 경우, 사용자 카드와 그룹 카드 모두에 사용됩니다. 따라서 여기서 하는 변경 사항은 둘 다에 영향을 미칩니다.
파일 내에서 검색하면 clickOutsideEventName이 처음 여기에서 정의된다는 것을 알 수 있습니다
그리고 여기에서 프로퍼티로 전달됩니다
마지막으로 여기에서 사용(consume)됩니다
좋습니다. 그런데 이 모든 것이 무엇을 의미할까요? 이 코드가 추가되는 위치를 보면 didInsertElement 안에 있다는 것을 알 수 있습니다
Ember 가이드는 이렇게 명시하고 있습니다
didInsertElement()이 호출될 때쯤이면 Ember는 다음을 보장합니다:
- 컴포넌트의 요소가 생성되고 DOM에 삽입되었습니다.
- 컴포넌트의 요소는 컴포넌트의
this.element프로퍼티를 통해 접근할 수 있습니다.
왜 이것이 필요한가요? 사용자 카드와 그룹 카드에 대해 다른 mousedown 핸들러가 필요하기 때문입니다. 조금 돌아가 보면, 이제 요소의 id가 clickOutsideEventName에서 사용되고 있다는 것을 알 수 있습니다
위에서 논의했듯이, 이것은 프로퍼티로 전달됩니다.
자, 이제 이것이 여러분이 하려는 것과 어떻게 관련되는지 살펴봅시다.
여러분은 사용자가 카드 밖을 클릭할 때 카드가 닫히지 않도록 막으려 하고 있습니다. 그렇다면 이를 위한 방법을 찾아봅시다. 기억하시나요? clickOutsideEventName이 결국 여기에서 사용(consume)된다고 논의했습니다
요약하자면, 이것은 카드가 삽입될 때(첫 페이지 뷰에서) HTML 요소에 mousedown 핸들러를 추가합니다. 그리고 mousedown 이벤트의 타겟을 확인합니다. 타겟이 카드 어딘가라면 처리를 중단합니다. 카드 밖이라면 this._close()를 호출하여 닫습니다.
_close()는 여기에서 정의됩니다
닫기 버튼을 추가할 때 호출해야 하는 것이 바로 이것입니다 - 하지만 이 부분은 나중에 다시 다루겠습니다.
이제, 카드 밖의 클릭이 닫히지 않도록 하려면 이 mousedown 핸들러를 제거하는 것이 목표입니다. 그렇다면 어떻게 해야 할까요? didInsertElement()를 수정해야 하며, 이렇게 할 수 있습니다.
Discourse 테마는 플러그인 API에 접근할 수 있으며, 여기서 사용할 수 있는 많은 메서드가 있습니다. 가장 자주 사용되는 메서드에 대한 더 자세한 내용은 여기에서 확인할 수 있습니다.
기억하시나요? 우리가 작업 중인 파일은 Mixin입니다. Mixin은 Ember 클래스입니다. 따라서 사용할 메서드는 modifyClass입니다
modifyClass를 사용하면 클래스 메서드를 추가, 수정 또는 완전히 덮어쓸 수 있습니다. 여러분이 하려는 것은 메서드 수정이므로 이에 초점을 맞추겠습니다.
didInsertElement()를 수정하려고 하므로 다음과 같은 코드를 작성할 수 있습니다
api.modifyClass('mixin:card-contents-base', {
didInsertElement() {
console.log("foo");
}
});
그리고 시도해 봅시다…
안 됩니다.
왜일까요? modifyClass 메서드가 현재 Mixin 수정을 지원하지 않는 것으로 밝혀졌습니다(제가 테스트한 범위 내에서). 왜 그런지 알아보고 수정할 수 있는지 확인해 보겠습니다. 하지만 지금은 여러분이 하려는 것으로 돌아가 봅시다.
Mixin을 수정할 수 없으므로, 막힌 것 같죠? 아닙니다. 조금 더 깊이 파고들어 봅시다.
앞서 언급했듯이, 그 Mixin은 사용자 user-card-contents와 group-card-contents Ember 컴포넌트 모두에서 사용됩니다(다시 말하지만, Mixin은 코드 재사용을 위해 설계되었기 때문입니다)
그러면 user-card-contents 컴포넌트를 여기서 살펴봅시다
주의 깊게 읽으면, 위에서 논의한 Mixin을 먼저 여기서 가져온다는 것을 알 수 있습니다
그리고 새로운 Ember 컴포넌트를 생성하여 Mixin을 전달합니다.
이것이 무엇을 의미할까요? user-card-contents의 didInsertElement()가 실제로는 Mixin에서 상속된다는 뜻입니다. group-card-contents에 대해서도 같은 말을 할 수 있습니다.
그렇다면 우리는 어디에 있는 걸까요? 좋은 소식과 나쁜 소식이 있습니다. 좋은 소식은 user-card-contents에 변경을 가하되 group-card-contents에는 영향을 미치지 않으려면 할 수 있다는 것입니다. 나쁜 소식은 변경 사항이 둘 다에 적용되기를 원한다면 일부 코드를 중복해야 한다는 것입니다.
modifyClass로 돌아가서 user-card-contents로 다시 시도해 봅시다. 즉, 다음과 같은 코드를 작성합니다:
api.modifyClass('component:user-card-contents', {
didInsertElement() {
console.log("foo");
}
});
그리고 무슨 일이 일어나는지 봅시다…
voilà ![]()
변경 사항이 등록되고 콘솔에서 볼 수 있지만, 아직 완전히 해결된 것은 아닙니다.
자, 이제 didInsertElement()를 수정할 수 있으므로, 여러분이 하려는 것으로 돌아가 봅시다. 기억하시나요? mousedown 핸들러는 didInsertElement에서 여기에서 정의됩니다
그렇다면 어떻게 해야 할까요? 간단합니다
$("html").off(clickOutsideEventName)
그렇게 하면 될까요? 아닙니다. 이렇게 하면
api.modifyClass('component:user-card-contents', {
didInsertElement() {
$("html").off(clickOutsideEventName)
}
});
깨진 결과를 얻게 됩니다. 왜일까요? 그것은 메서드 수정이 아니라 해당 컴포넌트의 코어 didInsertElement() 메서드를 완전히 덮어쓰는 것이기 때문입니다. 이렇게 하면 코어의 어떤 코드도 실제로 적용되지 않습니다.
이를 어떻게 수정할까요? Ember에는 this._super(...arguments)라는 것이 있다는 것이 밝혀졌습니다.
이것이 무엇을 하는 걸까요? 클래스 메서드가 이미 가진 것 외에도 코드를 추가하거나 앞에 붙일 수 있게 해줍니다. 예를 들어, 이렇게 하면
api.modifyClass('component:user-card-contents', {
didInsertElement() {
// 추가하려는 코드
$("html").off(clickOutsideEventName);
// 코어 코드
this._super(...arguments);
}
});
여기서 다른 모든 것보다 먼저 여러분이 추가하려는 코드가 실행됩니다
반면에, 이렇게 하면…
api.modifyClass('component:user-card-contents', {
didInsertElement() {
// 코어 코드
this._super(...arguments);
// 추가하려는 코드
$("html").off(clickOutsideEventName);
}
});
여기서 모든 것이 실행된 후에 여러분의 코드가 실행됩니다
코어의 변경 사항에 대해 테마가 견고하게 유지되는 좋은 방법입니다. 코어의 모든 것이 먼저 실행되고, 그 후에 여러분의 코드가 실행되기 때문입니다. 자, 다시 시도해 보고 무슨 일이 일어나는지 봅시다.
api.modifyClass('component:user-card-contents', {
didInsertElement() {
// 코어 코드
this._super(...arguments);
// 추가하려는 코드
$("html").off(clickOutsideEventName);
}
});
그리고…
왜 이런 일이 일어나는 걸까요? 코드 컨텍스트가 다르기 때문입니다. Ember 컴포넌트 파일에서는 clickOutsideEventName이 사용될 때 이미 정의되어 있습니다. 여러분의 테마는 다른 파일에 있으므로, 거기서 clickOutsideEventName은 정의되어 있지 않습니다.
이를 어떻게 수정할까요? 이것을 기억하나요?
clickOutsideEventName은 컴포넌트의 프로퍼티이므로, this.clickOutsideEventName을 사용하면 작동해야 합니다. 시도해 봅시다.
api.modifyClass('component:user-card-contents', {
didInsertElement() {
// 코어 코드
this._super(...arguments);
// 추가하려는 코드
$("html").off(this.clickOutsideEventName);
},
});
그리고 실제로 작동합니다 ![]()
사용자 카드가 이제 오류 없이 열리고, 밖에서 어디를 클릭해도 아무 일이 일어나지 않습니다.
위에서 언급했듯이 그룹 카드에 대해 정확히 같은 일을 할 수 있습니다
api.modifyClass('component:group-card-contents', {
didInsertElement() {
// 코어 코드
this._super(...arguments);
// 추가하려는 코드
$("html").off(this.clickOutsideEventName);
},
});
남은 일은 이전에 논의한 _close() 메서드에 닫기 버튼을 연결하는 것뿐이며, 이를 위해 세 단계가 필요합니다.
closeCard 액션을 추가합니다 (원하는 이름으로 명명할 수 있습니다)간단함을 위해 사용자 카드에 집중하겠습니다. 따라서 우리는 이것을 추가합니다 (이미 논의한 것과 함께)
api.modifyClass("component:user-card-contents", {
didInsertElement() {
this._super(...arguments);
$("html").off(this.clickOutsideEventName);
},
// 새로운 부분
actions: {
closeCard() {
this._close();
}
}
});
```\n
이것이 하는 일은 `closeCard` 커스텀 액션이 트리거될 때마다 코어의 `_close()` 메서드를 호출하는 것입니다.
다음으로 사용자 카드 템플릿에 버튼을 추가해야 하거나, 다음과 같은 것이 필요합니다
{{d-button
class=“btn-flat”
action=(action “closeCard”)
icon=“times”
}}
제 대략적인 결과는 다음과 같습니다

물론 위에서 언급했듯이 그룹 카드에 대해 비슷한 일을 할 수 있습니다.
그룹 카드와 모바일 구현은 여러분에게 연습으로 남겨두겠습니다. 위에서 논의한 개념을 그대로 사용하면 되지만, 변경 사항을 적용하는 과정에서 문제가 발생하면 알려주세요.
정말 감사합니다. 아주 훌륭한 튜토리얼이었습니다. 정말 감사드립니다. 플러그인 API에 대해서는 몰랐습니다.
위에서 바로 눈에 띄지 않던 부분은 테마 JS 변경 사항을 스크립트 태그로 감싸고 플러그인의 common/head_tag.html에 넣어야 한다는 점이었습니다:
<script type="text/discourse-plugin" version="0.2">
</script>
궁금해서 여쭤봅니다만, 태그의 버전 번호가 여기서 중요할까요? 그리고 항상 header.html보다 head_tag.html에 넣는 것이 최선인지, 아니면 크게 상관이 없는지 궁금합니다.
감사합니다!
@Johani 원하시면 이 내용으로 새 스레드를 만들 수도 있지만, 현재 클라이언트 측 user-card 컨트롤러를 추적하고 있습니다:
두 번째 클릭 기능을 제거하는 것이 목표입니다. 감사합니다!