최근 답변이 표시되는 데 2초가 걸립니다

가장 최근의 답변을 즉시 표시하는 방법이 있을까요? 현재 이 코드를 사용 중입니다:

  api.onPageChange(() => {
    if (window.location.pathname === "/") {
      const container = document.querySelector(".latest-topic-list");
      if (!container || container.dataset.modified === "true") return;

      fetch("/posts.json?order=created")
        .then(res => res.json())
        .then(data => {
          const replies = data.latest_posts
            .filter(p => p.post_number > 1 && !p.topic_slug.includes("private-message"))
            .slice(0, 15);

          const topicFetches = replies.map(post =>
            fetch(`/t/${post.topic_id}.json`)
              .then(res => res.json())
              .then(topic => {
                return {
                  post,
                  category: topic.category_id ? topic.category_name : null,
                  tags: topic.tags || []
                };
              })
          );

          Promise.all(topicFetches).then(results => {
            const rows = results.map(({ post, category, tags }) => {
              const url = `/t/${post.topic_slug}/${post.topic_id}/${post.post_number}`;
              const avatarUrl = post.avatar_template.replace("{size}", "45");
              const excerpt = post.excerpt?.replace(/<\/?[^>]+(>|$)/g, "")?.slice(0, 120) + (post.excerpt?.length > 120 ? '...' : '') || '';

              const categoryHtml = category
                ? `<span style="font-size: 0.85em; color: #666;">Categoria: <strong>${category}</strong></span><br>`
                : '';

              const tagsHtml = tags.length
                ? `<span style="font-size: 0.85em; color: #666;">Tags: ${tags.map(tag => `<span style="background:#eee; padding:2px 6px; border-radius:3px; margin-right:4px;">${tag}</span>`).join("")}</span>`
                : '';

              return `
                <tr class="topic-list-item">
                  <td class="main-link clearfix">
                    <div style="display: flex; align-items: center; gap: 16px; padding: 8px 0;">
                      <div style="flex-shrink: 0;">
                        <a class="avatar-link" href="/u/${post.username}">
                          <img loading="lazy" width="45" height="45" src="${avatarUrl}" class="avatar" alt="${post.username}">
                        </a>
                      </div>
                      <div style="display: flex; flex-direction: column; justify-content: center; padding-top: 8px; padding-bottom: 8px;">
                        <span class="link-top-line" style="margin-bottom: 6px;">
                          <a href="${url}" class="title raw-link">${excerpt}</a>
                        </span>
                        <div class="link-bottom-line">
                          ${categoryHtml}
                          ${tagsHtml}
                        </div>
                      </div>
                    </div>
                  </td>
                </tr>
              `;
            }).join("");

            // Cria o contêiner da seção de últimos comentários
            const latestRepliesContainer = document.createElement("div");
            latestRepliesContainer.className = "latest-replies-container";
            latestRepliesContainer.style.marginTop = "2em";

            latestRepliesContainer.innerHTML = `
              <table class="topic-list latest-topic-list">
                <thead>
                  <tr>
                    <th class="default">Últimos Comentários</th>
                  </tr>
                </thead>
                <tbody>
                  ${rows}
                </tbody>
              </table>
            `;

            container.parentNode.insertBefore(latestRepliesContainer, container.nextSibling);
            container.dataset.modified = "true";
          });
        })
        .catch(error => {
          console.error("Erro ao buscar últimos comentários:", error);
        });
    }
  });
</script>

하지만 콘텐츠가 표시되는 데 평균 2초 정도 걸립니다. 오른쪽 사이드바의 “최근 답변” 블록에서도 동일한 현상이 발생합니다. 이것이 정상적인가요?

왜 코드를 추가하시나요?

이것은 특별한 설정 없이 바로 작동합니다. 당신에게는 오류가 발생하나요?

그분의 문제입니다.

코드가 여러 API 요청을 수행하기 때문에 예상되는 현상이라고 봅니다.
최신 게시물을 가져온 후, 카테고리 이름을 가져오기 위해 각 주제 ID(여기서는 15개)마다 요청을 한 번씩 보내고 있습니다.

현재 플러그인을 사용하여 맞춤형 SQL 쿼리를 실행하는 것 외에는 다른 방법이 있는지 모릅니다.

정확합니다.
하지만 오른쪽 사이드바 블록도 요청을 보내나요? 최근 답변에 대해 동일한 결과를 반환합니다. 표시되는데 2초가 걸립니다.

플러그인을 만드는 방법을 알아볼게요. 감사합니다

네, 최신 게시물을 가져오는 방식은 동일합니다. 하지만 거기까지입니다. 카테고리 이름을 가져오려고 하지 않는데, 이것이 차이점입니다.

알겠습니다. 도움을 주셔서 감사합니다.

다른 해결책을 찾아보겠습니다.

결국, 지금은 이것을 사용하고 있습니다:

<script type="text/discourse-plugin" version="0.11.3">
  api.onPageChange(() => {
    if (window.location.pathname === "/") {
      const container = document.querySelector(".latest-topic-list");
      if (!container) return;
      
      // 초기화 중복 방지
      if (window.latestRepliesInitialized) return;
      window.latestRepliesInitialized = true;
      
      // 설정
      const POLLING_INTERVAL = 2000; // 2초
      const COMMENTS_TO_SHOW = 15;
      const CACHE_DURATION = 30 * 60 * 1000; // 30분
      
      // 캐시 키
      const CACHE_KEY = "discourse_latest_replies_data";
      const CACHE_TIMESTAMP_KEY = "discourse_latest_replies_timestamp";
      const CACHE_LAST_ID_KEY = "discourse_latest_replies_last_id";
      
      // 비교를 위해 마지막으로 확인된 게시글 ID 저장
      let lastSeenPostId = parseInt(localStorage.getItem(CACHE_LAST_ID_KEY) || "0");
      let pollingIntervalId = null;
      
      console.log(`최근 댓글 플러그인 초기화 중 (캐시된 마지막 ID: ${lastSeenPostId})`);
      
      // 댓글을 불러오는 함수
      function loadLatestReplies(silent = false, forceRefresh = false) {
        // 강제 새로고침이 아닌 경우 먼저 캐시를 확인합니다.
        if (!forceRefresh) {
          const cachedData = localStorage.getItem(CACHE_KEY);
          const cacheTimestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY);
          const now = Date.now();
          
          // 유효한 캐시 데이터가 있는 경우
          if (cachedData && cacheTimestamp && now - parseInt(cacheTimestamp) < CACHE_DURATION) {
            try {
              const results = JSON.parse(cachedData);
              console.log(`캐시된 데이터 사용 중 (${results.length}개 댓글, ${Math.round((now - parseInt(cacheTimestamp)) / 1000 / 60)}분 전의 캐시)`);
              renderLatestReplies(results, false);
              
              // 조용한 모드가 아니라면 더 이상 할 일이 없습니다.
              if (!silent) {
                return;
              }
              
              // 조용한 모드라면 업데이트 확인을 위해 계속 진행합니다.
            } catch (e) {
              console.error("캐시 처리 중 오류:", e);
              // 캐시에 오류가 있으면 최신 데이터를 가져오기 위해 계속 진행합니다.
            }
          } else if (cachedData) {
            console.log("캐시 만료, 최신 데이터 가져오는 중");
          } else {
            console.log("캐시 없음, 최신 데이터 가져오는 중");
          }
        } else {
          console.log("새로고침 강제, 캐시 무시");
        }
        
        // 조용한 모드가 아니라면 로딩 표시기를 표시합니다.
        if (!silent) {
          // 이미 댓글 컨테이너가 존재하는 경우 표시기를 표시하지 않습니다.
          const existingContainer = document.querySelector(".latest-replies-container");
          if (!existingContainer) {
            let loadingIndicator = document.getElementById("latest-replies-loading");
            if (!loadingIndicator) {
              loadingIndicator = document.createElement("div");
              loadingIndicator.id = "latest-replies-loading";
              loadingIndicator.innerHTML = `
                <div style="text-align: center; padding: 20px;">
                  <span class="spinner small"></span>
                  <span style="margin-left: 10px;">최근 댓글 로딩 중...</span>
                </div>
              `;
              container.parentNode.insertBefore(loadingIndicator, container.nextSibling);
            }
          }
        }
        
        // 최신 데이터를 가져옵니다.
        fetch("/posts.json?order=created")
          .then(res => res.json())
          .then(data => {
            // 진단용 로그
            if (!silent) {
              console.log("API에서 받은 데이터:", data.latest_posts.length);
            }
            
            const replies = data.latest_posts
              .filter(p => p.post_number > 1 && !p.topic_slug.includes("private-message"))
              .slice(0, COMMENTS_TO_SHOW);
            
            // 새 게시글이 있는지 확인합니다.
            const maxId = replies.length > 0 ? Math.max(...replies.map(post => post.id)) : 0;
            const hasNewPosts = maxId > lastSeenPostId;
            
            // 진단용 로그
            if (hasNewPosts && !silent) {
              console.log(`새 게시글 감지됨. 마지막 ID: ${lastSeenPostId}, 새로운 최대 ID: ${maxId}`);
            }
            
            // 마지막으로 확인된 ID를 업데이트합니다.
            if (maxId > lastSeenPostId) {
              lastSeenPostId = maxId;
              localStorage.setItem(CACHE_LAST_ID_KEY, lastSeenPostId.toString());
            }
            
            // 새 게시글이 없고 조용한 확인인 경우 아무것도 하지 않습니다.
            if (!hasNewPosts && silent && !forceRefresh) {
              return;
            }
            
            // 토픽 세부 정보를 가져옵니다.
            const topicPromises = replies.map(post => {
              // 토픽이 캐시에 있는지 확인합니다.
              const topicCacheKey = `discourse_topic_${post.topic_id}`;
              const cachedTopic = localStorage.getItem(topicCacheKey);
              
              if (cachedTopic && !forceRefresh) {
                try {
                  return Promise.resolve(JSON.parse(cachedTopic));
                } catch (e) {
                  console.error(`토픽 ${post.topic_id} 캐시 처리 중 오류:`, e);
                  // 캐시에 오류가 있으면 서버에서 가져옵니다.
                }
              }
              
              return fetch(`/t/${post.topic_id}.json`)
                .then(res => res.json())
                .then(topic => {
                  // 토픽을 캐시에 저장합니다.
                  localStorage.setItem(topicCacheKey, JSON.stringify(topic));
                  return topic;
                })
                .catch(error => {
                  console.error(`토픽 ${post.topic_id} 가져오기 오류:`, error);
                  return { category_id: null, category_name: null, tags: [] };
                });
            });
            
            Promise.all(topicPromises)
              .then(topics => {
                const results = replies.map((post, index) => {
                  const topic = topics[index];
                  return {
                    post,
                    category: topic.category_id ? topic.category_name : null,
                    tags: topic.tags || []
                  };
                });
                
                // 결과를 캐시에 저장합니다.
                localStorage.setItem(CACHE_KEY, JSON.stringify(results));
                localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString());
                
                // 결과를 렌더링합니다.
                renderLatestReplies(results, hasNewPosts || forceRefresh);
              })
              .catch(error => {
                console.error("토픽 처리 중 오류:", error);
              });
          })
          .catch(error => {
            console.error("최근 댓글 가져오기 오류:", error);
            // 오류 발생 시 로딩 표시기를 제거합니다.
            if (!silent) {
              const loadingElement = document.getElementById("latest-replies-loading");
              if (loadingElement) loadingElement.remove();
            }
          });
      }
      
      // 결과를 렌더링하는 함수
      function renderLatestReplies(results, animate = false) {
        // 로딩 표시기를 제거합니다.
        const loadingElement = document.getElementById("latest-replies-loading");
        if (loadingElement) loadingElement.remove();
        
        // 결과가 없으면 아무것도 하지 않습니다.
        if (!results || results.length === 0) {
          console.log("표시할 댓글을 찾을 수 없음");
          return;
        }
        
        const rows = results.map(({ post, category, tags }) => {
          const url = `/t/${post.topic_slug}/${post.topic_id}/${post.post_number}`;
          const avatarUrl = post.avatar_template.replace("{size}", "45");
          const excerpt = post.excerpt?.replace(/<\/?[^>]+(>|$)/g, "")?.slice(0, 120) + (post.excerpt?.length > 120 ? '...' : '') || '';

          const categoryHtml = category
            ? `<span style="font-size: 0.85em; color: #666;">카테고리: <strong>${category}</strong></span><br>`
            : '';

          const tagsHtml = tags.length
            ? `<span style="font-size: 0.85em; color: #666;">태그: ${tags.map(tag => `<span style="background:#eee; padding:2px 6px; border-radius:3px; margin-right:4px;">${tag}</span>`).join("")}</span>`
            : '';

          const animationClass = animate ? 'new-comment' : '';

          return `
            <tr class="topic-list-item ${animationClass}" data-post-id="${post.id}">
              <td class="main-link clearfix">
                <div style="display: flex; align-items: center; gap: 16px; padding: 8px 0;">
                  <div style="flex-shrink: 0;">
                    <a class="avatar-link" href="/u/${post.username}">
                      <img loading="lazy" width="45" height="45" src="${avatarUrl}" class="avatar" alt="${post.username}">
                    </a>
                  </div>
                  <div style="display: flex; flex-direction: column; justify-content: center; padding-top: 8px; padding-bottom: 8px;">
                    <span class="link-top-line" style="margin-bottom: 6px;">
                      <a href="${url}" class="title raw-link">${excerpt}</a>
                    </span>
                    <div class="link-bottom-line">
                      ${categoryHtml}
                      ${tagsHtml}
                    </div>
                  </div>
                </div>
              </td>
            </tr>
          `;
        }).join("");

        // 애니메이션 스타일이 아직 없으면 추가합니다.
        if (!document.getElementById('latest-replies-style')) {
          const style = document.createElement('style');
          style.id = 'latest-replies-style';
          style.textContent = `
            @keyframes highlightNew {
              0% { background-color: rgba(255, 255, 0, 0.3); }
              100% { background-color: transparent; }
            }
            .new-comment {
              animation: highlightNew 2s ease-out;
            }
          `;
          document.head.appendChild(style);
        }

        // 기존 컨테이너가 있으면 제거합니다.
        const existingContainer = document.querySelector(".latest-replies-container");
        if (existingContainer) {
          existingContainer.remove();
        }

        // 최근 댓글 섹션의 컨테이너를 생성합니다.
        const latestRepliesContainer = document.createElement("div");
        latestRepliesContainer.className = "latest-replies-container";
        latestRepliesContainer.style.marginTop = "2em";

        // 수동 새로고침 버튼과 상태 표시기를 추가합니다.
        const cacheTime = new Date(parseInt(localStorage.getItem(CACHE_TIMESTAMP_KEY) || Date.now()));
        const formattedTime = cacheTime.toLocaleTimeString();
        
        latestRepliesContainer.innerHTML = `
          <table class="topic-list latest-topic-list">
            <thead>
              <tr>
                <th class="default">
                  최근 댓글
                  <span id="comments-status" style="font-size: 0.8em; font-weight: normal; margin-left: 10px;">
                  </span>
                  <button id="refresh-comments" class="btn btn-flat no-text btn-icon" style="float: right;" title="댓글 새로고침">
                    <svg class="fa d-icon d-icon-sync svg-icon svg-string" width="16" height="16" aria-hidden="true"><use xlink:href="#sync"></use></svg>
                  </button>
                </th>
              </tr>
            </thead>
            <tbody id="latest-replies-tbody">
              ${rows}
            </tbody>
          </table>
        `;

        container.parentNode.insertBefore(latestRepliesContainer, container.nextSibling);
        container.dataset.modified = "true";
        
        // 새로고침 버튼에 클릭 이벤트를 추가합니다.
        document.getElementById("refresh-comments").addEventListener("click", function() {
          // 상태 텍스트를 업데이트합니다.
          const statusElement = document.getElementById("comments-status");
          if (statusElement) {
            statusElement.textContent = "(새로고침 중...)";
          }
          
          // 새로고침을 강제합니다.
          loadLatestReplies(false, true);
        });
        
        console.log(`댓글 ${results.length}개 렌더링 완료`);
      }
      
      // 초기 로딩을 시작합니다 (캐시 사용).
      loadLatestReplies(false, false);
      
      // 고빈도 폴링을 설정합니다.
      pollingIntervalId = setInterval(() => {
        // 사용자가 홈 페이지에 있을 때만 업데이트합니다.
        if (window.location.pathname === "/") {
          loadLatestReplies(true, false); // 조용한 모드, 사용 가능하면 캐시 사용
        }
      }, POLLING_INTERVAL);
      
      console.log(`폴링이 ${POLLING_INTERVAL}ms 간격으로 설정됨`);
      
      // 사용자가 페이지를 떠날 때 간격을 정리합니다.
      api.onPageChange((url) => {
        if (url !== "/") {
          console.log("홈 페이지에서 나감, 리소스 정리");
          
          if (pollingIntervalId) {
            clearInterval(pollingIntervalId);
            pollingIntervalId = null;
          }
          
          window.latestRepliesInitialized = false;
        }
      });
    }
  });
</script>

기대했던 대로 작동하고 있습니다. 새 댓글이 자동으로 추가되고, 그 후 캐시에 추가됩니다. 이것이 이상적인 방법은 아니라는 것을 알고 있습니다. 곧 플러그인을 만들어 보겠습니다.

여기서 목표는: 최신 토픽 페이지 스타일의 카테고리가 아니라, 최신 답변이 있는 카테고리를 만드는 것입니다.