This commit is contained in:
CSKIM
2026-05-07 09:44:53 +09:00
parent ed16fdafe6
commit 348288caeb
607 changed files with 7618 additions and 248 deletions
+325
View File
@@ -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 };