안전신문고 진행상태 상세 표시 및 요약 업데이트

나의 안전신고 목록에서 '진행' 상태를 더 명확하게 표시하고, 상단 요약 정보(표, 그래프)도 이에 맞춰 업데이트합니다.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         안전신문고 진행상태 상세 표시 및 요약 업데이트
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  나의 안전신고 목록에서 '진행' 상태를 더 명확하게 표시하고, 상단 요약 정보(표, 그래프)도 이에 맞춰 업데이트합니다.
// @author       Gamnamudan
// @match        https://www.safetyreport.go.kr/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      self
// @connect      www.safetyreport.go.kr
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        .custom-status-box {
            display: inline-block; padding: 2px 8px; font-size: 1.2rem; font-weight: normal;
            line-height: 1.5; text-align: center; white-space: nowrap; vertical-align: baseline;
            border-radius: .25em; min-width: 40px; color: #fff; cursor: help;
        }
        .custom-status-pending-reception { background-color: #f5941f; }
        .custom-status-received-by-agency { background-color: #20a95f; }
        .custom-status-error-fetching { background-color: #e54e53; }
    `);

    function fetchTransferHistory(cNo) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `/api/v1/portal/mypage/mysafereport/trnsfhist/${cNo}`,
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            const data = JSON.parse(response.responseText);
                            resolve({ cNo, data });
                        } catch (e) {
                            reject({ cNo, error: new Error(`JSON Parse Error for ${cNo}`) });
                        }
                    } else {
                        reject({ cNo, error: new Error(`API Error ${response.status} for ${cNo}`) });
                    }
                },
                onerror: function(error) {
                    reject({ cNo, error: new Error(`Network Error for ${cNo}`) });
                }
            });
        });
    }

    async function updateSingleReportStatus(statusSpan, cNo) {
        return fetchTransferHistory(cNo).then(result => {
            const { data } = result;
            statusSpan.className = '';
            statusSpan.classList.add('custom-status-box');

            const singoHist = data.singoHist || {};
            const trnsfHistList = data.singoTrnsfHistList || [];
            const sendYnC = singoHist.SEND_YN_C;
            const cNowNm = singoHist.C_NOW_NM || '진행';
            const lastTrnsfHist = trnsfHistList.length > 0 ? trnsfHistList[trnsfHistList.length - 1] : {};
            let rcptnInsttNm = (lastTrnsfHist.RCPTN_ALL_INSTT_NM || '').replace('경찰청 ', '');

            if (sendYnC === 'N') {
                statusSpan.textContent = '진행 (접수대기)';
                statusSpan.classList.add('custom-status-pending-reception');
                statusSpan.title = '시스템 처리 중, 아직 기관에 접수되지 않았습니다.';
                statusSpan.dataset.detailedStatus = 'pending';
            } else if (sendYnC === 'S') {
                if (cNowNm.trim() === '진행' && rcptnInsttNm) {
                     statusSpan.textContent = `진행 (기관확인중)`;
                } else {
                    statusSpan.textContent = `${cNowNm}`;
                }
                statusSpan.classList.add('custom-status-received-by-agency');
                statusSpan.title = `처리기관: ${rcptnInsttNm || '확인중'}\n실제상태: ${cNowNm}`;
                statusSpan.dataset.detailedStatus = 'received';
            } else {
                statusSpan.textContent = '진행 (상태확인필요)';
                statusSpan.classList.add('custom-status-error-fetching');
                statusSpan.title = `상태 확인 필요 (sendYnC: ${sendYnC})`;
                statusSpan.dataset.detailedStatus = 'unknown';
            }
            return statusSpan.dataset.detailedStatus;
        }).catch(errorResult => {
            console.error(`Error processing cNo ${errorResult.cNo}:`, errorResult.error);
            statusSpan.className = '';
            statusSpan.classList.add('custom-status-box', 'custom-status-error-fetching');
            statusSpan.textContent = '진행 (상태오류)';
            statusSpan.title = '상태 정보를 가져오는데 실패했습니다.';
            statusSpan.dataset.detailedStatus = 'error';
            return 'error';
        });
    }

    function updateSummaryAndChart(statuses) {
        const totalReportsElement = document.querySelector('p.bbs_info strong');
        if (!totalReportsElement) {
            console.warn("총 신고 건수 요소를 찾을 수 없습니다.");
            return;
        }
        const totalReports = parseInt(totalReportsElement.textContent.trim()) || 0;

        let actualProgressCount = 0;
        statuses.forEach(status => {
            if (status === 'received') {
                actualProgressCount++;
            }
        });

        const summaryTableBody = document.getElementById('tableCntBody');
        const chartContainer = document.getElementById('singoStatisticsChart');

        if (!summaryTableBody || !chartContainer) {
            console.warn("요약 테이블 또는 차트 컨테이너를 찾을 수 없습니다.");
            return;
        }

        const summaryCells = summaryTableBody.querySelectorAll('td');
        if (summaryCells.length < 4) {
            console.warn("요약 테이블 셀이 충분하지 않습니다.");
            return;
        }

        const withdrawnCount = parseInt(summaryCells[2].textContent.trim()) || 0;
        const completedCount = parseInt(summaryCells[3].textContent.trim()) || 0;

        summaryCells[0].textContent = totalReports;
        summaryCells[1].textContent = actualProgressCount;

        const chartListItems = chartContainer.querySelectorAll('ul > li');
        if (chartListItems.length < 12) {
            console.warn("차트 리스트 아이템이 충분하지 않습니다.");
            return;
        }

        const updateChartItem = (baseIndex, count, label) => {
            if (totalReports === 0 && count > 0) {
                 console.warn(`'${label}' 항목 건수는 ${count}이지만, 총 신고 건수가 0입니다. 퍼센티지를 0으로 설정합니다.`);
            }
            const percentage = totalReports > 0 ? (count / totalReports) * 100 : 0;

            const countStrong = chartListItems[baseIndex + 1].querySelector('.lst_safe strong');
            if (countStrong) countStrong.textContent = count;

            const barSpan = chartListItems[baseIndex + 2].querySelector('.g_action');
            const percentStrong = chartListItems[baseIndex + 2].querySelector('.g_percent strong');

            if (barSpan) barSpan.style.width = `${Math.round(percentage)}%`;
            if (percentStrong) percentStrong.textContent = Math.round(percentage);
        };

        updateChartItem(0, totalReports, '신고');
        updateChartItem(3, actualProgressCount, '진행');
        updateChartItem(6, withdrawnCount, '취하');
        updateChartItem(9, completedCount, '답변완료');
    }


    async function processPageUpdates() {
        const reportRows = document.querySelectorAll('#table1Body tr');
        if (!reportRows.length) return;

        const promises = [];
        for (const row of reportRows) {
            const statusSpan = row.querySelector('td.bbs_subject span[class*="ico_state_"]');
            const cNoInput = row.querySelector('input[name="cNo"]');

            if (statusSpan && cNoInput && statusSpan.textContent.trim() === '진행' && statusSpan.dataset.statusUpdated !== 'true') {
                statusSpan.dataset.statusUpdated = 'true';
                promises.push(updateSingleReportStatus(statusSpan, cNoInput.value));
            } else if (statusSpan && statusSpan.textContent.trim() !== '진행' && statusSpan.dataset.statusUpdated !== 'true') {
                statusSpan.dataset.statusUpdated = 'true';
                statusSpan.dataset.detailedStatus = 'original';
                 promises.push(Promise.resolve(statusSpan.dataset.detailedStatus));
            } else if (statusSpan && statusSpan.dataset.statusUpdated === 'true' && statusSpan.dataset.detailedStatus){
                promises.push(Promise.resolve(statusSpan.dataset.detailedStatus));
            }
        }

        if (promises.length > 0) {
            Promise.all(promises).then(statuses => {
                updateSummaryAndChart(statuses.filter(s => s));
            }).catch(error => {
                console.error("전체 상태 업데이트 중 오류 발생:", error);
            });
        } else {
             const currentDetailedStatuses = [];
             document.querySelectorAll('#table1Body td.bbs_subject span[data-detailed-status]').forEach(s => {
                 currentDetailedStatuses.push(s.dataset.detailedStatus);
             });
             if(currentDetailedStatuses.length > 0 || document.querySelector('p.bbs_info strong')){
                updateSummaryAndChart(currentDetailedStatuses);
             }
        }
    }

    const targetNode = document.getElementById('content');
    if (targetNode) {
        const observerConfig = { childList: true, subtree: true };
        const callback = function(mutationsList, observer) {
            if (document.querySelector('#table1Body tr') || document.querySelector('p.bbs_info strong')) {
                debounce(processPageUpdates, 500)();
            }
        };
        const observer = new MutationObserver(callback);
        observer.observe(targetNode, observerConfig);
        if (document.querySelector('#table1Body tr') || document.querySelector('p.bbs_info strong')) {
             processPageUpdates();
        }
    } else {
        console.warn("Target node for MutationObserver ('content') not found.");
        window.addEventListener('load', () => {
            if (document.querySelector('#table1Body tr') || document.querySelector('p.bbs_info strong')) {
                processPageUpdates();
            }
        });
    }

    let debounceTimer;
    function debounce(func, delay) {
        return function() {
            const context = this;
            const args = arguments;
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => func.apply(context, args), delay);
        };
    }
})();