import { AllCommunityModule, ModuleRegistry, createGrid, } from "ag-grid-community"; import "ag-grid-community/styles/ag-grid.css"; import "ag-grid-community/styles/ag-theme-quartz.css"; ModuleRegistry.registerModules([AllCommunityModule]); const DEFAULT_SELECTOR = "[data-portfolio-list-panel]"; const DEFAULT_PAGE_SIZE = 5; function toDate(value) { if (!value) { return null; } const date = new Date(value); return Number.isNaN(date.getTime()) ? null : date; } function formatDate(value) { const date = toDate(value); if (!date) { return ""; } return new Intl.DateTimeFormat("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit", }).format(date); } function formatRelativeDate(value) { const date = toDate(value); if (!date) { return ""; } const diffMs = Date.now() - date.getTime(); if (diffMs < 0) { return formatDate(value); } const diffMinutes = Math.floor(diffMs / 60000); if (diffMinutes < 1) { return "방금 전"; } if (diffMinutes < 60) { return `${diffMinutes}분 전`; } const diffHours = Math.floor(diffMinutes / 60); if (diffHours < 24) { return `${diffHours}시간 전`; } return formatDate(value); } function getPostUrl(portfolioIdntfNo) { return `/portfolio/${encodeURIComponent(portfolioIdntfNo)}`; } function createTitleCell(params) { const link = document.createElement("a"); link.className = "portfolio-post-title-link"; link.href = getPostUrl(params.data.portfolioIdntfNo); link.textContent = params.value || ""; link.title = params.value || ""; return link; } function createPageButton(label, options = {}) { const button = document.createElement("button"); button.type = "button"; button.className = "portfolio-post-page-button"; button.textContent = label; if (options.active) { button.classList.add("is-active"); button.setAttribute("aria-current", "page"); } if (options.disabled) { button.disabled = true; } if (typeof options.onClick === "function") { button.addEventListener("click", options.onClick); } return button; } function renderPagination(gridApi, paginationElement) { if (!gridApi || !paginationElement) { return; } const totalPages = gridApi.paginationGetTotalPages(); const currentPage = gridApi.paginationGetCurrentPage(); paginationElement.replaceChildren(); if (totalPages <= 0) { return; } const maxVisiblePages = 10; const currentPageGroup = Math.floor(currentPage / maxVisiblePages); const startPage = currentPageGroup * maxVisiblePages; const endPage = Math.min(startPage + maxVisiblePages, totalPages); if (startPage > 0) { paginationElement.appendChild( createPageButton("이전", { onClick: () => gridApi.paginationGoToPage(startPage - 1), }), ); } for (let page = startPage; page < endPage; page += 1) { paginationElement.appendChild( createPageButton(String(page + 1), { active: page === currentPage, onClick: () => gridApi.paginationGoToPage(page), }), ); } if (endPage < totalPages) { paginationElement.appendChild( createPageButton("다음 >", { onClick: () => gridApi.paginationGoToPage(endPage), }), ); } } function createGridOptions(rows, currentId, paginationElement) { return { rowData: rows, columnDefs: [ { field: "title", headerName: "글 제목", flex: 1, minWidth: 260, cellRenderer: createTitleCell, }, { field: "viewCount", headerName: "조회수", width: 96, type: "numericColumn", headerClass: "portfolio-post-number-header", cellClass: "portfolio-post-number-cell", }, { field: "registeredAt", headerName: "작성일", width: 132, valueFormatter: (params) => formatRelativeDate(params.value), headerClass: "portfolio-post-date-header", cellClass: "portfolio-post-date-cell", }, ], defaultColDef: { sortable: true, resizable: false, suppressMovable: true, }, domLayout: "autoHeight", headerHeight: 34, rowHeight: 40, pagination: true, paginationPageSize: DEFAULT_PAGE_SIZE, suppressPaginationPanel: true, suppressCellFocus: true, suppressDragLeaveHidesColumns: true, suppressHorizontalScroll: true, getRowId: (params) => params.data.portfolioIdntfNo, getRowClass: (params) => params.data.portfolioIdntfNo === currentId ? "ag-row-current-post" : "", onGridReady: (params) => renderPagination(params.api, paginationElement), onPaginationChanged: (event) => renderPagination(event.api, paginationElement), localeText: { page: "페이지", more: "더 보기", to: "-", of: "/", next: "다음", last: "마지막", first: "처음", previous: "이전", loadingOoo: "불러오는 중...", noRowsToShow: "등록된 글이 없습니다.", pageSizeSelectorLabel: "줄 보기", }, }; } async function fetchPortfolioItems(listUrl) { const response = await fetch(listUrl, { headers: { Accept: "application/json", }, credentials: "same-origin", }); if (!response.ok) { throw new Error("포트폴리오 목록을 불러오지 못했습니다."); } const rows = await response.json(); return Array.isArray(rows) ? rows : []; } function initPortfolioListPanel(selector = DEFAULT_SELECTOR) { const panel = document.querySelector(selector); if (!panel) { return null; } const toggleButton = panel.querySelector("[data-portfolio-list-toggle]"); const body = panel.querySelector("[data-portfolio-list-body]"); const gridElement = panel.querySelector("[data-portfolio-list-grid]"); const paginationElement = panel.querySelector("[data-portfolio-pagination]"); const pageSizeSelect = panel.querySelector("[data-portfolio-page-size]"); const countElement = panel.querySelector(".portfolio-post-list-summary span"); const listUrl = panel.dataset.listUrl; const currentId = panel.dataset.currentId || ""; const initialOpen = panel.dataset.initialOpen === "true"; if (!toggleButton || !body || !gridElement || !paginationElement || !listUrl) { return null; } let isOpen = false; let rowsPromise = null; let gridApi = null; const loadRows = () => { rowsPromise ||= fetchPortfolioItems(listUrl); return rowsPromise; }; const renderRows = async () => { const rows = await loadRows(); if (countElement) { countElement.textContent = `${rows.length}개의 글`; } if (!gridApi) { gridApi = createGrid( gridElement, createGridOptions(rows, currentId, paginationElement), ); renderPagination(gridApi, paginationElement); return; } gridApi.setGridOption("rowData", rows); renderPagination(gridApi, paginationElement); }; const setOpen = async (nextOpen) => { isOpen = nextOpen; toggleButton.textContent = isOpen ? "목록닫기" : "목록열기"; toggleButton.setAttribute("aria-expanded", String(isOpen)); body.hidden = !isOpen; if (!isOpen) { return; } toggleButton.disabled = true; try { await renderRows(); } catch (error) { window.alert(error.message || "포트폴리오 목록을 불러오지 못했습니다."); setOpen(false); } finally { toggleButton.disabled = false; } }; toggleButton.addEventListener("click", () => { void setOpen(!isOpen); }); pageSizeSelect?.addEventListener("change", () => { if (!gridApi) { return; } gridApi.setGridOption( "paginationPageSize", Number.parseInt(pageSizeSelect.value, 10) || DEFAULT_PAGE_SIZE, ); gridApi.paginationGoToPage(0); renderPagination(gridApi, paginationElement); }); void setOpen(initialOpen); return { open: () => setOpen(true), close: () => setOpen(false), reload: async () => { rowsPromise = null; await renderRows(); }, }; } document.addEventListener("DOMContentLoaded", () => { initPortfolioListPanel(); }); export { initPortfolioListPanel };