| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- /**
- * 2026.01.07 작성
- * 태그별 게시물 조회 JS
- */
- class ProcessTags
- {
- constructor()
- {
- this.loading = false;
- this.tags = document.getElementById('tags');
- this.posts = document.getElementById('posts');
- this.pagination = document.getElementById('pagination');
- }
- setActiveTag(target)
- {
- const links = this.tags.querySelectorAll('.tag-nav-link');
- links.forEach(link => link.classList.remove('active'));
- target.classList.add('active');
- }
- updateQueryString(tag, page)
- {
- const url = new URL(window.location.href);
-
- if (tag) {
- url.searchParams.set('name', tag);
- } else {
- url.searchParams.delete('name');
- }
- if (page > 1) {
- url.searchParams.set('page', page.toString());
- } else {
- url.searchParams.delete('page');
- }
- window.history.pushState({}, '', url.toString());
- }
- getTagFromQuery()
- {
- return new URLSearchParams(window.location.search).get('name')?.trim();
- }
- getPageFromQuery() {
- return parseInt(new URLSearchParams(window.location.search).get('page') || '1', 10) || 1;
- }
- findLinkByTag(tag)
- {
- if (!tag) {
- return null;
- }
- const links = this.tags.querySelectorAll('.tag-nav-link');
- for (const link of links) {
- const name = (link.dataset.name || link.textContent || '').trim();
- if (name === tag) {
- return link;
- }
- }
- return null;
- }
- async loadPosts(tag, page)
- {
- const params = new URLSearchParams({
- name: tag,
- page: page
- });
- this.updateQueryString(tag, page);
- const nav = this.findLinkByTag(tag);
- if (nav) {
- this.setActiveTag(nav);
- }
- this.posts.innerHTML = ' \
- <div class="text-center my-4"> \
- <div class="spinner-border" role="status"> \
- <span class="visually-hidden">Loading...</span> \
- </div> \
- </div> \
- ';
- try {
- if (this.loading) {
- return;
- }
- this.loading = true;
- const res = await fetch(`${BASE_URL}/tag/posts?${params.toString()}`, {
- headers: {
- 'X-Requested-With': 'XMLHttpRequest',
- 'Accept': 'application/json',
- }
- });
- if (!res.ok) {
- throw new Error();
- }
- const data = await res.json().then(r => r.data);
- this.renderPosts(data);
- this.renderPagination(data);
-
- } catch (error) {
- this.posts.innerHTML = `
- <p>
- 조회 중 오류가 발생했습니다.<br/>
- 잠시 후 다시 시도해주세요.
- </p>
- `;
- } finally {
- this.loading = false;
- }
- }
- async fetchPosts(e)
- {
- const target = e.target.closest?.('.tag-nav-link, .tag-sub-link, .page-link');
- if (!target) {
- return;
- }
- if (target.classList.contains('active') && target.classList.contains('tag-nav-link')) {
- this.posts.innerHTML = '<div class="text-center my-4">태그를 선택하면 게시글이 조회됩니다.</div>';
- this.pagination.innerHTML = '';
- target.classList.remove('active');
- this.updateQueryString('', 0);
- return;
- }
- let tag = (target.dataset.name || target.textContent || '').trim();
- let page = 1;
- if (target.classList.contains('page-link')) {
- e.preventDefault();
- tag = this.getTagFromQuery();
- page = parseInt(target.dataset.page || '1', 10) || 1;
- }
- return this.loadPosts(tag, page);
- }
- renderPosts(res)
- {
- if (!res || res.rows <= 0) {
- this.posts.innerHTML = '<p class="p-5 text-center">관련 게시물이 없습니다.</p>';
- return;
- }
- let html = "";
- res.list.forEach(function(item) {
- html += `
- <li class="list-group-item">
- <div class="row align-items-center">
- ${item.thumbnail ? `
- <div class="col col-lg-2">
- <a href="${BASE_URL}/board/photo/${item.id}" target="_blank" rel="noopener">
- <img src="${item.thumbnail}" alt="${item.subject}" class="img-fluid img-thumbnail"/>
- </a>
- </div>
- ` : ''}
- <div class="${item.thumbnail ? `col col-lg-10` : 'col'}">
- <dl class="mb-0">
- <dt>
- <div class="row">
- <div class="col">
- <h4>
- <a href="${BASE_URL}/board/photo/${item.id}" target="_blank" rel="noopener">
- ${item.subject}
- </a>
- </h4>
- </div>
- <div class="col-auto">
- <small>${item.createdAt}</small>
- </div>
- </div>
- </dt>
- <dd>
- <blockquote class="blockquote">
- ${item.content}
- </blockquote>
- </dd>
- <dd>${item.userName}</dd>
- <dd class="mb-0">
- ${item.tags.map(tag => `
- <span class="tag-sub-link" data-name="${tag.toString().replace(/"/g, '"')}">
- #${tag}
- </span>
- `).join(',')}
- </dd>
- </dl>
- </div>
- </div>
-
- </li>
- `;
- });
- this.posts.innerHTML = html;
- }
-
- renderPagination(res)
- {
- const total = parseInt(res.total || '0', 0) || 0;
- const page = parseInt(res.page || '1', 10) || 1;
- const last = Math.ceil(total / 10);
- if (last <= 1) {
- return;
- }
- const make = (p, label, disabled = false, active = false) => `
- <li class="page-item ${disabled ? 'disabled' : ''} ${active ? 'active' : ''}">
- <a class="page-link" href="#" data-page="${p}">${label}</a>
- </li>
- `;
- let html = `<hr/><nav><ul class="pagination">`;
- html += make(page - 1, '‹', page <= 1);
- // 간단히 1~last 다 찍으면 페이지 많을 때 별로라서 window 방식(최대 7개)
- const windowSize = 7;
- let start = Math.max(1, page - Math.floor(windowSize / 2));
- let end = Math.min(last, start + windowSize - 1);
- start = Math.max(1, end - windowSize + 1);
- for (let p = start; p <= end; p++) {
- html += make(p, String(p), false, p === page);
- }
- html += make(page + 1, '›', page >= last);
- html += `</ul></nav>`;
- this.pagination.innerHTML = html;
- }
- initFromUrl()
- {
- const tag = this.getTagFromQuery();
- if (!tag) {
- return;
- }
- const link = this.findLinkByTag(tag);
- if (!link) {
- return;
- }
- // 새로고침 시 active 유지
- this.setActiveTag(link);
- // 새로고침 시 자동 조회
- this.fetchPosts({ target: link });
- }
- }
- document.addEventListener('DOMContentLoaded', function()
- {
- let processTags = new ProcessTags();
- if (processTags.tags) {
- processTags.tags.addEventListener('click', (e) => processTags.fetchPosts(e));
- }
- if (processTags.posts) {
- processTags.posts.addEventListener('click', (e) => processTags.fetchPosts(e));
- }
- if (processTags.pagination) {
- processTags.pagination.addEventListener('click', (e) => processTags.fetchPosts(e));
- }
- // 새로고침/직접접속 시 URL의 tag 반영
- processTags.initFromUrl();
- // 뒤로가기/앞으로가기 대응
- window.addEventListener('popstate', () => processTags.initFromUrl());
- });
|