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

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
        };
    }
})();