init
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user