<?php
$sub_menu = '650200';
include_once('./_common.php');

include_once(G5_PATH.'/mylib/ikev2_provision.lib.php');

auth_check($auth[$sub_menu], 'r');

vpnlink_ikev2_create_tables();

$g5['title'] = 'IKEv2 프로비저닝 모니터';
include_once(G5_ADMIN_PATH.'/admin.head.php');
include_once('./_vpn_help_card.php');
vpn_help_card(array(
    'title'     => 'IKEv2 연동모니터',
    'purpose'   => 'VPN 상품 주문 완료 후 IKEv2 서버에 계정(ipsec.secrets)이 정상 등록됐는지 프로비저닝 큐를 모니터링합니다.',
    'functions' => array('프로비저닝 큐 상태 조회: queued / retrying / success / failed', '실패 항목 수동 재시도', '자동재시도 중지/재개', '자동 새로고침 (10·30·60초, 또는 OFF)', '상품ID·회원ID·서버IP 등으로 필터링'),
    'results'   => array('success: 사용자가 해당 VPN 서버에 IKEv2로 접속 가능', 'queued/retrying: 백그라운드 자동 처리 중 (수 분 내)', 'failed: 수동 개입 필요 — 서버 상태 확인 후 재시도'),
    'target'    => 'IKEv2 VPN 서버 (Ubuntu 183.108.10.158) — /etc/ipsec.secrets 파일',
    'warnings'  => array('오래된 failed 항목 방치 시 해당 사용자 VPN 접속 불가 지속', '서버 접속 불가 상태에서 재시도 시 반복 실패', '대량 재시도는 서버 부하 유발 — 소수씩 처리 권장'),
));


$has_status_param = isset($_GET['status']);
$status = isset($_GET['status']) ? trim((string)$_GET['status']) : '';
$it_id = isset($_GET['it_id']) ? trim((string)$_GET['it_id']) : '';
$item_type = isset($_GET['item_type']) ? strtolower(trim((string)$_GET['item_type'])) : '';
$show_unknown = isset($_GET['show_unknown']) ? (int)$_GET['show_unknown'] : 0;
$od_id = isset($_GET['od_id']) ? trim((string)$_GET['od_id']) : '';
$mb_id = isset($_GET['mb_id']) ? trim((string)$_GET['mb_id']) : '';
$sfl = isset($_GET['sfl']) ? trim((string)$_GET['sfl']) : '';
$stx = isset($_GET['stx']) ? trim((string)$_GET['stx']) : '';
$sort1 = isset($_GET['sort1']) ? trim((string)$_GET['sort1']) : 'next_retry_at';
$sort2 = isset($_GET['sort2']) ? trim((string)$_GET['sort2']) : 'asc';
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
// 0 means disabled
$auto_refresh = isset($_GET['auto_refresh']) ? (int)$_GET['auto_refresh'] : 10;
if ($page < 1) $page = 1;

$allowed_status = array('', 'queued', 'retrying', 'success', 'manual_required');
if (!in_array($status, $allowed_status, true)) $status = '';
if (!$has_status_param) $status = '';

$allowed_auto_refresh = array(0, 10, 30, 60);
if (!in_array($auto_refresh, $allowed_auto_refresh, true)) $auto_refresh = 10;

$allowed_sfl = array('', 'protocol', 'server_ip', 'vpn_user_id', 'last_error');
if (!in_array($sfl, $allowed_sfl, true)) $sfl = '';

$allowed_item_type = array('', 'vpn', 'proxy');
if (!in_array($item_type, $allowed_item_type, true)) $item_type = '';

$allowed_sort1 = array('queue_id', 'updated_at', 'created_at', 'attempt_count', 'fail_count', 'next_retry_at', 'last_retry_at');
if (!in_array($sort1, $allowed_sort1, true)) $sort1 = 'queue_id';
$sort2 = (strtolower($sort2) === 'asc') ? 'asc' : 'desc';

function oi_ikev2_monitor_error_humanize($rawError)
{
    $raw = trim((string)$rawError);
    if ($raw === '') {
        return array('label' => '-', 'tip' => '');
    }

    $lower = strtolower($raw);

    if (preg_match('/^http_status_(\d{3})(?:\|(.*))?$/i', $raw, $m)) {
        $code = (int)$m[1];
        $metaRaw = isset($m[2]) ? trim((string)$m[2]) : '';
        $meta = array();
        if ($metaRaw !== '') {
            parse_str($metaRaw, $meta);
        }
        $fromUrl = isset($meta['from']) ? trim((string)$meta['from']) : '';
        $toUrl = isset($meta['to']) ? trim((string)$meta['to']) : '';

        $map = array(
            301 => array('HTTP 301 리다이렉트', '요청이 다른 URL로 영구 이동되었습니다. 서버 API 주소/프로토콜(http↔https)/리버스프록시 리다이렉트 설정을 확인하세요.'),
            302 => array('HTTP 302 리다이렉트', '요청이 임시 URL로 이동되었습니다. API 엔드포인트가 로그인/다른 페이지로 리다이렉트되는지 확인하세요.'),
            400 => array('HTTP 400 잘못된 요청', '요청 파라미터 형식이 올바른지 확인하세요.'),
            401 => array('HTTP 401 인증 실패', 'API 인증 정보(ID/PW, 토큰, 시크릿)를 확인하세요.'),
            403 => array('HTTP 403 접근 거부', '서버 방화벽/권한/IP 허용 정책을 확인하세요.'),
            404 => array('HTTP 404 경로 없음', 'API URL 경로가 올바른지 확인하세요.'),
            405 => array('HTTP 405 메서드 불가', '요청 메서드(GET/POST)가 API 요구사항과 일치하는지 확인하세요.'),
            408 => array('HTTP 408 요청 시간초과', '네트워크 지연 또는 서버 부하를 확인하세요.'),
            429 => array('HTTP 429 요청 과다', '호출 빈도를 낮추거나 재시도 간격을 늘리세요.'),
            500 => array('HTTP 500 서버 내부오류', '원격 API 서버 로그를 확인하세요.'),
            502 => array('HTTP 502 게이트웨이 오류', '상위/하위 서버 연결 상태를 확인하세요.'),
            503 => array('HTTP 503 서비스 불가', '원격 서비스 점검 또는 과부하 여부를 확인하세요.'),
            504 => array('HTTP 504 게이트웨이 타임아웃', '백엔드 응답 지연/네트워크 이슈를 확인하세요.')
        );

        $redirectDetail = '';
        if ($fromUrl !== '' || $toUrl !== '') {
            $redirectDetail = "\n원요청 URL: " . ($fromUrl !== '' ? $fromUrl : '(미확인)')
                            . "\n이동 URL: " . ($toUrl !== '' ? $toUrl : '(미확인)');
        }

        if (isset($map[$code])) {
            return array('label' => $map[$code][0], 'tip' => $map[$code][1] . $redirectDetail . "\n원본: " . $raw);
        }

        return array('label' => 'HTTP 오류 (' . $code . ')', 'tip' => 'HTTP 상태코드 기반 실패입니다.' . $redirectDetail . "\n원본: " . $raw);
    }

    if ($lower === 'network_error' || strpos($lower, 'timeout') !== false) {
        return array('label' => '네트워크/타임아웃 오류', 'tip' => '원격 서버 연결 지연 또는 타임아웃 가능성이 큽니다. 네트워크 상태와 대상 서버 응답속도를 확인하세요. (원본: ' . $raw . ')');
    }

    if (strpos($lower, 'ssl') !== false || strpos($lower, 'certificate') !== false) {
        return array('label' => 'SSL 인증서 오류', 'tip' => 'HTTPS 인증서 검증에 실패했습니다. 인증서 만료/도메인 불일치/신뢰체인을 확인하세요. (원본: ' . $raw . ')');
    }

    if ($lower === 'auth_failed' || strpos($lower, 'unauthorized') !== false || strpos($lower, 'forbidden') !== false) {
        return array('label' => '인증/권한 오류', 'tip' => '계정 정보 또는 권한 정책 문제일 수 있습니다. API 인증정보와 권한을 점검하세요. (원본: ' . $raw . ')');
    }

    return array('label' => $raw, 'tip' => '원본 오류: ' . $raw);
}

$where = array('1=1');
if ($status !== '') $where[] = "final_status = '" . sql_escape_string($status) . "'";
if ($it_id !== '') $where[] = "it_id = '" . sql_escape_string($it_id) . "'";
if ($od_id !== '') $where[] = "od_id = '" . sql_escape_string($od_id) . "'";
if ($mb_id !== '') $where[] = "mb_id = '" . sql_escape_string($mb_id) . "'";
if ($sfl !== '' && $stx !== '') $where[] = $sfl . " like '%" . sql_escape_string($stx) . "%'";
if ($item_type === 'vpn') {
        $where[] = " exists (
                                        select 1
                                            from {$g5['g5_shop_item_table']} i
                                         where i.it_id = q.it_id
                                             and (
                                                        i.it_spec_sales_type = 'vpn-mall'
                                                        or i.ca_id like '30%'
                                                        or i.ca_id2 like '30%'
                                                        or i.ca_id3 like '30%'
                                             )
                                ) ";
} else if ($item_type === 'proxy') {
        $where[] = " exists (
                                        select 1
                                            from {$g5['g5_shop_item_table']} i
                                         where i.it_id = q.it_id
                                             and (
                                                        i.it_spec_sales_type = 'proxy-mall'
                                                        or i.ca_id like '40%'
                                                        or i.ca_id2 like '40%'
                                                        or i.ca_id3 like '40%'
                                             )
                                ) ";
}

// 운영 가독성을 위해 UNKNOWN은 기본 숨김(필요시만 노출)
if ($show_unknown !== 1 && $item_type === '') {
        $where[] = " exists (
                                        select 1
                                            from {$g5['g5_shop_item_table']} i
                                         where i.it_id = q.it_id
                                             and (
                                                        i.it_spec_sales_type in ('vpn-mall', 'proxy-mall')
                                                        or i.ca_id like '30%'
                                                        or i.ca_id2 like '30%'
                                                        or i.ca_id3 like '30%'
                                                        or i.ca_id like '40%'
                                                        or i.ca_id2 like '40%'
                                                        or i.ca_id3 like '40%'
                                             )
                                ) ";
}

$sql_search = ' where ' . implode(' and ', $where);

$total_row = sql_fetch("select count(*) as cnt from app_ikev2_provision_queue q {$sql_search}");
$total_count = (int)$total_row['cnt'];
$rows = $config['cf_page_rows'];
$total_page = ($rows > 0) ? (int)ceil($total_count / $rows) : 1;
$from_record = ($page - 1) * $rows;

$sql = "select q.*,
               m.mb_name,
               it.it_name,
               it.it_spec_grade,
               it.it_spec_sales_type,
               case
                   when exists (
                       select 1 from {$g5['g5_shop_item_table']} i
                        where i.it_id = q.it_id
                          and (
                              i.it_spec_sales_type = 'proxy-mall'
                              or i.ca_id like '40%'
                              or i.ca_id2 like '40%'
                              or i.ca_id3 like '40%'
                          )
                   ) then 'proxy'
                   when exists (
                       select 1 from {$g5['g5_shop_item_table']} i
                        where i.it_id = q.it_id
                          and (
                              i.it_spec_sales_type = 'vpn-mall'
                              or i.ca_id like '30%'
                              or i.ca_id2 like '30%'
                              or i.ca_id3 like '30%'
                          )
                   ) then 'vpn'
                   else 'unknown'
               end as item_type
          from app_ikev2_provision_queue q
                    left join {$g5['member_table']} m
                        on m.mb_id = q.mb_id
                    left join {$g5['g5_shop_item_table']} it
                        on it.it_id = q.it_id
          {$sql_search}
         order by {$sort1} {$sort2}
         limit {$from_record}, {$rows}";
$result = sql_query($sql, false);

$sum_all = sql_fetch("select count(*) as cnt from app_ikev2_provision_queue");
$sum_queued = sql_fetch("select count(*) as cnt from app_ikev2_provision_queue where final_status = 'queued'");
$sum_retrying = sql_fetch("select count(*) as cnt from app_ikev2_provision_queue where final_status = 'retrying'");
$sum_success = sql_fetch("select count(*) as cnt from app_ikev2_provision_queue where final_status = 'success'");
$sum_manual  = sql_fetch("select count(*) as cnt from app_ikev2_provision_queue where final_status = 'manual_required'");

$qstr = 'status=' . urlencode($status)
      . '&amp;it_id=' . urlencode($it_id)
    . '&amp;item_type=' . urlencode($item_type)
            . '&amp;show_unknown=' . (int)$show_unknown
      . '&amp;od_id=' . urlencode($od_id)
      . '&amp;mb_id=' . urlencode($mb_id)
      . '&amp;sfl=' . urlencode($sfl)
      . '&amp;stx=' . urlencode($stx)
      . '&amp;sort1=' . urlencode($sort1)
      . '&amp;sort2=' . urlencode($sort2)
    . '&amp;auto_refresh=' . (int)$auto_refresh
      . '&amp;page=' . (int)$page;

$listall = '<a href="' . $_SERVER['SCRIPT_NAME'] . '" class="ov_listall">전체목록</a>';
?>

<style>
.final-error-cell { position:relative; }
.final-error-wrap { display:flex; align-items:flex-start; gap:6px; }
.final-error-label { flex:1; min-width:0; }
.final-error-help {
    border:1px solid #b7c3d0;
    background:#f5f8fb;
    color:#35506b;
    width:18px;
    height:18px;
    border-radius:50%;
    font-size:11px;
    line-height:16px;
    text-align:center;
    font-weight:700;
    cursor:pointer;
    padding:0;
}
.final-error-help:hover { background:#e9f0f7; }
.final-error-popover {
    display:none;
    position:absolute;
    right:8px;
    top:100%;
    margin-top:4px;
    z-index:30;
    width:320px;
    max-width:min(92vw, 420px);
    background:#1f2937;
    color:#fff;
    border-radius:8px;
    padding:10px 12px;
    box-shadow:0 8px 22px rgba(0, 0, 0, 0.25);
    font-size:12px;
    line-height:1.5;
    white-space:normal;
    word-break:break-word;
}
.final-error-popover.is-open { display:block; }

.hist-layer-backdrop {
    display:none;
    position:fixed;
    inset:0;
    background:rgba(15, 23, 42, 0.45);
    z-index:2000;
}
.hist-layer-backdrop.is-open { display:block; }
.hist-layer {
    display:none;
    position:fixed;
    top:50%;
    left:50%;
    transform:translate(-50%, -50%);
    width:min(1280px, 96vw);
    max-height:88vh;
    min-width:480px;
    min-height:280px;
    background:#fff;
    border-radius:10px;
    border:1px solid #d7dde5;
    box-shadow:0 20px 50px rgba(0, 0, 0, 0.28);
    z-index:2010;
    overflow:hidden;
}
.hist-layer-head {
    cursor:move;
    user-select:none;
    -webkit-user-select:none;
}
.hist-layer-head button,
.hist-layer-head select,
.hist-layer-head a,
.hist-layer-head input { cursor:default; }
.hist-resize-handle {
    position:absolute;
    bottom:0;
    right:0;
    width:20px;
    height:20px;
    cursor:se-resize;
    z-index:20;
    border-radius:0 0 10px 0;
    background-image:repeating-linear-gradient(
        -45deg,
        transparent,
        transparent 2px,
        rgba(150,150,150,0.5) 2px,
        rgba(150,150,150,0.5) 3px
    );
    opacity:0.55;
}
.hist-resize-handle:hover { opacity:1; }
[data-theme="dark"] .hist-resize-handle {
    background-image:repeating-linear-gradient(
        -45deg,
        transparent,
        transparent 2px,
        rgba(200,200,200,0.45) 2px,
        rgba(200,200,200,0.45) 3px
    );
    opacity:0.7;
}
/* ── 다크모드 스크롤바 ── */
.hist-layer-body::-webkit-scrollbar,
.hist-pre::-webkit-scrollbar { width:8px; height:8px; }
.hist-layer-body::-webkit-scrollbar-track,
.hist-pre::-webkit-scrollbar-track { background:#1a1a1a; }
.hist-layer-body::-webkit-scrollbar-thumb,
.hist-pre::-webkit-scrollbar-thumb { background:#454545; border-radius:4px; }
.hist-layer-body::-webkit-scrollbar-thumb:hover,
.hist-pre::-webkit-scrollbar-thumb:hover { background:#606060; }
.hist-layer-body::-webkit-scrollbar-corner,
.hist-pre::-webkit-scrollbar-corner { background:#1a1a1a; }
.hist-layer.is-open { display:block; }
.hist-layer-head {
    display:flex;
    justify-content:space-between;
    align-items:center;
    flex-wrap:nowrap;
    gap:10px;
    overflow-x:auto;
    overflow-y:hidden;
    max-width:100%;
    -webkit-overflow-scrolling:touch;
    padding:12px 14px;
    border-bottom:1px solid #e5e9f0;
    background:#f8fafc;
}
.hist-layer-head h3 {
    margin:0;
    font-size:14px;
    font-weight:700;
    white-space:nowrap;
    min-width:0;
    flex:1 1 auto;
    overflow:hidden;
    text-overflow:ellipsis;
}
.hist-layer-head-actions {
    display:flex;
    align-items:center;
    flex-wrap:nowrap;
    flex:0 0 auto;
    gap:6px;
}
.hist-layer-body {
    padding:12px;
    overflow:auto;
    max-height:calc(88vh - 54px);
}
.hist-mini { color:#334155; font-size:12px; margin-left:0; display:block; white-space:normal; word-break:break-all; }
.hist-updated-at { color:#475569; font-size:12px; margin-right:6px; }
.hist-layer-context {
    padding:8px 14px;
    border-bottom:1px solid #e5e9f0;
    background:#f8fafc;
}
.hist-layer-head-actions .btn,
.hist-layer-head-actions .hist-filter,
.hist-layer-head-actions .hist-updated-at,
.hist-layer-head-actions .hist-filter-summary,
.hist-layer-head .hist-mini {
    white-space:nowrap;
}
.hist-filter-summary {
    display:inline-flex;
    align-items:center;
    height:28px;
    padding:0 10px;
    border-radius:999px;
    border:1px solid #d9e2ec;
    background:#eef4ff;
    color:#1e3a8a;
    font-size:12px;
    font-weight:700;
    white-space:nowrap;
}
.hist-filter-summary.is-neutral {
    border-color:#d9e2ec;
    background:#eef4ff;
    color:#1e3a8a;
}
.hist-filter-summary.is-success {
    border-color:#b7ebc6;
    background:#e8f7ee;
    color:#166534;
}
.hist-filter-summary.is-fail {
    border-color:#ffd8a8;
    background:#fff4e6;
    color:#9a3412;
}
.hist-filter-summary.is-info {
    border-color:#c7d2fe;
    background:#eef2ff;
    color:#3730a3;
}
.hist-filter-summary.src-provision {
    box-shadow:inset 0 0 0 1px rgba(37, 99, 235, 0.18);
}
.hist-filter-summary.src-order {
    box-shadow:inset 0 0 0 1px rgba(5, 150, 105, 0.2);
}
.hist-filter-summary.src-cart {
    box-shadow:inset 0 0 0 1px rgba(217, 119, 6, 0.2);
}
.hist-filter-summary.src-point {
    box-shadow:inset 0 0 0 1px rgba(190, 24, 93, 0.22);
}
.hist-close-btn { min-width:64px; }
.hist-filter { min-width:120px; height:30px; }
.hist-table {
    width:100%;
    border-collapse:collapse;
    table-layout:auto;
    font-size:12px;
}
.hist-table th,
.hist-table td {
    border:1px solid #e5e9f0;
    padding:7px 8px;
    vertical-align:top;
    word-break:break-word;
    white-space:normal;
}
.hist-table td:last-child {
    min-width:200px;
    max-width:320px;
}
.hist-table th { background:#f8fafc; }
.hist-status-chip {
    display:inline-block;
    border-radius:999px;
    padding:1px 8px;
    font-size:11px;
    font-weight:700;
}
.hist-status-chip.success { background:#e8f7ee; color:#166534; }
.hist-status-chip.retrying { background:#fff4e6; color:#9a3412; }
.hist-status-chip.queued { background:#e6f0ff; color:#1d4ed8; }
.hist-status-chip.point_plus { background:#e8f7ee; color:#166534; }
.hist-status-chip.point_minus { background:#ffe4e6; color:#be123c; }
.hist-status-chip.info { background:#eef2ff; color:#3730a3; }
.hist-status-chip.muted { background:#f1f5f9; color:#334155; }
.hist-pre {
    max-height:140px;
    overflow:auto;
    margin:0;
    white-space:pre-wrap;
    word-break:break-word;
    font-size:11px;
    background:#f8fafc;
    border:1px solid #e5e9f0;
    padding:8px;
    border-radius:6px;
}
.hist-empty {
    padding:24px;
    text-align:center;
    color:#64748b;
}

.local_sch01.local_sch {
    display:flex;
    flex-wrap:wrap;
    align-items:center;
    gap:8px 10px;
}

#refresh_timer_text.refresh-chip {
    display:inline-flex;
    align-items:center;
    padding:4px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:700;
    line-height:1.2;
    margin-right:2px;
    border:1px solid transparent;
}

#refresh_timer_text.refresh-chip.is-running {
    color:#1e3a8a;
    background:#e8f0ff;
    border-color:#c8d8ff;
}

#refresh_timer_text.refresh-chip.is-paused {
    color:#9a3412;
    background:#fff4e6;
    border-color:#ffd8a8;
}

#refresh_timer_text.refresh-chip.is-off {
    color:#475569;
    background:#f1f5f9;
    border-color:#dbe3ec;
}

#monitor_table_wrap th.col-history,
#monitor_table_wrap td.hist-btn-cell {
    width:88px;
    min-width:88px;
    white-space:nowrap;
    text-align:center;
}

#monitor_table_wrap td.hist-btn-cell .hist-view-btn {
    display:inline-block;
    min-width:48px;
    padding:6px 10px;
    line-height:1.1;
    white-space:nowrap;
    box-sizing:border-box;
}

/* ── 상품종류 / 상품등급 배지 ── */
.it-type-badge, .it-grade-badge {
    display:inline-block; border-radius:4px;
    padding:1px 7px; font-size:11px; font-weight:700; white-space:nowrap;
}
.it-type-vpn      { background:#e3f2fd; color:#0d47a1; }
.it-type-proxy    { background:#f3e5f5; color:#6a1b9a; }
.it-type-ikev2    { background:#e3f2fd; color:#0d47a1; }
.it-type-wireguard{ background:#e8f5e9; color:#1b5e20; }
.it-type-pptp     { background:#fce4ec; color:#880e4f; }
.it-type-openvpn  { background:#fff3e0; color:#bf360c; }
.it-grade-basic{ background:#e8f5e9; color:#1b5e20; }
.it-grade-plus { background:#fff8e1; color:#e65100; }
[data-theme="dark"] .it-type-vpn,
[data-theme="dark"] .it-type-ikev2      { background:#0d2a4a !important; color:#64b5f6 !important; }
[data-theme="dark"] .it-type-proxy      { background:#2a1040 !important; color:#ce93d8 !important; }
[data-theme="dark"] .it-type-wireguard  { background:#1a3020 !important; color:#81c784 !important; }
[data-theme="dark"] .it-type-pptp       { background:#3a0a1a !important; color:#f48fb1 !important; }
[data-theme="dark"] .it-type-openvpn    { background:#3a1800 !important; color:#ffb74d !important; }
[data-theme="dark"] .it-grade-basic { background:#1a3020 !important; color:#81c784 !important; }
[data-theme="dark"] .it-grade-plus  { background:#3a2800 !important; color:#ffb74d !important; }

/* ── 상태 컬러 클래스 (라이트 모드) ── */
.prov-st-success  { color:#0a7b34; }
.prov-st-retrying { color:#d46b08; }
.prov-st-queued   { color:#0056b3; }
.prov-st-manual   { color:#c62828; }
.prov-st-unknown  { color:#666; }
.prov-sub-paused  { margin-top:3px; color:#64748b; font-size:11px; font-weight:600; }
.prov-sub-retrying{ margin-top:3px; color:#9a3412; font-size:11px; font-weight:600; }

/* ── 다크모드: 상태 컬러 ── */
[data-theme="dark"] .prov-st-success  { color:#4ade80 !important; }
[data-theme="dark"] .prov-st-retrying { color:#fb923c !important; }
[data-theme="dark"] .prov-st-queued   { color:#60a5fa !important; }
[data-theme="dark"] .prov-st-manual   { color:#f87171 !important; }
[data-theme="dark"] .prov-st-unknown  { color:#9ca3af !important; }
[data-theme="dark"] .prov-sub-paused  { color:#94a3b8 !important; }
[data-theme="dark"] .prov-sub-retrying{ color:#fb923c !important; }

/* ── 다크모드: 오버뷰 버튼 (.ov_a) — 핑크 배경 → 다크 배경 ── */
[data-theme="dark"] #monitor_summary_wrap .ov_a {
    background: #1a3a5c !important;
    color: #93c5fd !important;
    border-color: #2d5a8e !important;
}
[data-theme="dark"] #monitor_summary_wrap .ov_a:hover {
    background: #1e4a78 !important;
    color: #bfdbfe !important;
}
[data-theme="dark"] #monitor_summary_wrap a.ov_a[style] {
    color: #fca5a5 !important;
}

/* ── 다크모드: 자동새로고침 칩 ── */
[data-theme="dark"] #refresh_timer_text.refresh-chip.is-running {
    color: #93c5fd !important;
    background: #1e3a5c !important;
    border-color: #2d5a8e !important;
}
[data-theme="dark"] #refresh_timer_text.refresh-chip.is-paused {
    color: #fca5a5 !important;
    background: #3b1c1c !important;
    border-color: #7f2020 !important;
}
[data-theme="dark"] #refresh_timer_text.refresh-chip.is-off {
    color: #9ca3af !important;
    background: #1f2937 !important;
    border-color: #374151 !important;
}

/* ── 다크모드: 오류 팝오버 도움말 버튼 ── */
[data-theme="dark"] .final-error-help {
    border-color: #454545 !important;
    background: #3c3c3c !important;
    color: #cccccc !important;
}
[data-theme="dark"] .final-error-help:hover { background:#505050 !important; }

/* ── 다크모드: 히스토리 레이어(모달) ── */
[data-theme="dark"] .hist-layer {
    background: #1e1e1e !important;
    border-color: #454545 !important;
}
[data-theme="dark"] .hist-layer-head {
    background: #252526 !important;
    border-color: #454545 !important;
}
[data-theme="dark"] .hist-layer-head h3 { color:#cccccc !important; }
[data-theme="dark"] .hist-updated-at { color:#9ca3af !important; }
[data-theme="dark"] .hist-layer-context {
    background: #252526 !important;
    border-color: #454545 !important;
}
[data-theme="dark"] .hist-mini { color:#94a3b8 !important; }
[data-theme="dark"] .hist-layer-body { background:#1e1e1e !important; }
[data-theme="dark"] .hist-empty { color:#9ca3af !important; }

/* ── 다크모드: 히스토리 상태 칩 ── */
[data-theme="dark"] .hist-status-chip.success    { background:#1a3a28 !important; color:#4ade80 !important; }
[data-theme="dark"] .hist-status-chip.retrying   { background:#3b1c0a !important; color:#fb923c !important; }
[data-theme="dark"] .hist-status-chip.queued     { background:#1e2a5c !important; color:#60a5fa !important; }
[data-theme="dark"] .hist-status-chip.point_plus { background:#1a3a28 !important; color:#4ade80 !important; }
[data-theme="dark"] .hist-status-chip.point_minus{ background:#3b1c1c !important; color:#f87171 !important; }
[data-theme="dark"] .hist-status-chip.info       { background:#1e2a5c !important; color:#818cf8 !important; }
[data-theme="dark"] .hist-status-chip.muted      { background:#252526 !important; color:#9ca3af !important; }

/* ── 다크모드: 응답본문 details/summary ── */
[data-theme="dark"] .hist-table td details summary {
    color: #60a5fa;
    cursor: pointer;
    outline: none;
}
[data-theme="dark"] .hist-table td details summary:hover { color: #93c5fd; }

/* ── 다크모드: 히스토리 테이블 ── */
[data-theme="dark"] .hist-table th {
    background: #252526 !important;
    border-color: #454545 !important;
    color: #b0b0b0 !important;
}
[data-theme="dark"] .hist-table td {
    background: #1e1e1e !important;
    border-color: #454545 !important;
    color: #cccccc !important;
}
[data-theme="dark"] .hist-table tr:nth-child(even) td {
    background: #252526 !important;
}
[data-theme="dark"] .hist-pre {
    background: #2d2d2d !important;
    border-color: #454545 !important;
    color: #cccccc !important;
}

/* ── 다크모드: 필터 요약 칩 ── */
[data-theme="dark"] .hist-filter-summary {
    border-color: #454545 !important;
    background: #252526 !important;
    color: #7db8f0 !important;
}
[data-theme="dark"] .hist-filter-summary.is-success {
    background: #1a3a28 !important;
    border-color: #2a5a3a !important;
    color: #4ade80 !important;
}
[data-theme="dark"] .hist-filter-summary.is-fail {
    background: #3b1c1c !important;
    border-color: #7f2020 !important;
    color: #f87171 !important;
}
[data-theme="dark"] .hist-filter-summary.is-info {
    background: #1e2a5c !important;
    border-color: #2d3a8e !important;
    color: #818cf8 !important;
}
</style>

<div id="monitor_summary_wrap" class="local_ov01 local_ov">
    <?php echo $listall; ?>
    <span class="btn_ov01"><span class="ov_txt">전체</span><span class="ov_num"> <?php echo number_format((int)$sum_all['cnt']); ?>건</span></span>
    <span class="btn_ov01"><span class="ov_txt">대기</span><span class="ov_num"> <?php echo number_format((int)$sum_queued['cnt']); ?>건</span></span>
    <span class="btn_ov01"><span class="ov_txt">재시도중</span><span class="ov_num"> <?php echo number_format((int)$sum_retrying['cnt']); ?>건</span></span>
    <span class="btn_ov01"><span class="ov_txt">성공</span><span class="ov_num"> <?php echo number_format((int)$sum_success['cnt']); ?>건</span></span>
    <?php if ((int)$sum_manual["cnt"] > 0): ?><a href="./ikev2_provision_queue_manual.php" class="ov_a" style="color:#c62828;font-weight:700;">⚠ 수동처리 <?php echo number_format((int)$sum_manual["cnt"]); ?>건</a><?php endif; ?>
    <a href="<?php echo G5_ADMIN_URL; ?>/shop_admin/orderlist4vpn.php" class="ov_a">VPN 주문목록</a>
    <a href="./ikev2_provision_monitor.php?status=retrying&amp;auto_refresh=<?php echo (int)$auto_refresh; ?>" class="ov_a">실패/재시도만</a>
    <a href="./ikev2_provision_queue_manual.php" class="ov_a">수동처리 필요</a>
    <a href="./ikev2_provision_monitor.php?status=&amp;auto_refresh=<?php echo (int)$auto_refresh; ?>" class="ov_a">전체상태보기</a>
</div>

<div class="local_desc01 local_desc">
    <p>
        본 모니터는 <strong>SHOP 주문 중 VPN/PROXY 상품</strong>의 서버 계정(ID/PW) 등록 처리 상태를 확인하기 위한 화면입니다.<br>
        <strong>RPC(원격제어 일반/경매) 상품은 대상이 아닙니다.</strong>
    </p>
</div>

<form name="fsearch" class="local_sch01 local_sch">
<input type="hidden" name="sort1" value="<?php echo $sort1; ?>">
<input type="hidden" name="sort2" value="<?php echo $sort2; ?>">
<input type="hidden" name="page" value="1">
<input type="hidden" name="show_unknown" value="0">

<label for="auto_refresh" class="sound_only">자동새로고침</label>
<select name="auto_refresh" id="auto_refresh">
    <option value="0" <?php echo get_selected((string)$auto_refresh, '0'); ?>>새로고침 끔</option>
    <option value="10" <?php echo get_selected((string)$auto_refresh, '10'); ?>>10초</option>
    <option value="30" <?php echo get_selected((string)$auto_refresh, '30'); ?>>30초</option>
    <option value="60" <?php echo get_selected((string)$auto_refresh, '60'); ?>>60초</option>
</select>
<span id="refresh_timer_text" class="refresh-chip" aria-live="polite"></span>

<label for="status" class="sound_only">상태</label>
<select name="status" id="status">
    <option value="" <?php echo get_selected($status, ''); ?>>상태전체</option>
    <option value="queued" <?php echo get_selected($status, 'queued'); ?>>queued</option>
    <option value="retrying" <?php echo get_selected($status, 'retrying'); ?>>retrying</option>
    <option value="success" <?php echo get_selected($status, 'success'); ?>>success</option>
    <option value="manual_required" <?php echo get_selected($status, 'manual_required'); ?>>manual_required ⚠</option>
</select>

<label for="it_id" class="sound_only">상품번호</label>
<input type="text" name="it_id" id="it_id" value="<?php echo get_text($it_id); ?>" class="frm_input" placeholder="상품번호">

<label for="item_type" class="sound_only">상품구분</label>
<select name="item_type" id="item_type">
    <option value="" <?php echo get_selected($item_type, ''); ?>>상품구분전체</option>
    <option value="vpn" <?php echo get_selected($item_type, 'vpn'); ?>>VPN</option>
    <option value="proxy" <?php echo get_selected($item_type, 'proxy'); ?>>PROXY</option>
</select>

<input type="checkbox" name="show_unknown" value="1" id="show_unknown" <?php echo get_checked((string)$show_unknown, '1'); ?>>
<label for="show_unknown">UNKNOWN 포함</label>

<label for="od_id" class="sound_only">주문번호</label>
<input type="text" name="od_id" id="od_id" value="<?php echo get_text($od_id); ?>" class="frm_input" placeholder="주문번호">

<label for="mb_id" class="sound_only">회원ID</label>
<input type="text" name="mb_id" id="mb_id" value="<?php echo get_text($mb_id); ?>" class="frm_input" placeholder="회원ID">

<label for="sfl" class="sound_only">검색필드</label>
<select name="sfl" id="sfl">
    <option value="" <?php echo get_selected($sfl, ''); ?>>추가검색필드</option>
    <option value="protocol" <?php echo get_selected($sfl, 'protocol'); ?>>프로토콜</option>
    <option value="server_ip" <?php echo get_selected($sfl, 'server_ip'); ?>>서버IP</option>
    <option value="vpn_user_id" <?php echo get_selected($sfl, 'vpn_user_id'); ?>>VPN ID</option>
    <option value="last_error" <?php echo get_selected($sfl, 'last_error'); ?>>마지막오류</option>
</select>

<label for="stx" class="sound_only">검색어</label>
<input type="text" name="stx" id="stx" value="<?php echo get_text($stx); ?>" class="frm_input" placeholder="추가검색어">

<input type="submit" value="검색" class="btn_submit">
</form>

<form name="fqueue" action="./ikev2_provision_monitor_update.php" method="post" onsubmit="return fqueue_submit(this);">
<input type="hidden" name="token" value="">
<input type="hidden" name="status" value="<?php echo get_text($status); ?>">
<input type="hidden" name="it_id" value="<?php echo get_text($it_id); ?>">
<input type="hidden" name="item_type" value="<?php echo get_text($item_type); ?>">
<input type="hidden" name="show_unknown" value="<?php echo (int)$show_unknown; ?>">
<input type="hidden" name="od_id" value="<?php echo get_text($od_id); ?>">
<input type="hidden" name="mb_id" value="<?php echo get_text($mb_id); ?>">
<input type="hidden" name="sfl" value="<?php echo get_text($sfl); ?>">
<input type="hidden" name="stx" value="<?php echo get_text($stx); ?>">
<input type="hidden" name="sort1" value="<?php echo get_text($sort1); ?>">
<input type="hidden" name="sort2" value="<?php echo get_text($sort2); ?>">
<input type="hidden" name="auto_refresh" value="<?php echo (int)$auto_refresh; ?>">
<input type="hidden" name="page" value="<?php echo (int)$page; ?>">

<div id="monitor_table_wrap" class="tbl_head01 tbl_wrap">
<table>
<caption>IKEv2 프로비저닝 큐 목록</caption>
<thead>
<tr>
    <th scope="col"><input type="checkbox" name="chkall" value="1" onclick="check_all(this.form)"></th>
    <th scope="col">queue_id</th>
    <th scope="col">상태</th>
    <th scope="col">주문번호</th>
    <th scope="col">상품번호</th>
    <th scope="col">상품구분</th>
    <th scope="col">상품종류</th>
    <th scope="col">상품등급</th>
    <th scope="col">구매자(이름/ID)</th>
    <th scope="col">프로토콜</th>
    <th scope="col">서버IP</th>
    <th scope="col">VPN ID</th>
    <th scope="col">시도/실패</th>
    <th scope="col">마지막재시도</th>
    <th scope="col">다음재시도</th>
    <th scope="col">최종오류</th>
    <th scope="col" class="col-history">히스토리</th>
</tr>
</thead>
<tbody>
<?php
for ($i = 0; $row = sql_fetch_array($result); $i++) {
    $bg = 'bg' . ($i % 2);
    $status_class = 'prov-st-unknown';
    if ($row['final_status'] === 'success') $status_class = 'prov-st-success';
    if ($row['final_status'] === 'retrying') $status_class = 'prov-st-retrying';
    if ($row['final_status'] === 'queued') $status_class = 'prov-st-queued';
    if ($row['final_status'] === 'manual_required') $status_class = 'prov-st-manual';

    $attempt_count = isset($row['attempt_count']) ? (int)$row['attempt_count'] : 0;
    $fail_count = isset($row['fail_count']) ? (int)$row['fail_count'] : 0;
    $is_retry_enabled = isset($row['is_retry_enabled']) ? (int)$row['is_retry_enabled'] : 1;
    $next_retry_at_raw = trim((string)(isset($row['next_retry_at']) ? $row['next_retry_at'] : ''));
    $next_retry_at_view = $next_retry_at_raw;
    if ($row['final_status'] !== 'success' && $is_retry_enabled === 0) {
        $next_retry_at_view = '자동재시도 중지';
    } else if ($next_retry_at_view === '' || $next_retry_at_view === '0000-00-00 00:00:00') {
        $next_retry_at_view = '-';
    }
    $status_sub_text = '';
    if ($row['final_status'] === 'retrying') {
        $status_sub_text = '시도 ' . number_format($attempt_count) . '회 · 연속실패 ' . number_format($fail_count) . '회';
    }

    $hist_url = G5_URL . '/mylib/ikev2_provision_history.php?od_id=' . urlencode($row['od_id']) . '&it_id=' . urlencode($row['it_id']) . '&limit=200';
    $error_view = oi_ikev2_monitor_error_humanize(isset($row['last_error']) ? $row['last_error'] : '');
?>
<tr class="<?php echo $bg; ?>">
    <td class="td_chk">
        <input type="checkbox" name="chk[]" value="<?php echo $i; ?>" id="chk_<?php echo $i; ?>">
        <input type="hidden" name="queue_id[<?php echo $i; ?>]" value="<?php echo (int)$row['queue_id']; ?>">
    </td>
    <td class="td_num"><?php echo (int)$row['queue_id']; ?></td>
    <td>
        <strong class="<?php echo $status_class; ?>"><?php echo get_text($row['final_status']); ?></strong>
        <?php if ($row['final_status'] !== 'success' && $is_retry_enabled === 0) { ?>
            <div class="prov-sub-paused">자동재시도 중지</div>
        <?php } ?>
        <?php if ($status_sub_text !== '') { ?>
            <div class="prov-sub-retrying"><?php echo get_text($status_sub_text); ?></div>
        <?php } ?>
    </td>
    <td><?php echo get_text($row['od_id']); ?></td>
    <td><?php echo get_text($row['it_id']); ?></td>
    <td><?php echo strtoupper(get_text($row['item_type'])); ?></td>
    <td><?php
        $protocol = trim((string)($row['protocol'] ?? ''));
        $vpnType  = preg_match('/^([A-Za-z0-9]+)/i', $protocol, $pm) ? $pm[1] : '';
        echo $vpnType !== '' ? '<span class="it-type-badge it-type-' . strtolower($vpnType) . '">' . htmlspecialchars($vpnType) . '</span>' : '-';
    ?></td>
    <td><?php
        $grade = trim((string)($row['it_spec_grade'] ?? ''));
        echo $grade !== '' ? '<span class="it-grade-badge it-grade-' . strtolower($grade) . '">' . htmlspecialchars($grade) . '</span>' : '-';
    ?></td>
    <td>
        <?php
        $buyer_id = trim((string)$row['mb_id']);
        $buyer_name = trim((string)$row['mb_name']);
        if ($buyer_name !== '' && $buyer_id !== '') {
            echo get_text($buyer_name) . ' (' . get_text($buyer_id) . ')';
        } else if ($buyer_id !== '') {
            echo get_text($buyer_id);
        } else {
            echo '-';
        }
        ?>
    </td>
    <td><?php echo get_text($row['protocol']); ?></td>
    <td><?php echo get_text($row['server_ip']); ?></td>
    <td><?php echo get_text($row['vpn_user_id']); ?></td>
    <td class="td_num"><?php echo $attempt_count; ?> / <?php echo $fail_count; ?></td>
    <td><?php echo get_text($row['last_retry_at']); ?></td>
    <td><?php echo get_text($next_retry_at_view); ?></td>
    <td class="final-error-cell" style="max-width:320px; white-space:normal; word-break:break-all;">
        <div class="final-error-wrap">
            <span class="final-error-label"><?php echo get_text($error_view['label']); ?></span>
            <?php if (trim((string)$error_view['tip']) !== '') { ?>
            <button type="button" class="final-error-help" aria-expanded="false" aria-label="오류 설명 보기" data-error-help="1">?</button>
            <div class="final-error-popover" role="tooltip"><?php echo nl2br(get_text($error_view['tip'])); ?></div>
            <?php } ?>
        </div>
    </td>
    <td class="hist-btn-cell">
        <button type="button" class="btn btn_03 hist-view-btn" data-history-url="<?php echo $hist_url; ?>" data-od-id="<?php echo get_text($row['od_id']); ?>" data-it-id="<?php echo get_text($row['it_id']); ?>" data-it-name="<?php echo get_text(isset($row['it_name']) ? $row['it_name'] : ''); ?>">보기</button>
    </td>
</tr>
<?php }
if (!$i) {
    echo '<tr><td colspan="17" class="empty_table"><span>자료가 없습니다.</span></td></tr>';
}
?>
</tbody>
</table>
</div>

<div class="btn_fixed_top">
    <input type="submit" name="act_button" value="선택 즉시재시도" onclick="document.pressed=this.value" class="btn_submit btn">
    <input type="submit" name="act_button" value="선택 자동재시도 중지" onclick="document.pressed=this.value" class="btn btn_02">
    <input type="submit" name="act_button" value="선택 자동재시도 재개" onclick="document.pressed=this.value" class="btn btn_02">
    <a href="./ikev2_provision_monitor.php?status=retrying&amp;auto_refresh=<?php echo (int)$auto_refresh; ?>" class="btn btn_02">재시도중 기본보기</a>
    <a href="./ikev2_provision_monitor.php?auto_refresh=<?php echo (int)$auto_refresh; ?>" class="btn btn_02">초기화</a>
</div>
</form>

<div id="hist_layer_backdrop" class="hist-layer-backdrop"></div>
<div id="hist_layer" class="hist-layer" role="dialog" aria-modal="true" aria-labelledby="hist_layer_title">
    <div class="hist-layer-head">
        <h3 id="hist_layer_title">프로비저닝 히스토리</h3>
        <div class="hist-layer-head-actions">
            <span id="hist_layer_updated_at" class="hist-updated-at">마지막 갱신: -</span>
            <span id="hist_layer_filter_summary" class="hist-filter-summary">필터: 출처전체 · 전체 (0/0)</span>
            <select id="hist_layer_source_filter" class="hist-filter">
                <option value="all">출처전체</option>
                <option value="provision">PROV</option>
                <option value="order">ORDER</option>
                <option value="cart">CART</option>
                <option value="point">POINT</option>
            </select>
            <select id="hist_layer_filter" class="hist-filter">
                <option value="all">전체</option>
                <option value="success">성공만</option>
                <option value="fail">실패만</option>
                <option value="info">정보만</option>
            </select>
            <button type="button" id="hist_layer_csv" class="btn btn_03">CSV 다운로드</button>
            <button type="button" id="hist_layer_close" class="btn btn_02 hist-close-btn">닫기</button>
        </div>
    </div>
    <div class="hist-layer-context">
        <span id="hist_layer_meta" class="hist-mini">주문번호: - / 상품번호: - / 상품명: -</span>
    </div>
    <div id="hist_layer_body" class="hist-layer-body">
        <div class="hist-empty">히스토리를 불러오는 중입니다...</div>
    </div>
</div>

<div id="monitor_paging_wrap">
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, "{$_SERVER['SCRIPT_NAME']}?status=".urlencode($status)."&amp;it_id=".urlencode($it_id)."&amp;item_type=".urlencode($item_type)."&amp;show_unknown=".(int)$show_unknown."&amp;od_id=".urlencode($od_id)."&amp;mb_id=".urlencode($mb_id)."&amp;sfl=".urlencode($sfl)."&amp;stx=".urlencode($stx)."&amp;sort1=".urlencode($sort1)."&amp;sort2=".urlencode($sort2)."&amp;auto_refresh=".(int)$auto_refresh."&amp;page="); ?>
</div>

<script>
var autoRefreshSeconds = <?php echo (int)$auto_refresh; ?>;
var refreshRemain = autoRefreshSeconds;
var refreshTimerEl = document.getElementById('refresh_timer_text');
var autoRefreshSelectEl = document.getElementById('auto_refresh');
var monitorRefreshInFlight = false;
var countdownTimer = null;

function renderRefreshTimerText() {
    if (!refreshTimerEl) {
        return;
    }

    if (autoRefreshSeconds > 0 && shouldPauseAutoRefresh()) {
        refreshTimerEl.className = 'refresh-chip is-paused';
        refreshTimerEl.textContent = '⏸ 일시정지 · 남은 ' + refreshRemain + '초';
        return;
    }

    if (autoRefreshSeconds > 0) {
        refreshTimerEl.className = 'refresh-chip is-running';
        refreshTimerEl.textContent = '⟳ ' + refreshRemain + '초 후 갱신';
    } else {
        refreshTimerEl.className = 'refresh-chip is-off';
        refreshTimerEl.textContent = '자동 갱신 꺼짐';
    }
}

renderRefreshTimerText();

function shouldPauseAutoRefresh() {
    if (histLayer && histLayer.classList.contains('is-open')) {
        return true;
    }

    if (document.querySelector('.final-error-popover.is-open')) {
        return true;
    }

    return false;
}

function stopCountdownTimer() {
    if (countdownTimer) {
        clearInterval(countdownTimer);
        countdownTimer = null;
    }
}

function startCountdownTimer() {
    stopCountdownTimer();

    if (autoRefreshSeconds <= 0) {
        renderRefreshTimerText();
        return;
    }

    countdownTimer = setInterval(function() {
        syncAutoRefreshFromSelectIfChanged();

        if (shouldPauseAutoRefresh()) {
            renderRefreshTimerText();
            return;
        }

        refreshRemain--;
        if (refreshRemain <= 0) {
            refreshRemain = autoRefreshSeconds;
            refreshMonitorDataAjax();
            refreshOpenedHistoryLayer();
            renderRefreshTimerText();
            return;
        }
        renderRefreshTimerText();
    }, 1000);
}

function syncAutoRefreshFromSelect() {
    if (!autoRefreshSelectEl) {
        return;
    }

    var nextVal = parseInt(String(autoRefreshSelectEl.value || '0'), 10);
    if (isNaN(nextVal) || nextVal < 0) {
        nextVal = 0;
    }

    autoRefreshSeconds = nextVal;
    refreshRemain = autoRefreshSeconds;
    startCountdownTimer();
    renderRefreshTimerText();
}

function syncAutoRefreshFromSelectIfChanged() {
    if (!autoRefreshSelectEl) {
        return;
    }

    var nextVal = parseInt(String(autoRefreshSelectEl.value || '0'), 10);
    if (isNaN(nextVal) || nextVal < 0) {
        nextVal = 0;
    }

    if (nextVal !== autoRefreshSeconds) {
        autoRefreshSeconds = nextVal;
        refreshRemain = autoRefreshSeconds;
        startCountdownTimer();
        renderRefreshTimerText();
    }
}

if (autoRefreshSelectEl) {
    autoRefreshSelectEl.addEventListener('change', syncAutoRefreshFromSelect);
}

function refreshMonitorDataAjax() {
    if (monitorRefreshInFlight) {
        return;
    }

    var summaryWrap = document.getElementById('monitor_summary_wrap');
    var tableWrap = document.getElementById('monitor_table_wrap');
    var pagingWrap = document.getElementById('monitor_paging_wrap');
    if (!summaryWrap || !tableWrap || !pagingWrap) {
        return;
    }

    monitorRefreshInFlight = true;

    var url = window.location.href;
    url += (url.indexOf('?') >= 0 ? '&' : '?') + '_ajax_ts=' + Date.now();

    fetch(url, { credentials: 'same-origin' })
        .then(function(res) {
            if (!res.ok) throw new Error('HTTP ' + res.status);
            return res.text();
        })
        .then(function(html) {
            var parser = new DOMParser();
            var doc = parser.parseFromString(html, 'text/html');

            var nextSummary = doc.getElementById('monitor_summary_wrap');
            var nextTable = doc.getElementById('monitor_table_wrap');
            var nextPaging = doc.getElementById('monitor_paging_wrap');

            if (!nextSummary || !nextTable || !nextPaging) {
                throw new Error('refresh_fragment_missing');
            }

            summaryWrap.innerHTML = nextSummary.innerHTML;
            tableWrap.innerHTML = nextTable.innerHTML;
            pagingWrap.innerHTML = nextPaging.innerHTML;
        })
        .catch(function(err) {
            console.warn('monitor ajax refresh failed:', err);
        })
        .finally(function() {
            monitorRefreshInFlight = false;
        });
}

syncAutoRefreshFromSelect();

function closeAllErrorPopovers(exceptCell) {
    var cells = document.querySelectorAll('.final-error-cell');
    for (var i = 0; i < cells.length; i++) {
        if (exceptCell && cells[i] === exceptCell) {
            continue;
        }
        var btn = cells[i].querySelector('[data-error-help="1"]');
        var pop = cells[i].querySelector('.final-error-popover');
        if (btn) btn.setAttribute('aria-expanded', 'false');
        if (pop) pop.classList.remove('is-open');
    }
}

document.addEventListener('click', function(e) {
    var btn = e.target.closest('[data-error-help="1"]');
    if (btn) {
        var cell = btn.closest('.final-error-cell');
        if (!cell) return;
        var pop = cell.querySelector('.final-error-popover');
        if (!pop) return;

        var isOpen = pop.classList.contains('is-open');
        closeAllErrorPopovers(cell);
        if (isOpen) {
            pop.classList.remove('is-open');
            btn.setAttribute('aria-expanded', 'false');
        } else {
            pop.classList.add('is-open');
            btn.setAttribute('aria-expanded', 'true');
        }
        return;
    }

    if (!e.target.closest('.final-error-cell')) {
        closeAllErrorPopovers(null);
    }
});

var histLayer = document.getElementById('hist_layer');
var histLayerBackdrop = document.getElementById('hist_layer_backdrop');
var histLayerBody = document.getElementById('hist_layer_body');
var histLayerMeta = document.getElementById('hist_layer_meta');
var histLayerUpdatedAt = document.getElementById('hist_layer_updated_at');
var histLayerFilterSummary = document.getElementById('hist_layer_filter_summary');
var histLayerClose = document.getElementById('hist_layer_close');
var histLayerFilter = document.getElementById('hist_layer_filter');
var histLayerSourceFilter = document.getElementById('hist_layer_source_filter');
var histLayerCsvBtn = document.getElementById('hist_layer_csv');
var histRowsAll = [];
var histCurrentFilter = 'all';
var histCurrentSource = 'all';
var histCurrentOdId = '';
var histCurrentItId = '';
var histCurrentItName = '';
var histCurrentUrl = '';
var histRefreshInFlight = false;
var histLastUpdatedAtMs = 0;
var histUpdatedState = 'idle';

function oiEscHtml(v) {
    return String(v == null ? '' : v)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
}

function oiFormatDateTime(dt) {
    var d = dt instanceof Date ? dt : new Date(dt);
    if (!(d instanceof Date) || isNaN(d.getTime())) {
        return '-';
    }

    var yy = d.getFullYear();
    var mm = String(d.getMonth() + 1).padStart(2, '0');
    var dd = String(d.getDate()).padStart(2, '0');
    var hh = String(d.getHours()).padStart(2, '0');
    var mi = String(d.getMinutes()).padStart(2, '0');
    var ss = String(d.getSeconds()).padStart(2, '0');

    return yy + '-' + mm + '-' + dd + ' ' + hh + ':' + mi + ':' + ss;
}

function oiFormatRelativeFromMs(ms) {
    var diffSec = Math.max(0, Math.floor((Date.now() - ms) / 1000));
    if (diffSec < 60) {
        return diffSec + '초 전';
    }

    var diffMin = Math.floor(diffSec / 60);
    if (diffMin < 60) {
        return diffMin + '분 전';
    }

    var diffHour = Math.floor(diffMin / 60);
    if (diffHour < 24) {
        return diffHour + '시간 전';
    }

    var diffDay = Math.floor(diffHour / 24);
    return diffDay + '일 전';
}

function renderHistoryUpdatedAtText() {
    if (!histLayerUpdatedAt) {
        return;
    }

    if (histUpdatedState === 'loading') {
        histLayerUpdatedAt.textContent = '마지막 갱신: 갱신 중...';
        return;
    }
    if (histUpdatedState === 'fail') {
        histLayerUpdatedAt.textContent = '마지막 갱신: 실패';
        return;
    }
    if (histUpdatedState !== 'ok' || histLastUpdatedAtMs <= 0) {
        histLayerUpdatedAt.textContent = '마지막 갱신: -';
        return;
    }

    var rel = oiFormatRelativeFromMs(histLastUpdatedAtMs);
    var abs = oiFormatDateTime(new Date(histLastUpdatedAtMs));
    histLayerUpdatedAt.textContent = '마지막 갱신: ' + rel + ' (' + abs + ')';
}

function openHistoryLayer() {
    if (!histLayer || !histLayerBackdrop) return;
    // 드래그/리사이즈 상태 초기화 — 항상 화면 중앙으로 재배치
    histLayer.style.left = '';
    histLayer.style.top = '';
    histLayer.style.width = '';
    histLayer.style.height = '';
    histLayer.style.maxHeight = '';
    histLayer.style.transform = '';
    if (typeof histLayerDragInit !== 'undefined') histLayerDragInit = false;
    histLayer.classList.add('is-open');
    histLayerBackdrop.classList.add('is-open');
}

function closeHistoryLayer() {
    if (!histLayer || !histLayerBackdrop) return;
    histLayer.classList.remove('is-open');
    histLayerBackdrop.classList.remove('is-open');
}

function getFilteredHistoryRows(rows, filterType, sourceType) {
    var list = rows || [];
    var type = String(filterType || 'all');
    var source = String(sourceType || 'all');

    function classifyRowOutcome(r) {
        var row = r || {};
        var src = String(row.source || '').toLowerCase();
        var st = String(row.event_status || '').toLowerCase();

        // PROV: 성공/실패만 사용 (정보 분류 없음)
        if (src === 'provision') {
            return (st === 'success') ? 'success' : 'fail';
        }

        // ORDER/CART/POINT는 운영 변경 이력(중립 정보)
        if (src === 'order' || src === 'cart' || src === 'point') {
            return 'info';
        }

        return 'info';
    }

    if (source !== 'all') {
        list = list.filter(function(r) {
            return String((r || {}).source || '').toLowerCase() === source;
        });
    }

    if (type === 'success') {
        return list.filter(function(r) {
            return classifyRowOutcome(r) === 'success';
        });
    }
    if (type === 'fail') {
        return list.filter(function(r) {
            return classifyRowOutcome(r) === 'fail';
        });
    }
    if (type === 'info') {
        return list.filter(function(r) {
            return classifyRowOutcome(r) === 'info';
        });
    }
    return list;
}

function getStatusOptionsBySource(source) {
    var src = String(source || 'all').toLowerCase();

    // 전체 출처: 모든 필터
    if (src === 'all') {
        return [
            { value: 'all', label: '전체' },
            { value: 'success', label: '성공만' },
            { value: 'fail', label: '실패만' },
            { value: 'info', label: '정보만' }
        ];
    }

    // PROV: 전체/성공/실패
    if (src === 'provision') {
        return [
            { value: 'all', label: '전체' },
            { value: 'success', label: '성공만' },
            { value: 'fail', label: '실패만' }
        ];
    }

    // ORDER/CART/POINT: 전체/정보
    return [
        { value: 'all', label: '전체' },
        { value: 'info', label: '정보만' }
    ];
}

function syncStatusFilterOptionsBySource() {
    if (!histLayerFilter) {
        return;
    }

    var options = getStatusOptionsBySource(histCurrentSource);
    var prev = String(histCurrentFilter || 'all');
    var html = '';
    var allowed = {};

    for (var i = 0; i < options.length; i++) {
        var opt = options[i];
        var v = String(opt.value || 'all');
        allowed[v] = true;
        html += '<option value="' + oiEscHtml(v) + '">' + oiEscHtml(String(opt.label || v)) + '</option>';
    }

    histLayerFilter.innerHTML = html;

    if (!allowed[prev]) {
        // 허용되지 않는 기존 값은 기본 'all'로 보정
        prev = 'all';
    }

    histCurrentFilter = prev;
    histLayerFilter.value = prev;
}

function renderHistoryFilterSummary(filteredCount, totalCount) {
    if (!histLayerFilterSummary) {
        return;
    }

    var sourceMap = {
        all: '출처전체',
        provision: 'PROV',
        order: 'ORDER',
        cart: 'CART',
        point: 'POINT'
    };
    var statusMap = {
        all: '전체',
        success: '성공만',
        fail: '실패만',
        info: '정보만'
    };
    var sourceIconMap = {
        all: '🧭',
        provision: '🔐',
        order: '🧾',
        cart: '📦',
        point: '💰'
    };
    var statusIconMap = {
        all: '●',
        success: '✅',
        fail: '⚠️',
        info: 'ℹ️'
    };

    var sourceLabel = sourceMap[String(histCurrentSource || 'all')] || String(histCurrentSource || 'all');
    var statusLabel = statusMap[String(histCurrentFilter || 'all')] || String(histCurrentFilter || 'all');
    var sourceIcon = sourceIconMap[String(histCurrentSource || 'all')] || '🧭';
    var statusIcon = statusIconMap[String(histCurrentFilter || 'all')] || '●';
    var shown = (typeof filteredCount === 'number' && filteredCount >= 0) ? filteredCount : 0;
    var total = (typeof totalCount === 'number' && totalCount >= 0) ? totalCount : 0;

    var cls = ['hist-filter-summary'];
    if (String(histCurrentFilter || 'all') === 'fail') {
        cls.push('is-fail');
    } else if (String(histCurrentFilter || 'all') === 'success') {
        cls.push('is-success');
    } else if (String(histCurrentFilter || 'all') === 'info') {
        cls.push('is-info');
    } else {
        cls.push('is-neutral');
    }

    var src = String(histCurrentSource || 'all');
    if (src === 'provision' || src === 'order' || src === 'cart' || src === 'point') {
        cls.push('src-' + src);
    }

    histLayerFilterSummary.className = cls.join(' ');

    histLayerFilterSummary.textContent = '필터: ' + sourceIcon + ' ' + sourceLabel + ' · ' + statusIcon + ' ' + statusLabel + ' (' + shown + '/' + total + ')';
}

function renderHistoryRows(rows) {
    if (!rows || !rows.length) {
        return '<div class="hist-empty">히스토리 데이터가 없습니다.</div>';
    }

    var html = '';
    html += '<table class="hist-table">';
    html += '<thead><tr>';
    html += '<th style="width:70px;">ID</th>';
    html += '<th style="width:130px;">일시</th>';
    html += '<th style="width:72px;">출처</th>';
    html += '<th style="width:90px;">이벤트</th>';
    html += '<th style="width:95px;">상태</th>';
    html += '<th style="width:70px;">HTTP</th>';
    html += '<th style="width:120px;">프로토콜</th>';
    html += '<th style="width:120px;">서버IP</th>';
    html += '<th style="width:110px;">VPN ID</th>';
    html += '<th style="width:130px;">다음재시도</th>';
    html += '<th style="width:180px;">오류메시지</th>';
    html += '<th style="min-width:200px;">응답본문</th>';
    html += '</tr></thead><tbody>';

    for (var i = 0; i < rows.length; i++) {
        var r = rows[i] || {};
        var evStatus = String(r.event_status || '');
        var statusCls = (evStatus === 'success' || evStatus === 'retrying' || evStatus === 'queued' || evStatus === 'point_plus' || evStatus === 'point_minus') ? evStatus : 'info';
        var statusHtml = evStatus !== ''
            ? '<span class="hist-status-chip ' + oiEscHtml(statusCls) + '">' + oiEscHtml(evStatus) + '</span>'
            : '-';

        var sourceRaw = String(r.source || '');
        var sourceMap = {
            provision: 'PROV',
            order: 'ORDER',
            cart: 'CART',
            point: 'POINT'
        };
        var sourceLabel = sourceMap[sourceRaw] || (sourceRaw !== '' ? sourceRaw.toUpperCase() : '-');

        var body = String(r.response_body || '');
        var bodyHtml = '-';
        if (body !== '') {
            bodyHtml = '<details><summary>본문 보기</summary><pre class="hist-pre">' + oiEscHtml(body) + '</pre></details>';
        }

        html += '<tr>';
        html += '<td>' + oiEscHtml(r.history_id) + '</td>';
        html += '<td>' + oiEscHtml(r.created_at || '-') + '</td>';
        html += '<td>' + oiEscHtml(sourceLabel) + '</td>';
        html += '<td>' + oiEscHtml(r.event_type || '-') + '</td>';
        html += '<td>' + statusHtml + '</td>';
        html += '<td>' + oiEscHtml(r.http_code || '-') + '</td>';
        html += '<td>' + oiEscHtml(r.protocol || '-') + '</td>';
        html += '<td>' + oiEscHtml(r.server_ip || '-') + '</td>';
        html += '<td>' + oiEscHtml(r.vpn_user_id || '-') + '</td>';
        html += '<td>' + oiEscHtml(r.next_retry_at || '-') + '</td>';
        html += '<td>' + oiEscHtml(r.error_message || '-') + '</td>';
        html += '<td>' + bodyHtml + '</td>';
        html += '</tr>';
    }

    html += '</tbody></table>';
    return html;
}

function applyHistoryFilterView() {
    if (!histLayerBody) return;
    var rows = getFilteredHistoryRows(histRowsAll, histCurrentFilter, histCurrentSource);
    histLayerBody.innerHTML = renderHistoryRows(rows);
    renderHistoryFilterSummary(rows.length, (histRowsAll || []).length);
}

function csvCell(v) {
    var s = String(v == null ? '' : v);
    return '"' + s.replace(/"/g, '""') + '"';
}

function downloadHistoryCsv() {
    var rows = getFilteredHistoryRows(histRowsAll, histCurrentFilter, histCurrentSource);
    if (!rows || !rows.length) {
        alert('다운로드할 히스토리 데이터가 없습니다.');
        return;
    }

    var headers = ['history_id', 'source', 'created_at', 'event_type', 'event_status', 'http_code', 'protocol', 'server_ip', 'vpn_user_id', 'next_retry_at', 'error_message', 'response_body'];
    var lines = [];
    lines.push(headers.map(csvCell).join(','));

    for (var i = 0; i < rows.length; i++) {
        var r = rows[i] || {};
        lines.push([
            r.history_id,
            r.source,
            r.created_at,
            r.event_type,
            r.event_status,
            r.http_code,
            r.protocol,
            r.server_ip,
            r.vpn_user_id,
            r.next_retry_at,
            r.error_message,
            r.response_body
        ].map(csvCell).join(','));
    }

    var bom = '\uFEFF';
    var csv = bom + lines.join('\n');
    var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    var url = URL.createObjectURL(blob);
    var a = document.createElement('a');
    var suffix = histCurrentFilter === 'all' ? 'all' : (histCurrentFilter === 'success' ? 'success' : (histCurrentFilter === 'fail' ? 'fail' : 'info'));
    var sourceSuffix = histCurrentSource === 'all' ? 'src-all' : ('src-' + histCurrentSource);
    a.href = url;
    a.download = 'ikev2_history_' + (histCurrentOdId || 'od') + '_' + (histCurrentItId || 'it') + '_' + suffix + '_' + sourceSuffix + '.csv';
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
}

function requestHistoryData(url, showLoading) {
    var reqUrl = String(url || '');
    if (reqUrl === '' || histRefreshInFlight) {
        return;
    }

    histRefreshInFlight = true;

    if (showLoading && histLayerBody) {
        histLayerBody.innerHTML = '<div class="hist-empty">히스토리를 불러오는 중입니다...</div>';
    }
    if (showLoading) {
        histUpdatedState = 'loading';
        renderHistoryUpdatedAtText();
    }

    fetch(reqUrl, { credentials: 'same-origin' })
        .then(function(res) {
            if (!res.ok) throw new Error('HTTP ' + res.status);
            return res.json();
        })
        .then(function(data) {
            if (!data || data.ok !== true) {
                var msg = data && data.error ? String(data.error) : 'unknown_error';
                throw new Error(msg);
            }
            histRowsAll = data.history || [];
            applyHistoryFilterView();
            histLastUpdatedAtMs = Date.now();
            histUpdatedState = 'ok';
            renderHistoryUpdatedAtText();
        })
        .catch(function(err) {
            if (showLoading && histLayerBody) {
                histLayerBody.innerHTML = '<div class="hist-empty">히스토리 조회 실패: ' + oiEscHtml(err && err.message ? err.message : '오류') + '</div>';
            } else {
                console.warn('history auto refresh failed:', err);
            }
            if (showLoading) {
                histUpdatedState = 'fail';
                renderHistoryUpdatedAtText();
            }
        })
        .finally(function() {
            histRefreshInFlight = false;
        });
}

function refreshOpenedHistoryLayer() {
    if (!histLayer || !histLayer.classList.contains('is-open')) {
        return;
    }
    if (histCurrentUrl === '') {
        return;
    }
    requestHistoryData(histCurrentUrl, false);
}

function loadHistoryToLayer(url, odId, itId, itName) {
    if (!histLayerBody) return;
    histRowsAll = [];
    histCurrentOdId = String(odId || '');
    histCurrentItId = String(itId || '');
    histCurrentItName = String(itName || '');
    histCurrentUrl = String(url || '');
    histCurrentFilter = 'all';
    histCurrentSource = 'all';
    syncStatusFilterOptionsBySource();
    if (histLayerSourceFilter) {
        histLayerSourceFilter.value = 'all';
    }
    if (histLayerMeta) {
        histLayerMeta.textContent = '주문번호: ' + String(odId || '-') + ' / 상품번호: ' + String(itId || '-') + ' / 상품명: ' + (histCurrentItName !== '' ? histCurrentItName : '-');
    }
    histLastUpdatedAtMs = 0;
    histUpdatedState = 'idle';
    renderHistoryUpdatedAtText();
    renderHistoryFilterSummary(0, 0);
    openHistoryLayer();
    requestHistoryData(histCurrentUrl, true);
}

document.addEventListener('click', function(e) {
    var histBtn = e.target.closest('[data-history-url]');
    if (histBtn) {
        e.preventDefault();
        loadHistoryToLayer(
            String(histBtn.getAttribute('data-history-url') || ''),
            String(histBtn.getAttribute('data-od-id') || ''),
            String(histBtn.getAttribute('data-it-id') || ''),
            String(histBtn.getAttribute('data-it-name') || '')
        );
        return;
    }
});

if (histLayerBackdrop) {
    histLayerBackdrop.addEventListener('click', closeHistoryLayer);
}
if (histLayerClose) {
    histLayerClose.addEventListener('click', closeHistoryLayer);
}
if (histLayerFilter) {
    histLayerFilter.addEventListener('change', function() {
        histCurrentFilter = String(histLayerFilter.value || 'all');
        applyHistoryFilterView();
    });
}
if (histLayerSourceFilter) {
    histLayerSourceFilter.addEventListener('change', function() {
        histCurrentSource = String(histLayerSourceFilter.value || 'all');
        syncStatusFilterOptionsBySource();
        applyHistoryFilterView();
    });
}
if (histLayerCsvBtn) {
    histLayerCsvBtn.addEventListener('click', downloadHistoryCsv);
}
document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape') {
        closeHistoryLayer();
    }
});

setInterval(function() {
    renderHistoryUpdatedAtText();
}, 1000);

// ── 히스토리 레이어 드래그 & 리사이즈 ────────────────────────────────
(function() {
    var layer = document.getElementById('hist_layer');
    var head = layer ? layer.querySelector('.hist-layer-head') : null;
    if (!layer || !head) return;

    var dragging = false;
    var resizing = false;
    var startMouseX, startMouseY, startLeft, startTop, startW, startH;
    window.histLayerDragInit = false;

    // transform:translate(-50%,-50%) → 픽셀 좌표로 전환 (드래그/리사이즈 첫 시작 시 1회)
    function initAbsolutePos() {
        if (window.histLayerDragInit) return;
        var r = layer.getBoundingClientRect();
        layer.style.left   = r.left + 'px';
        layer.style.top    = r.top  + 'px';
        layer.style.width  = r.width  + 'px';
        layer.style.height = r.height + 'px';
        layer.style.maxHeight = 'none';
        layer.style.transform = 'none';
        window.histLayerDragInit = true;
        syncBodyHeight();
    }

    function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }

    function syncBodyHeight() {
        var bodyEl = document.getElementById('hist_layer_body');
        if (!bodyEl) return;
        var headEl  = layer.querySelector('.hist-layer-head');
        var ctxEl   = layer.querySelector('.hist-layer-context');
        var headH   = headEl  ? headEl.offsetHeight  : 0;
        var ctxH    = ctxEl   ? ctxEl.offsetHeight   : 0;
        bodyEl.style.maxHeight = (layer.offsetHeight - headH - ctxH - 2) + 'px';
    }

    // ── 드래그 ──────────────────────────────────────────────────────
    head.addEventListener('mousedown', function(e) {
        if (e.target.closest('button, select, a, input')) return;
        if (e.button !== 0) return;
        initAbsolutePos();
        dragging  = true;
        startMouseX = e.clientX;
        startMouseY = e.clientY;
        startLeft   = parseFloat(layer.style.left) || 0;
        startTop    = parseFloat(layer.style.top)  || 0;
        e.preventDefault();
    });

    // 터치 드래그
    head.addEventListener('touchstart', function(e) {
        if (e.target.closest('button, select, a, input')) return;
        var t = e.touches[0];
        initAbsolutePos();
        dragging    = true;
        startMouseX = t.clientX;
        startMouseY = t.clientY;
        startLeft   = parseFloat(layer.style.left) || 0;
        startTop    = parseFloat(layer.style.top)  || 0;
    }, { passive: true });

    // ── 리사이즈 핸들 생성 ──────────────────────────────────────────
    var rh = document.createElement('div');
    rh.className = 'hist-resize-handle';
    rh.title = '크기 조절';
    layer.appendChild(rh);

    rh.addEventListener('mousedown', function(e) {
        if (e.button !== 0) return;
        initAbsolutePos();
        resizing    = true;
        startMouseX = e.clientX;
        startMouseY = e.clientY;
        startW      = layer.offsetWidth;
        startH      = layer.offsetHeight;
        e.preventDefault();
        e.stopPropagation();
    });

    // ── mousemove ───────────────────────────────────────────────────
    document.addEventListener('mousemove', function(e) {
        if (dragging) {
            var nx = clamp(startLeft + e.clientX - startMouseX, 0, window.innerWidth  - 120);
            var ny = clamp(startTop  + e.clientY - startMouseY, 0, window.innerHeight -  60);
            layer.style.left = nx + 'px';
            layer.style.top  = ny + 'px';
        }
        if (resizing) {
            var nw = clamp(startW + e.clientX - startMouseX, 480, window.innerWidth  - (parseFloat(layer.style.left) || 0));
            var nh = clamp(startH + e.clientY - startMouseY, 280, window.innerHeight - (parseFloat(layer.style.top)  || 0));
            layer.style.width  = nw + 'px';
            layer.style.height = nh + 'px';
            syncBodyHeight();
        }
    });

    document.addEventListener('touchmove', function(e) {
        if (!dragging) return;
        var t = e.touches[0];
        var nx = clamp(startLeft + t.clientX - startMouseX, 0, window.innerWidth  - 120);
        var ny = clamp(startTop  + t.clientY - startMouseY, 0, window.innerHeight -  60);
        layer.style.left = nx + 'px';
        layer.style.top  = ny + 'px';
    }, { passive: true });

    // ── mouseup / touchend ──────────────────────────────────────────
    document.addEventListener('mouseup',   function() { dragging = false; resizing = false; });
    document.addEventListener('touchend',  function() { dragging = false; });
})();

function fqueue_submit(f)
{
    var act = String(document.pressed || '');

    if (!is_checked('chk[]')) {
        alert((act || '처리') + ' 하실 항목을 하나 이상 선택하세요.');
        return false;
    }

    var msg = '선택한 항목을 처리하시겠습니까?';
    if (act === '선택 즉시재시도') {
        msg = '선택한 항목을 즉시 재시도 하시겠습니까?';
    } else if (act === '선택 자동재시도 중지') {
        msg = '선택한 항목의 상품번호에 대해 자동재시도를 중지하시겠습니까?';
    } else if (act === '선택 자동재시도 재개') {
        msg = '선택한 항목의 상품번호에 대해 자동재시도를 재개하시겠습니까?';
    }

    if (!confirm(msg)) {
        return false;
    }

    return true;
}
</script>

<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
