// ==UserScript==
// @name Chzzk Cheese Stats Viewer
// @namespace agak
// @version 1.1.0
// @description 치지직 채널별 치즈 후원 통계 표시
// @author agak
// @match https://game.naver.com/profile*
// @icon https://imgur.com/EJEmHgU.png
// @grant none
// @license MIT
// ==/UserScript==
(function() {
"use strict";
console.log("[치즈 후원 통계] Loaded at", new Date());
// 이 스크립트는 https://github.com/makeAppsGreat/Log-Power-Clicker 스크립트를 허락받고 참고하여 만들었습니다.
let isStatsCreated = false;
// 구독권 티어별 가격
const SUBSCRIPTION_PRICES = {
'TIER_1': 4900,
'TIER_2': 14900
};
// 치즈양(후원금액) 통계 생성 함수
function createCheeseStats() {
// 이미 생성되었거나 치즈 페이지가 아니면 종료
if (isStatsCreated) return;
if (!window.location.href.includes('#cash')) return;
const targetSection = document.querySelector('section.profile_section__23jL-');
if (!targetSection) {
console.debug("[치즈 후원 통계] Target section not found");
return;
}
// 이미 통계 컨테이너가 있는지 확인
if (document.getElementById("cheese_stats_container")) {
console.debug("[치즈 후원 통계] Stats container already exists");
return;
}
console.debug("[치즈 후원 통계] Creating stats container");
isStatsCreated = true;
// 기존 컨테이너 찾기
const existingContainer = targetSection.querySelector('.profile_common_container__2Q8-1');
if (!existingContainer) {
console.debug("[치즈 후원 통계] Existing container not found");
isStatsCreated = false;
return;
}
// 헤더 생성
const header = document.createElement('div');
header.className = 'profile_common_header__2X1Vd';
header.style.marginTop = '40px';
const title = document.createElement('h4');
title.className = 'profile_common_title__2ttHE';
title.textContent = '채널별 치즈 후원 통계';
header.appendChild(title);
// 설명 문구 추가
const description = document.createElement('p');
description.style.setProperty("color", "#666");
description.style.setProperty("font-size", "13px");
description.style.setProperty("margin-top", "12px");
description.textContent = '사용자 스크립트 "Chzzk Cheese Stats Viewer"에 의해 만들어진 영역 입니다.';
header.appendChild(description);
// 통계 컨테이너 생성
const container = document.createElement('div');
container.className = 'profile_common_container__2Q8-1 profile_common_type_default__1CARE';
container.id = 'cheese_stats_container';
const statsDiv = document.createElement("div");
statsDiv.style.setProperty("padding", "24px");
statsDiv.style.setProperty("text-align", "center");
statsDiv.textContent = "불러오는 중...";
container.appendChild(statsDiv);
// 섹션 끝에 추가
targetSection.appendChild(header);
targetSection.appendChild(container);
// 데이터 로딩
setTimeout(() => {
const fetchYearData = (year) => {
return fetch(`https://api.chzzk.naver.com/commercial/v1/product/purchase/history?page=0&size=1000000&searchYear=${year}`, {
"headers": {
"accept": "*/*",
},
"method": "GET",
"credentials": "include"
}).then(response => response.json());
};
const fetchSubscriptionGifts = () => {
return fetch(`https://api.chzzk.naver.com/commercial/v1/gift/subscription/send-history?page=0&size=1000000`, {
"headers": {
"accept": "*/*",
},
"method": "GET",
"credentials": "include"
}).then(response => response.json());
};
// 2023년부터 현재 연도까지 자동으로 생성
const currentYear = new Date().getFullYear();
const startYear = 2023;
const yearPromises = [];
for (let year = startYear; year <= currentYear; year++) {
yearPromises.push(fetchYearData(year));
}
Promise.all([...yearPromises, fetchSubscriptionGifts()]).then((results) => {
const purchasesByChannel = {};
// 치즈 후원 데이터 처리
const subscriptionData = results.pop(); // 마지막이 구독권 데이터
const yearDataArray = results; // 나머지가 연도별 데이터
// 모든 연도의 데이터를 하나로 합치기
const allData = [];
yearDataArray.forEach(yearData => {
if (yearData.content?.data) {
allData.push(...yearData.content.data);
}
});
allData.forEach(purchase => {
const channelId = purchase.channelId;
const channelName = purchase.channelName;
const channelImageUrl = purchase.channelImageUrl;
const purchaseDate = new Date(purchase.purchaseDate);
if (!purchasesByChannel[channelId]) {
purchasesByChannel[channelId] = {
channelName: channelName,
channelId: channelId,
totalAmount: 0,
count: 0,
subscriptionTier1Count: 0,
subscriptionTier2Count: 0,
channelImageUrl: channelImageUrl,
firstDate: purchaseDate,
lastDate: purchaseDate
};
}
// 총 후원금액은 항상 추가
purchasesByChannel[channelId].totalAmount += purchase.payAmount;
// TTS가 아닌 경우만 후원 횟수 증가
if (purchase.donationType !== 'TTS') {
purchasesByChannel[channelId].count += 1;
}
// 가장 오래된 날짜와 최근 날짜 업데이트
if (purchaseDate < purchasesByChannel[channelId].firstDate) {
purchasesByChannel[channelId].firstDate = purchaseDate;
}
if (purchaseDate > purchasesByChannel[channelId].lastDate) {
purchasesByChannel[channelId].lastDate = purchaseDate;
}
});
// 구독권 선물 데이터 처리
if (subscriptionData.content?.data) {
subscriptionData.content.data.forEach(gift => {
const channelId = gift.channelId;
const channelName = gift.channelName;
const channelImageUrl = gift.channelImageUrl;
const giftPrice = SUBSCRIPTION_PRICES[gift.tier] || 0;
const quantity = gift.quantity || 1;
if (!purchasesByChannel[channelId]) {
purchasesByChannel[channelId] = {
channelName: channelName,
channelId: channelId,
totalAmount: 0,
count: 0,
subscriptionTier1Count: 0,
subscriptionTier2Count: 0,
channelImageUrl: channelImageUrl,
firstDate: null,
lastDate: null
};
}
// 구독권 금액 추가
purchasesByChannel[channelId].totalAmount += giftPrice * quantity;
// 티어별 카운트 증가
if (gift.tier === 'TIER_1') {
purchasesByChannel[channelId].subscriptionTier1Count += quantity;
} else if (gift.tier === 'TIER_2') {
purchasesByChannel[channelId].subscriptionTier2Count += quantity;
}
});
}
const sortedChannels = Object.values(purchasesByChannel)
.sort((a, b) => b.totalAmount - a.totalAmount);
// 테이블 생성
const table = document.createElement("table");
table.style.setProperty("width", "100%");
table.style.setProperty("border-collapse", "collapse");
table.cellPadding = "0";
table.cellSpacing = "0";
table.className = "table_wrapper__319N0";
// 헤더 생성
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
['순위', '채널명', '후원 횟수', '구독권 선물\n(1티어/2티어)', '총 후원금액', '첫 후원', '최근 후원'].forEach((text, index) => {
const th = document.createElement("th");
th.scope = "col";
th.style.setProperty("padding", "12px 8px");
th.style.setProperty("text-align", index === 1 ? 'left' : 'center');
th.style.setProperty("white-space", "pre-line");
th.textContent = text;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// 바디 생성
const tbody = document.createElement("tbody");
sortedChannels.forEach((channel, index) => {
const row = document.createElement("tr");
// 순위
const rankCell = document.createElement("td");
rankCell.style.setProperty("padding", "12px 8px");
rankCell.style.setProperty("text-align", "center");
rankCell.textContent = `${index + 1}위`;
// 채널명 (링크 포함)
const nameCell = document.createElement("td");
nameCell.style.setProperty("padding", "12px 8px");
const link = document.createElement("a");
link.href = `https://chzzk.naver.com/${channel.channelId}`;
link.target = "_blank";
link.className = "table_link__2Y8y7";
link.rel = "noreferrer";
link.style.setProperty("display", "flex");
link.style.setProperty("align-items", "center");
const imageDiv = document.createElement("div");
imageDiv.className = "table_image__3XY6X";
if (channel.channelImageUrl) {
imageDiv.style.setProperty("background-image", `url("${channel.channelImageUrl}")`);
}
imageDiv.style.setProperty("width", "24px");
imageDiv.style.setProperty("height", "24px");
imageDiv.style.setProperty("border-radius", "50%");
imageDiv.style.setProperty("background-size", "cover");
imageDiv.style.setProperty("background-position", "center");
imageDiv.style.setProperty("margin-right", "8px");
imageDiv.style.setProperty("flex-shrink", "0");
const nameSpan = document.createElement("span");
nameSpan.className = "table_text__1aOIu";
nameSpan.textContent = channel.channelName;
link.appendChild(imageDiv);
link.appendChild(nameSpan);
nameCell.appendChild(link);
// 후원 횟수
const countCell = document.createElement("td");
countCell.style.setProperty("padding", "12px 8px");
countCell.style.setProperty("text-align", "center");
const countStrong = document.createElement("strong");
countStrong.className = "table_text__1aOIu";
countStrong.textContent = `${channel.count.toLocaleString()}회`;
countCell.appendChild(countStrong);
// 구독권 선물 횟수 (1티어/2티어)
const subCountCell = document.createElement("td");
subCountCell.style.setProperty("padding", "12px 8px");
subCountCell.style.setProperty("text-align", "center");
const subCountStrong = document.createElement("strong");
subCountStrong.className = "table_text__1aOIu";
subCountStrong.textContent = `${channel.subscriptionTier1Count.toLocaleString()}회 / ${channel.subscriptionTier2Count.toLocaleString()}회`;
subCountCell.appendChild(subCountStrong);
// 총 후원금액
const amountCell = document.createElement("td");
amountCell.style.setProperty("padding", "12px 8px");
amountCell.style.setProperty("text-align", "center");
const amountStrong = document.createElement("strong");
amountStrong.className = "table_text__1aOIu";
amountStrong.textContent = `${channel.totalAmount.toLocaleString()}원`;
amountCell.appendChild(amountStrong);
// 첫 후원 날짜
const firstDateCell = document.createElement("td");
firstDateCell.style.setProperty("padding", "12px 8px");
firstDateCell.style.setProperty("text-align", "center");
firstDateCell.style.setProperty("font-size", "13px");
if (channel.firstDate) {
const firstDateStr = `${channel.firstDate.getFullYear()}.${String(channel.firstDate.getMonth() + 1).padStart(2, '0')}.${String(channel.firstDate.getDate()).padStart(2, '0')}`;
const firstTimeStr = `${String(channel.firstDate.getHours()).padStart(2, '0')}:${String(channel.firstDate.getMinutes()).padStart(2, '0')}:${String(channel.firstDate.getSeconds()).padStart(2, '0')}`;
firstDateCell.innerHTML = `${firstDateStr}<br>${firstTimeStr}`;
} else {
firstDateCell.textContent = '-';
}
// 최근 후원 날짜
const lastDateCell = document.createElement("td");
lastDateCell.style.setProperty("padding", "12px 8px");
lastDateCell.style.setProperty("text-align", "center");
lastDateCell.style.setProperty("font-size", "13px");
if (channel.lastDate) {
const lastDateStr = `${channel.lastDate.getFullYear()}.${String(channel.lastDate.getMonth() + 1).padStart(2, '0')}.${String(channel.lastDate.getDate()).padStart(2, '0')}`;
const lastTimeStr = `${String(channel.lastDate.getHours()).padStart(2, '0')}:${String(channel.lastDate.getMinutes()).padStart(2, '0')}:${String(channel.lastDate.getSeconds()).padStart(2, '0')}`;
lastDateCell.innerHTML = `${lastDateStr}<br>${lastTimeStr}`;
} else {
lastDateCell.textContent = '-';
}
row.appendChild(rankCell);
row.appendChild(nameCell);
row.appendChild(countCell);
row.appendChild(subCountCell);
row.appendChild(amountCell);
row.appendChild(firstDateCell);
row.appendChild(lastDateCell);
tbody.appendChild(row);
});
table.appendChild(tbody);
// 총합 행 추가
const totalRow = document.createElement("tr");
totalRow.style.setProperty("border-top", "2px solid #e5e5e5");
totalRow.style.setProperty("font-weight", "600");
const totalLabelCell = document.createElement("td");
totalLabelCell.colSpan = 2;
totalLabelCell.style.setProperty("padding", "12px 8px");
totalLabelCell.style.setProperty("text-align", "center");
totalLabelCell.textContent = "총계";
const totalCount = sortedChannels.reduce((sum, ch) => sum + ch.count, 0);
const totalCountCell = document.createElement("td");
totalCountCell.style.setProperty("padding", "12px 8px");
totalCountCell.style.setProperty("text-align", "center");
totalCountCell.textContent = `${totalCount.toLocaleString()}회`;
const totalSubTier1 = sortedChannels.reduce((sum, ch) => sum + ch.subscriptionTier1Count, 0);
const totalSubTier2 = sortedChannels.reduce((sum, ch) => sum + ch.subscriptionTier2Count, 0);
const totalSubCountCell = document.createElement("td");
totalSubCountCell.style.setProperty("padding", "12px 8px");
totalSubCountCell.style.setProperty("text-align", "center");
totalSubCountCell.textContent = `${totalSubTier1.toLocaleString()}회 / ${totalSubTier2.toLocaleString()}회`;
const totalAmount = sortedChannels.reduce((sum, ch) => sum + ch.totalAmount, 0);
const totalAmountCell = document.createElement("td");
totalAmountCell.style.setProperty("padding", "12px 8px");
totalAmountCell.style.setProperty("text-align", "center");
totalAmountCell.textContent = `${totalAmount.toLocaleString()}원`;
// 날짜 열은 빈 셀로 채우기
const emptyCell1 = document.createElement("td");
emptyCell1.style.setProperty("padding", "12px 8px");
const emptyCell2 = document.createElement("td");
emptyCell2.style.setProperty("padding", "12px 8px");
totalRow.appendChild(totalLabelCell);
totalRow.appendChild(totalCountCell);
totalRow.appendChild(totalSubCountCell);
totalRow.appendChild(totalAmountCell);
totalRow.appendChild(emptyCell1);
totalRow.appendChild(emptyCell2);
tbody.appendChild(totalRow);
// 테이블 컨테이너
const tableContainer = document.createElement("div");
tableContainer.className = "table_container__2G8Bx";
tableContainer.appendChild(table);
statsDiv.replaceChildren(tableContainer);
console.log("[치즈 후원 통계] 데이터 로드 완료:", new Date());
}).catch(error => {
console.error('[치즈 후원 통계] 데이터 로드 실패:', error);
statsDiv.textContent = "데이터 로드 중 오류가 발생했습니다.";
});
}, 100);
}
// URL 변경 감지 함수
function onUrlChange() {
// 치즈 페이지에서 벗어나면 플래그 초기화
if (!window.location.href.includes('#cash')) {
isStatsCreated = false;
document.getElementById("cheese_stats_container")?.parentElement?.remove();
document.querySelector('#cheese_stats_container')?.previousElementSibling?.remove();
} else {
// 치즈 페이지에 들어오면 생성 시도
setTimeout(createCheeseStats, 500);
}
}
// 페이지 이동 감지 설정
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function () {
originalPushState.apply(this, arguments);
onUrlChange();
};
history.replaceState = function () {
originalReplaceState.apply(this, arguments);
onUrlChange();
};
window.addEventListener('popstate', onUrlChange);
window.addEventListener('hashchange', onUrlChange);
// 초기 실행 (페이지 로드 후 약간의 지연)
setTimeout(onUrlChange, 1000);
})();