테마 컴포넌트로 사용자 로그인 이벤트를 청취하는 방법이 있을까요?

안녕하세요,

사용자가 처음 로그인할 때 Theme 컴포넌트를 사용하여 팝업 상자를 표시하려고 합니다.

1. 사용자가 성공적으로 로그인할 때 팝업 상자를 표시하기 위해 Theme 컴포넌트를 사용하여 로그인 컨트롤러의 로그인 액션을 오버라이드해 보았습니다.
여기서 로그인 액션 내에서 hiddenLoginForm.submit(); 코드 앞에 bootbox.alert(“Test Alert”); 코드를 추가했습니다.

하지만 팝업은 hiddenLoginForm이 제출되어 홈 페이지로 리디렉션될 때까지 표시되고, 이후 사라집니다.

제 요구사항은 성공적인 로그인 후 홈 페이지로 리디렉션된 후에 팝업 상자가 표시되도록 하는 것입니다.

2. 사용자가 처음 로그인하는지 확인하기 위해 현재 사용자의 previous_visit_at 속성의 값을 확인해 보았습니다.
만약 previous_visit_at = null 이면 (즉, 사용자가 처음 로그인하는 경우)
하지만 여기에서도 테스트가 실패합니다. 두 번째로 로그인했을 때 previous_visit_at 속성의 값이 여전히 null이었기 때문입니다.
previous_visit_at 속성의 값이 정확히 언제 업데이트되는지 알고 싶습니다.

위 요구사항을 달성하는 데 도움을 주시면 감사하겠습니다.

감사합니다,
Saurabh Khandelwal

Hey @Saurabh_Khandelwal :wave:

이렇게 하는 것은 권장하지 않습니다. 로그인은 가장 중요한 액션 중 하나이며, 코어에서 해당 영역에 변경 사항이 생기면 오버라이드된 액션이 취약해지고 깨지기 쉽습니다.

기억하시겠지만, 이 사이트에 처음 가입했을 때 다음과 같은 것을 보셨을 것입니다.

이것을 확인하기 위해 사용하는 조건은 다음과 같습니다.

!user.read_first_notification && !user.enforcedSecondFactor

따라서 이니셜라이저에서 동일한 조건을 사용하면, 로그인 후 사용자의 첫 페이지 뷰에서 Bootbox를 렌더링할 수 있습니다.

테마 컴포넌트에서 다음과 같은 코드를 시도해 볼 수 있습니다.

import { withPluginApi } from "discourse/lib/plugin-api";
import bootbox from "bootbox";

export default {
  name: "first-login-bootbox",
  initialize() {
    withPluginApi("0.8", api => {
      const user = api.getCurrentUser();
      if (!user) return;

      if (!user.read_first_notification && !user.enforcedSecondFactor) {
        const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor`;
        bootbox.alert(text);
      }
    });
  }
};

그리고 다음과 같은 결과가 나옵니다.

필요하다면 bootbox 텍스트에 HTML을 사용할 수 있습니다. 사용자가 bootbox를 닫고 원으로 표시된 아바타를 클릭하면, 다시는 bootbox를 보지 않게 됩니다.

bootbox는 단순한 대화상자/확인 창을 위해 설계되었습니다. 그보다 조금 더 복잡한 것이 필요하다면, showModal()을 사용하여 모달을 사용하는 것이 더 나을 것입니다.

조언해 주셔서 감사합니다, @Johani. 로그인 액션에서 변경 사항을 피하고, 말씀해 주신 대로 구현해 보겠습니다.

.

안녕하세요, @Johani 팝업 상자 안에 간단한 하이퍼링크(예: “/new-topic”)를 추가했는데, 이 링크는 새 탭에서 열리도록 설정되어 있습니다.
해당 링크를 클릭하면 새 탭에서 열리지만, 아래 조건을 확인하고 있기 때문에 다시 팝업 상자가 표시됩니다.

!user.read_first_notification && !user.enforcedSecondFactor 

제 경우라면 링크를 방문한 후에는 팝업 상자가 표시되지 않아야 합니다.

팝업 상자가 한 번 표시된 후 이 속성들의 값을 업데이트할 수 있는지, 가능하다면 어떻게 해야 하는지 알려주실 수 있을까요?

안타깝게도 불가능합니다.

해당 속성들은 세터(setter)가 없습니다. 설령 세터가 있다고 하더라도, 사용자의 변경 사항은 첫 번째 탭에서만 일시적으로 적용됩니다. 사용자가 두 번째 탭을 방문하면 데이터는 데이터베이스에 저장된 내용을 기반으로 하게 됩니다. 테마는 백엔드에 접근할 수 없으며, 프론트엔드만 변경할 수 있습니다.

할 수 있는 것은 링크에 해시(hash)를 추가하고 다음과 같이 확인하는 것입니다.

import { withPluginApi } from "discourse/lib/plugin-api";
import bootbox from "bootbox";

export default {
  name: "first-login-bootbox",
  initialize() {
    withPluginApi("0.8", api => {
      const user = api.getCurrentUser();
      if (!user) return;

      if (
        !user.read_first_notification &&
        !user.enforcedSecondFactor &&
        !window.location.hash
      ) {
        const text = `Lorem ipsum dolor sit amet <a href="http://localhost:3000/new-topic#some-hash" target="_blank">링크</a>, consectetur adipiscing elit, sed do eiusmod tempor`;
        bootbox.alert(text);
      }
    });
  }
};

게시물에 "/new-topic"으로 링크를 건 것이 단순히 예시인지, 아니면 실제로 그렇게 하기를 원하는지 확실하지 않습니다. 만약 이것이 원하는 결과라면, 또 다른 문제가 있습니다. 해시가 포함된 페이지에서 부트박스(bootbox)가 표시되지 않더라도, 사용자는 여전히 이것을 보게 됩니다…

…그리고 컴포저(composer)가 열리지 않습니다. 사용자가 처음 페이지를 보는 즉시 주제 작성을 시작하는 것은 매우 예상 밖의 상황이므로 이는 당연합니다.

여기서 무엇을 이루고자 하는지 여쭤봐도 될까요? 사용자에게 무언가를 알려주기 위한 것인가요?

다른 사이트에서 이렇게 하는 것을 본 적이 있는데, 환영 메시지를 편집하는 것입니다. 하지만 그것이 옵션이라면 대안이 있습니다.

다음과 같이 제안합니다.

  1. 주제를 생성하고 원하는 모든 정보를 그곳에 추가합니다
  2. 그 주제를 게시합니다
  3. 부트박스에서 그 주제로 링크를 걸고, 해당 링크를 새 탭에서 열도록 설정합니다.

이렇게 하면 사용자가 링크를 클릭했을 때 다음과 같은 것을 보게 됩니다(오버레이 제외)

사용자가 해당 페이지를 마친 후, 첫 번째 탭으로 돌아가 부트박스를 닫고 첫 번째 알림을 읽은 다음 사이트를 계속 사용할 수 있습니다.

이 방식은 해시를 추가하거나 확인하는 것조차 필요하지 않습니다. 예시 스니펫은 다음과 같습니다.

import { withPluginApi } from "discourse/lib/plugin-api";
import bootbox from "bootbox";

export default {
  name: "first-login-bootbox",
  initialize() {
    withPluginApi("0.8", api => {
      const user = api.getCurrentUser();
      if (!user) return;

      if (!user.read_first_notification && !user.enforcedSecondFactor) {
        const text = `Lorem ipsum dolor sit amet <a href="http://my.site.com/pub/bentley-flying-spur-s-production-milestone" target="_blank">링크</a>, consectetur adipiscing elit, sed do eiusmod tempor`;
        bootbox.alert(text);
      }
    });
  }
};

"/new-topic"나 “/my/preferences” 같은 링크를 팝업 안에 포함시키고 싶습니다.

그렇지 않으면, 현재 URL을 확인하여 팝업을 홈 페이지에서만 표시하는 방법을 고려하고 있었습니다.
그래서 다른 페이지를 방문할 때는 팝업이 표시되지 않도록 하려 합니다.

“/new-topic” 링크를 클릭한 후 홈페이지 URL을 확인하더라도 위의 문제가 발생합니다

사우라브님, 안녕하세요.
현재 URL을 확인하고 팝업을 홈페이지에서만 표시하면 작동할 것 같습니다.

일정 시간, 예를 들어 2분 후에 디스코봇(Discobot)으로 신규 사용자 소개를 표시할 수 있습니다:

안녕하세요, @Johani

아래와 같이 위젯을 생성하고 그 안에서 bootbox()를 호출할 수 있을까요?
그리고 bootbox를 호출한 후 "skipNewUserTips()" 메서드를 호출할 수 있을까요? 이 메서드는 header-notifications 위젯에서도 Discourse가 사용 중인 메서드입니다.

<script type="text/discourse-plugin" version="0.8">
 const { h } = require('virtual-dom');
 const { ajax } = require("discourse/lib/ajax").default;
 const DiscourseURL =  require("discourse/lib/url");
 const { userPath } = require("discourse/lib/url");

api.createWidget("welcome-popup", {
  tagName: "div.welcome-popup",
  html(attrs) {
      let user = this.currentUser;
      if (!user) return;
      if (
        !user.get("read_first_notification") &&
        !user.get("enforcedSecondFactor")
      ) {
          const title = `<div class="first-login-bootbox-title">You're a member. Welcome aboard!</div>`;
          const body = `<div class="first-login-bootbox-body">Now you can: </br> <ol class="user-suggestions"><li><a href="/new-topic" target="_blank" >Ask a question or start a discussion</a></li> <li><a href="/my/preferences/tags" target="_blank">Set up your notification settings</a></li></ol></div>`;
          bootbox.alert({
                        title: title,
                        message: body
                    });
            this.skipNewUserTips();        
      }
      return null;
  },

  skipNewUserTips() {

    ajax(userPath(this.currentUser.username_lower), {
      type: "PUT",
      data: {
        skip_new_user_tips: true,
      },
    }).then(() => {
      this.currentUser.set("skip_new_user_tips", true);
    });
  },
});
</script>

<script
  type="text/x-handlebars"
  data-template-name="/connectors/below-site-header/welcome-popup"
>
  {{mount-widget widget="welcome-popup"}}
</script> 

header-notifications 위젯의 Discourse 코드:
https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/app/widgets/header.js#L101

이렇게 하면 문제가 생길까요?

이 방법은 모든 신규 사용자에 대해 이 오버레이를 완전히 건너뜁니다. 따라서 사용자는 절대 이를 볼 수 없게 됩니다.

이것이 괜찮다면, 네, 작동할 것입니다.

다만, bootbox 호출을 변경해야 합니다. 그렇지 않으면 다음과 같은 결과가 나타납니다.

다음과 같이 처리하는 대신

단일 인자만 전달해야 합니다.

해당 옵션들은 Bootbox의 더 새로운 버전에서 추가된 것이며, 현재 Discourse에서 사용 중인 버전에서는 작동하지 않습니다(내부적으로 논의 중이지만, 당장은 사용할 수 없습니다).

또한, PUT 요청을 생성하는 중이므로 이 부분도 건너뛸 수 있습니다.

this.currentUser.set("skip_new_user_tips", true);

따라서 다음과 같은 방식으로 처리해 볼 수 있습니다.

  api.createWidget("welcome-popup", {
    tagName: "div.welcome-popup",
    html(attrs) {
      let user = this.currentUser;
      if (!user) return;
      if (
        !user.get("read_first_notification") &&
        !user.get("enforcedSecondFactor")
      ) {
        const body = `<h2 class="first-login-bootbox-title">회원 가입을 환영합니다!</h2>
                      <hr>
                      <div class="first-login-bootbox-body">
                        이제 다음을 수행할 수 있습니다:
                        <br>
                        <ol class="user-suggestions">
                          <li>
                            <a href="/new-topic" target="_blank"
                              >질문을 하거나 토론을 시작하기</a
                            >
                          </li>
                          <li>
                            <a href="/my/preferences/tags" target="_blank"
                              >알림 설정 구성하기</a
                            >
                          </li>
                        </ol>
                      </div>`;
        bootbox.alert(body);
        this.skipNewUserTips();
      }
      return null;
    },

    skipNewUserTips() {
      ajax(userPath(this.currentUser.username_lower), {
        type: "PUT",
        data: {
          skip_new_user_tips: true
        }
      });
    }
  });

Johani 님, 안녕하세요.

skip_new_user_tips를 true로 설정했기 때문에 Discobot 인사 알림도 함께 건너뛰어집니다. 첫 번째 알림 오버레이 마스크가 표시되지 않는 것은 괜찮지만, 우리는 Discobot 인사 알림은 표시되기를 원합니다.

Discobot 알림:

이를 위해 모든 신규 사용자에게 Discobot 인사 알림이 로드되도록 코드를 수정했습니다.
이제 헤더 위젯에 "closeFirstNotificationMask()"라는 새 메서드를 추가하여 "ringBackdrop"을 false로, "userVisible"을 그 반대값으로 설정하도록 했습니다. 그리고 welcome-popup 위젯에서 bootbox 메서드를 호출한 후에 이 메서드를 호출합니다.

이는 헤더 위젯의 “toggeleUserMenu” 메서드를 참고하여 구현했으며, 이 메서드는 사용자 아이콘이 클릭될 때 호출됩니다.

<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/5.4.1/bootbox.min.js" integrity="sha512-eoo3vw71DUo5NRvDXP/26LFXjSFE1n5GQ+jZJhHz+oOTR4Bwt7QBCjsgGvuVMQUMMMqeEvKrQrNEI4xQMXp3uA==" crossorigin="anonymous"></script>

<script type="text/discourse-plugin" version="0.8">
 const { h } = require('virtual-dom');
 const { ajax } = require("discourse/lib/ajax").default;

/* 환영 팝업 표시 후에 호출될 헤더 위젯의 새 메서드 추가 */
api.reopenWidget("header",{
    closeFirstNotificationMask() {
        this.state.ringBackdrop = false;
        this.state.userVisible = !this.state.userVisible;
        this.toggleBodyScrolling(this.state.userVisible);
    }
});

/* 새 위젯 "welcome-popup" 생성, 이 위젯은 사용자가 처음 로그인할 때만 렌더링됩니다 */
api.createWidget("welcome-popup", {
  tagName: "div.welcome-popup",
  html(attrs) {
      let user = this.currentUser;
      if (!user) return;
      if ( !user.get("read_first_notification") && !user.get("enforcedSecondFactor") ) {
          const title = `<div class="first-login-bootbox-title">You're a member. Welcome aboard!</div>`;
          const body = `<div class="first-login-bootbox-body">Now you can: </br> <ol class="user-suggestions"><li><a href="/new-topic" target="_blank" >Ask a question or start a discussion</a></li> <li><a href="/my/preferences/tags" target="_blank">Set up your notification settings</a></li></ol></div>`;
          
          bootbox.alert({
                        title: title,
                        message: body
                    });
            // 헤더 위젯의 closeFirstNotificationMask 액션 메서드 호출
            this.sendWidgetAction("closeFirstNotificationMask", this.attrs);
        }
      return null;
  },
  
});

/* 아래 코드는 사용자가 처음 로그인할 때 "header-notifications" 위젯 렌더링 후 "welcome-popup" 위젯을 렌더링합니다 */
api.decorateWidget("header-notifications:after", helper => { 
    if(!helper.attrs.active && helper.attrs.ringBackdrop){
        return helper.attach("welcome-popup", helper.attrs);     
    }else{
        return null;    
    }
});
</script>

한 가지 더, 현재 Discourse가 다중 매개변수 bootbox 메서드를 지원하지 않기 때문에 테마 구성 요소에 아래 스크립트를 포함하여 Bootbox 5.4.1 버전을 사용하고 있습니다. 이렇게 하는 것이 괜찮을까요?

<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/5.4.1/bootbox.min.js" integrity="sha512-eoo3vw71DUo5NRvDXP/26LFXjSFE1n5GQ+jZJhHz+oOTR4Bwt7QBCjsgGvuVMQUMMMqeEvKrQrNEI4xQMXp3uA==" crossorigin="anonymous"></script>

의견을 주시면 감사하겠습니다!

좋아 보입니다 :+1:

로컬에서 테스트해 보았지만 문제가 발견되지 않았습니다. 몇 가지 사소한 수정을 했습니다.

<script
  src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/5.4.1/bootbox.min.js"
  integrity="sha512-eoo3vw71DUo5NRvDXP/26LFXjSFE1n5GQ+jZJhHz+oOTR4Bwt7QBCjsgGvuVMQUMMMqeEvKrQrNEI4xQMXp3uA=="
  crossorigin="anonymous"
></script>

<script type="text/discourse-plugin" version="0.8">
  const user = api.getCurrentUser();
  if (!user || user.read_first_notification || user.enforcedSecondFactor) {
    return;
  }

  const bootboxTitle = `<div class="first-login-bootbox-title">회원가입이 완료되었습니다. 환영합니다!</div>`;
  const bootboxBody = `<div class="first-login-bootbox-body">이제 다음을 수행할 수 있습니다: </br> <ol class="user-suggestions"><li><a href="/new-topic" target="_blank" >질문을 하거나 토론을 시작하기</a></li> <li><a href="/my/preferences/tags" target="_blank">알림 설정 구성하기</a></li></ol></div>`;

  /* Welcome-popup 표시 후 호출될 header 위젯의 새로운 메서드 추가 */
  api.reopenWidget("header", {
    closeFirstNotificationMask() {
      this.state.ringBackdrop = false;
      this.state.userVisible = !this.state.userVisible;
    }
  });

  /* 사용자가 처음 로그인할 때만 렌더링되는 새로운 위젯 "welcome-popup" 생성 */
  api.createWidget("welcome-popup", {
    tagName: "div.welcome-popup",
    html(attrs) {
      // header 위젯의 closeFirstNotificationMask 액션 메서드 호출
      this.sendWidgetAction("closeFirstNotificationMask", attrs);

      bootbox.alert({
        title: bootboxTitle,
        message: bootboxBody
      });
    }
  });

  /* 아래 코드는 사용자가 처음 로그인할 때 "header-notifications" 위젯 렌더링 후 "welcome-popup" 위젯을 렌더링합니다 */
  api.decorateWidget("header-notifications:before", helper => {
    if (!helper.attrs.active && helper.attrs.ringBackdrop) {
      return helper.attach("welcome-popup", helper.attrs);
    }
  });
</script>

네, 괜찮습니다. Discourse는 우리가 사용하는 bootbox 버전을 shim으로 로드하므로, 테마 컴포넌트에서 다른 버전을 로드해도 코어에 아무런 변경이 일어나지 않습니다. 새 버전은 오직 테마 컴포넌트 내에서만 사용됩니다. 유일한 단점은 초기 페이지 로드 시 요청이 하나 추가되고 약 4kb의 용량이 증가한다는 것입니다.

@Johani님, 안녕하세요. welcome-popup 위젯을 header-notifications 위젯보다 먼저 로드하셨기 때문에 Discobot 알림이 로드되지 않는 것으로 보입니다. 또한 welcome-popup 내부의 링크를 클릭한 후에도 다시 welcome-popup이 사용자에게 표시되는 문제가 있습니다.

저는 header-notifications 위젯 이후에 welcome-popup 위젯을 로드하도록 테스트해 보았을 때 정상적으로 작동하는 것을 확인했습니다. 따라서 이를 "header-notifications:after"로 변경해도 될까요?

이상하네요 :thinking:

방금 3명의 다른 신규 사용자를 대상으로 동일한 코드를 다시 테스트해 보았는데, 매번 예상한 결과가 나왔습니다. 첫 페이지 조회 시 팝업이 표시되고, Discobot 인사 메시지가 전송됩니다. 이후 페이지 조회 시에는 팝업이 표시되지 않습니다.

네, 귀하에게는 잘 작동하는 것 같으니, 데코레이터의 위치는 중요하지 않을 것입니다.

@Johani 감사합니다. 데코레이터 뒤에 사용할게요.