GNU/skin/board/outline/list.skin.php
<?php
if (!defined('_GNUBOARD_')) exit;

add_stylesheet('<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">', 0);
add_stylesheet('<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Noto+Sans+KR:wght@300;400;500;700&family=JetBrains+Mono:wght@400;700&display=swap">', 0);

error_reporting(E_ALL & ~E_NOTICE);
ini_set('display_errors', '0');

$new_board = sql_query("select * from $write_table order by wr_datetime desc limit 1");
$lat_board = sql_fetch_array($new_board);

$upbit_names = [];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.upbit.com/v1/market/all");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$resp = curl_exec($ch);
curl_close($ch);
if($resp) {
    $arr = json_decode($resp, true);
    foreach($arr as $c) { $upbit_names[$c['market']] = $c['korean_name']; }
}
?>

<style>
    @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
    :root {
        --neon-cyan: #00d4ff; --neon-pink: #ff2d55; --bg-black: #0d1117;
        --panel-bg: #161b22; --input-bg: #0d1117; --border-color: #30363d;
        --text-dim: #8b949e; --side-margin: 50px;
        --price-up: #4ade80; --price-down: #ff2d55;
        --realtime-blue: #5de2ff;
        --golden-yellow: #ffdf00;
        color-scheme: dark;
    }

    #LIST_WRAP { position: relative; min-height: 100vh; background: var(--bg-black); padding: 40px 0; font-family: 'Pretendard', sans-serif; color: #c9d1d9; overflow-x: hidden; }
    #starCanvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; opacity: 0.2; }
    #LIST_CONTENT { position: relative; z-index: 2; width: calc(100% - 100px); margin: 0 var(--side-margin); max-width: none; }

    .list-frame { background: var(--panel-bg); border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.5); overflow: hidden; position: relative; }
    .list-frame::before, .list-frame::after { content: ''; position: absolute; width: 30px; height: 30px; border: 2px solid var(--neon-cyan); z-index: 10; pointer-events: none; }
    .list-frame::before { top: -1px; left: -1px; border-radius: 8px 0 0 0; border-right:0; border-bottom:0; }
    .list-frame::after { bottom: -1px; right: -1px; border-radius: 0 0 8px 0; border-left:0; border-top:0; }

    .Header-Area { display: flex; justify-content: space-between; align-items: center; padding: 25px 35px; border-bottom: 1px solid var(--border-color); background: linear-gradient(90deg, rgba(0, 212, 255, 0.05), transparent); }
    .TopTitle p { font-family: 'Orbitron', sans-serif; font-size: 1.4rem; font-weight: 900; margin: 0; color: #fff; letter-spacing: 2px; }
    .TopTitle span { display: block; margin-top: 5px; font-size: 0.75rem; color: var(--neon-cyan); letter-spacing: 1px; font-weight: 600; text-transform: uppercase; }

    .btn-cyber { background: var(--neon-cyan); color: #000 !important; font-family: 'Orbitron', sans-serif; font-weight: 800; border: none; padding: 10px 25px; cursor: pointer; border-radius: 4px; transition: all 0.3s; font-size: 0.8rem; letter-spacing: 1px; }

    .Control-Bar { display: flex; justify-content: space-between; align-items: center; padding: 15px 35px; background: rgba(0, 0, 0, 0.15); border-bottom: 1px solid var(--border-color); }
    .Filter-Group { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }

    button.sub-btn, select { background: var(--input-bg) !important; border: 1px solid var(--border-color) !important; color: var(--text-dim) !important; padding: 7px 12px !important; border-radius: 4px !important; font-size: 0.75rem; cursor: pointer; transition: 0.2s; font-family: 'JetBrains Mono'; outline: none; }

    .List-Table { width: 100%; border-collapse: collapse; table-layout: fixed; }
    .List-Table tr { border-bottom: 1px solid var(--border-color); transition: 0.2s; cursor: pointer; }
    .List-Table tr:hover { background: rgba(255, 255, 255, 0.05); }
    .List-Table th, .List-Table td { padding: 12px 10px; font-size: 0.8rem; vertical-align: middle; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

    .txt-right { text-align: right !important; }
    .txt-center { text-align: center !important; }
    
    .sortable { cursor: pointer; transition: color 0.2s; }
    .sortable:hover { color: #ff0000 !important; text-shadow: 0 0 8px rgba(255, 0, 0, 0.8); }

    .price-int { color: var(--price-up); font-weight: 700; font-family: 'JetBrains Mono'; }
    .price-decimal { color: #555; font-size: 0.85em; font-family: 'JetBrains Mono'; }
    .rt-blue-main { color: var(--realtime-blue); font-weight: 700; font-family: 'JetBrains Mono'; }
    .rt-blue-sub { color: #48a9c0; font-size: 0.85em; }

    .st_badge { display: inline-block; padding: 4px 12px; border-radius: 3px; font-size: 0.65rem; font-weight: 700; text-transform: uppercase; border: 1px solid var(--border-color); font-family: 'Orbitron'; }
    .st_blue { color: var(--neon-cyan); border-color: rgba(0, 212, 255, 0.3); background: rgba(0, 212, 255, 0.05); }
    .st_pink { color: var(--neon-pink); border-color: rgba(255, 45, 85, 0.3); background: rgba(255, 45, 85, 0.05); }
    .status-btn { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 0.65rem; font-weight: 800; color: #000; }
    .bg-on { background: var(--price-up); }
    .bg-off { background: var(--text-dim); color: #fff; opacity: 0.5; }

    .mini-chart-bg { width: 100%; height: 6px; background: rgba(255,255,255,0.05); border-radius: 3px; overflow: hidden; position: relative; }
    .mini-chart-bar { height: 100%; background: var(--neon-cyan); transition: width 0.3s ease; }
    .target-dot { position: absolute; top: 0; right: 0; width: 2px; height: 100%; background: var(--golden-yellow); box-shadow: 0 0 5px var(--golden-yellow); }

    .cyber-modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 10002; align-items: center; justify-content: center; }
    .cyber-modal { display: block; position: relative; width: 90%; max-width: 450px; background: var(--panel-bg); padding: 30px; border: 1px solid var(--neon-cyan); z-index: 10001; box-shadow: 0 20px 50px rgba(0,0,0,0.8); border-radius: 8px; }
    .modal-header { font-family: 'Orbitron'; color: var(--neon-cyan); font-size: 1rem; margin-bottom: 20px; border-bottom: 1px solid var(--border-color); padding-bottom: 10px; }
</style>

<div id="LIST_WRAP">
    <canvas id="starCanvas"></canvas>
    <article id='LIST_CONTENT'>
        <div class="list-frame">
            <section class="Header-Area">
                <div class="TopTitle"><p><?php echo strtoupper($bo_table); ?></p><span><i class="fa-solid fa-microchip"></i> <?php echo $board["bo_subject"]; ?></span></div>
                <div class="Top-Action"><?php if ($write_href) { ?><button type="button" onclick="location.href='<?php echo $write_href; ?>'" class="btn-cyber">새 항목 등록</button><?php } ?></div>
            </section>

            <div class="Control-Bar">
                <div class="Filter-Group">
                    <button type="button" class="sub-btn" onclick="$('#NoticeOverlay').css('display','flex').fadeIn(200);"><i class="fa-solid fa-bullhorn"></i> 공지사항</button>
                    <?php if ($is_category) { ?>
                        <select onchange="location.href=this.value;">
                            <option value="<?php echo get_pretty_url($bo_table); ?>">카테고리</option>
                            <?php $categories = explode('|', $board['bo_category_list']); foreach($categories as $ca) { if(!$ca) continue; echo "<option value='".get_pretty_url($bo_table, '', 'sca='.urlencode($ca))."' ".($sca==$ca?'selected':'').">$ca</option>"; } ?>
                        </select>
                    <?php } ?>
                    <select onchange="location.href=this.value;">
                        <option value="<?php echo get_pretty_url($bo_table); ?>">타입</option>
                        <?php $opts1 = explode('|', $board['bo_1']); foreach($opts1 as $opt) { if(!$opt) continue; echo "<option value='".get_pretty_url($bo_table, '', 'sfl=x2_ca2&stx='.urlencode($opt))."' ".($stx==$opt?'selected':'').">$opt</option>"; } ?>
                    </select>
                    <select onchange="location.href=this.value;">
                        <option value="">포지션</option>
                        <option value="<?php echo get_pretty_url($bo_table, '', 'sfl=x2_ca4&stx=롱'); ?>" <?php echo ($stx=='롱'?'selected':''); ?>>롱</option>
                        <option value="<?php echo get_pretty_url($bo_table, '', 'sfl=x2_ca4&stx=숏'); ?>" <?php echo ($stx=='숏'?'selected':''); ?>>숏</option>
                    </select>
                    <select onchange="location.href=this.value;">
                        <option value="">상태</option>
                        <option value="<?php echo get_pretty_url($bo_table, '', 'sfl=x2_ca3&stx=1'); ?>" <?php echo ($stx=='1'?'selected':''); ?>>가동</option>
                        <option value="<?php echo get_pretty_url($bo_table, '', 'sfl=x2_ca3&stx=0'); ?>" <?php echo ($stx=='0'?'selected':''); ?>>중지</option>
                    </select>
                </div>
                <div class="Filter-Group">
                    <button type="button" class="sub-btn" onclick="$('#SearchOverlay').css('display','flex').fadeIn(200);"><i class="fa-solid fa-search"></i> 검색</button>
                    <?php if ($is_admin) { ?>
                    <button type="button" class="sub-btn" onclick="location.href='<?php echo G5_ADMIN_URL; ?>';"><i class="fa-solid fa-gear"></i> 관리자</button>
                    <?php } ?>
                </div>
            </div>

            <table class="List-Table">
                <thead>
                    <tr style="background: rgba(255,255,255,0.02); color: var(--neon-cyan); font-family: 'Orbitron'; font-size: 0.7rem;">
                        <th style="width:70px;" class="txt-center">번호</th>
                        <th style="width:160px;" class="sortable" onclick="doSort('x2_coin')">코인</th>
                        <th class="txt-right sortable" onclick="doSort('wr_subject')">감시가</th>
                        <th style="width:250px;" class="txt-right sortable" onclick="doSort('trade_price')">현재가</th>
                        <th style="width:180px;" class="txt-right sortable" onclick="doSort('diff')">변동액</th>
                        <th style="width:120px;" class="txt-right sortable" onclick="doSort('percent')">수익률</th>
                        
                        <th style="width:100px;" class="txt-center">도달여부</th>
                        <th style="width:90px;" class="txt-right">도달률</th>
                        <th style="width:120px;" class="txt-center">진행상황</th>

                        <th style="width:100px;" class="txt-center">분류</th>
                        <th style="width:100px;" class="txt-center">타입</th>
                        <th style="width:80px;" class="txt-center">포지션</th>
                        <th style="width:100px;" class="txt-center">상태</th>
                    </tr>
                </thead>
                <tbody id="coin_list_body">
                    <?php
                    for ($i=0; $i<count($list); $i++) {
                        $isActive = ($list[$i]['x2_ca3'] == '1');
                        $raw_watch = (float)$list[$i]['wr_subject'];
                        
                        // 1. 리스트에서도 목표 타입에 따른 데이터 핸들링 추가
                        $targetType = $list[$i]['x2_target_type'];
                        $raw_goal = (float)$list[$i]['x2_target'];
                        $raw_rate = (float)$list[$i]['x2_rate'];

                        $p_watch = number_format($raw_watch, 8, '.', ',');
                        $t_ex = explode('.', $p_watch);
                        $disp_watch = "<span class='price-int'>".$t_ex[0]."</span>.<span class='price-decimal'>".$t_ex[1]."</span>";
                        $korean_name = isset($upbit_names[$list[$i]['x2_coin']]) ? $upbit_names[$list[$i]['x2_coin']] : 'Unknown';
                        $status_text = ($list[$i]['x2_ca3'] == '1') ? '가동' : '중지';
                        $status_class = ($list[$i]['x2_ca3'] == '1') ? 'bg-on' : 'bg-off';
                    ?>
                    <tr onclick="location.href='<?php echo $list[$i]['href']; ?>'" 
                        data-coin-name="<?php echo $list[$i]['x2_coin']; ?>" 
                        data-watch-val="<?php echo $raw_watch; ?>"
                        data-goal-val="<?php echo $raw_goal; ?>"
                        data-target-type="<?php echo $targetType; ?>"
                        data-rate-val="<?php echo $raw_rate; ?>"
                        data-pos="<?php echo $list[$i]['x2_ca4']; ?>"
                        data-status="<?php echo $list[$i]['x2_ca3']; ?>">
                        
                        <td class="txt-center" style="font-family:'Orbitron'; opacity:0.5;"><?php echo $list[$i]['num']; ?></td>
                        <td style="text-align:left;"><span style="color:var(--neon-cyan); font-weight:700;"><?php echo $list[$i]['x2_coin']; ?></span> <span style="font-size:0.75rem; color:var(--text-dim);"><?php echo $korean_name; ?></span></td>
                        <td class="txt-right"><?php echo $isActive ? $disp_watch : '-'; ?></td>
                        <td class="txt-right realtime-price-cell" data-coin="<?php echo $list[$i]['x2_coin']; ?>"><?php echo $isActive ? "<span class='rt-blue-main'>0</span>.<span class='rt-blue-sub'>00000000</span>" : '-'; ?></td>
                        <td class="txt-right profit-diff-cell" style="font-weight:bold;"><?php echo $isActive ? '-' : '-'; ?></td>
                        <td class="txt-right profit-percent-cell" style="font-weight:bold;"><?php echo $isActive ? '-' : '-'; ?></td>
                        
                        <td class="txt-center target-reach-cell" style="font-weight:900;"><?php echo $isActive ? '-' : '-'; ?></td>
                        <td class="txt-right target-progress-cell" style="font-family:'JetBrains Mono'; color:var(--golden-yellow);"><?php echo $isActive ? '-' : '-'; ?></td>
                        <td class="txt-center">
                            <?php if($isActive) { ?>
                            <div class="mini-chart-bg">
                                <div class="mini-chart-bar" style="width:0%;"></div>
                                <div class="target-dot"></div>
                            </div>
                            <?php } else { echo "-"; } ?>
                        </td>

                        <td class="txt-center"><span style="font-size:0.75rem;"><?php echo $isActive ? $list[$i]['ca_name'] : '-'; ?></span></td>
                        <td class="txt-center"><?php echo $isActive ? "<span class='st_badge st_blue'>".$list[$i]['x2_ca2']."</span>" : '-'; ?></td>
                        <td class="txt-center"><?php echo $isActive ? "<span class='st_badge ".($list[$i]['x2_ca4']=='롱'?'st_blue':'st_pink')."'>".$list[$i]['x2_ca4']."</span>" : '-'; ?></td>
                        <td class="txt-center"><span class="status-btn <?php echo $status_class; ?>"><?php echo $status_text; ?></span></td>
                    </tr>
                    <?php } ?>
                </tbody>
            </table>
        </div>
        <div class="Pagination"><?php echo $write_pages; ?></div>
    </article>
</div>

<div id="SearchOverlay" class="cyber-modal-overlay">
    <div class="cyber-modal">
        <form name="fsearch" method="get">
            <input type="hidden" name="bo_table" value="<?php echo $bo_table; ?>">
            <input type="hidden" name="sca" value="<?php echo $sca; ?>">
            <input type="hidden" name="sfl" value="wr_subject||wr_content">
            <input type="text" name="stx" value="<?php echo stripslashes($stx); ?>" required placeholder="검색어 입력..." style="width:100%; background:var(--input-bg); border:1px solid var(--border-color); color:#fff; font-size:1rem; outline:none; padding:12px; border-radius:4px;">
            <div style="margin-top:25px; display:flex; gap:10px;">
                <button type="submit" class="btn-cyber" style="flex:1;">실행</button>
                <button type="button" onclick="$('#SearchOverlay').fadeOut(200);" class="sub-btn" style="flex:1;">닫기</button>
            </div>
        </form>
    </div>
</div>

<div id="NoticeOverlay" class="cyber-modal-overlay">
    <div class="cyber-modal">
        <div class="modal-header">SYSTEM_NOTICE</div>
        <div style="font-size:0.85rem; line-height:1.6; color:#c9d1d9; max-height:300px; overflow-y:auto; margin-bottom:25px;">
            <?php echo nl2br(stripslashes($board['notice'])); ?>
        </div>
        <div style="text-align:right; display:flex; gap:10px; justify-content: flex-end;">
            <button type="button" onclick="open_notice_window('<?php echo $board_skin_url; ?>');" class="btn-cyber">입력하기</button>
            <button type="button" onclick="$('#NoticeOverlay').fadeOut(200);" class="sub-btn">닫기</button>
        </div>
    </div>
</div>

<script>
function doSort(key) {
    const tbody = document.getElementById('coin_list_body');
    const rows = Array.from(tbody.querySelectorAll('tr'));
    const isAsc = tbody.dataset.sortOrder !== 'asc';
    rows.sort((a, b) => {
        let valA, valB;
        if (key === 'x2_coin') { valA = a.dataset.coinName; valB = b.dataset.coinName; }
        else if (key === 'wr_subject') { valA = parseFloat(a.dataset.watchVal); valB = parseFloat(b.dataset.watchVal); }
        else if (key === 'trade_price') { 
            const aPrice = a.querySelector('.rt-blue-main');
            const bPrice = b.querySelector('.rt-blue-main');
            valA = aPrice ? parseFloat(aPrice.innerText.replace(/,/g, '')) : -1;
            valB = bPrice ? parseFloat(bPrice.innerText.replace(/,/g, '')) : -1;
        }
        else if (key === 'diff') { 
            const aDiff = a.querySelector('.profit-diff-cell');
            valA = (aDiff && aDiff.innerText !== '-') ? parseFloat(aDiff.innerText.replace(/[▲▼, ]/g, '')) : -999999999;
            const bDiff = b.querySelector('.profit-diff-cell');
            valB = (bDiff && bDiff.innerText !== '-') ? parseFloat(bDiff.innerText.replace(/[▲▼, ]/g, '')) : -999999999;
        }
        else if (key === 'percent') { 
            const aPer = a.querySelector('.profit-percent-cell');
            valA = (aPer && aPer.innerText !== '-') ? parseFloat(aPer.innerText.replace(/[()%]/g, '')) : -100;
            const bPer = b.querySelector('.profit-percent-cell');
            valB = (bPer && bPer.innerText !== '-') ? parseFloat(bPer.innerText.replace(/[()%]/g, '')) : -100;
        }
        if (valA < valB) return isAsc ? -1 : 1;
        if (valA > valB) return isAsc ? 1 : -1;
        return 0;
    });
    tbody.dataset.sortOrder = isAsc ? 'asc' : 'desc';
    rows.forEach(row => tbody.appendChild(row));
}

function fetchMultiUpbitPrice() {
    const rows = document.querySelectorAll('#coin_list_body tr');
    let markets = [];
    rows.forEach(row => { 
        if(row.dataset.coinName && row.dataset.status === '1') markets.push(row.dataset.coinName); 
    });
    if(markets.length === 0) return;
    
    fetch(`https://api.upbit.com/v1/ticker?markets=${markets.join(',')}`)
        .then(res => res.json())
        .then(data => {
            data.forEach(item => {
                const row = document.querySelector(`tr[data-coin-name="${item.market}"]`);
                if(!row || row.dataset.status !== '1') return;

                const pCell = row.querySelector('.realtime-price-cell');
                const diffCell = row.querySelector('.profit-diff-cell');
                const percentCell = row.querySelector('.profit-percent-cell');
                const reachCell = row.querySelector('.target-reach-cell');
                const progressCell = row.querySelector('.target-progress-cell');
                const chartBar = row.querySelector('.mini-chart-bar');

                const curPrice = item.trade_price;
                const watchPrice = parseFloat(row.dataset.watchVal);
                const position = row.dataset.pos;

                // 2. 목표선택 타입에 따른 목표가(goalPrice) 실시간 계산 로직 연동
                const targetType = row.dataset.targetType;
                let goalPrice = 0;
                if (targetType === 'rate') {
                    const rate = parseFloat(row.dataset.rateVal) / 100;
                    if (position === "롱") {
                        goalPrice = watchPrice * (1 + rate);
                    } else {
                        goalPrice = watchPrice * (1 - rate);
                    }
                } else {
                    goalPrice = parseFloat(row.dataset.goalVal);
                }

                const formatted = new Intl.NumberFormat('ko-KR', { minimumFractionDigits: 8, maximumFractionDigits: 8 }).format(curPrice);
                const parts = formatted.split('.');
                if(pCell) pCell.innerHTML = `<span class='rt-blue-main'>${parts[0]}</span>.<span class='rt-blue-sub'>${parts[1]}</span>`;

                const diff = curPrice - watchPrice;
                const diffPercent = watchPrice !== 0 ? (diff / watchPrice * 100).toFixed(2) : 0;
                const sign = diff > 0 ? "▲" : (diff < 0 ? "▼" : "");
                const color = diff > 0 ? "#4ade80" : (diff < 0 ? "#ff2d55" : "#c9d1d9");
                
                if(diffCell) {
                    diffCell.style.color = color;
                    diffCell.innerText = `${sign} ${new Intl.NumberFormat('ko-KR').format(Math.abs(diff).toFixed(2))}`;
                }
                if(percentCell) {
                    percentCell.style.color = color;
                    percentCell.innerText = `(${diffPercent}%)`;
                }

                let isReached = false;
                let progressPercent = 0;
                const totalDist = Math.abs(goalPrice - watchPrice);
                const currentMove = Math.abs(curPrice - watchPrice);
                
                if (totalDist > 0) progressPercent = ((currentMove / totalDist) * 100).toFixed(2);

                if (position === "롱") { if (curPrice >= goalPrice) isReached = true; }
                else { if (curPrice <= goalPrice) isReached = true; }

                if(reachCell) {
                    if (isReached) {
                        reachCell.innerText = "도달"; reachCell.style.color = "#4ade80";
                    } else {
                        reachCell.innerText = "미도달"; reachCell.style.color = "#8b949e";
                    }
                }
                
                if(progressCell) progressCell.innerText = progressPercent + "%";
                if(chartBar) {
                    chartBar.style.width = Math.min(progressPercent, 100) + "%";
                    chartBar.style.background = (progressPercent >= 100) ? "var(--golden-yellow)" : "var(--neon-cyan)";
                }
            });
        });
}

function open_notice_window(skinUrl) {
    var w=800, h=600, left=(screen.width/2)-(w/2), top=(screen.height/2)-(h/2);
    window.open(skinUrl + '/notice.php', 'notice_win', 'width='+w+',height='+h+',left='+left+',top='+top+',scrollbars=yes');
}

$(function() {
    fetchMultiUpbitPrice(); setInterval(fetchMultiUpbitPrice, 1000);
    const canvas = document.getElementById('starCanvas'); const ctx = canvas.getContext('2d'); let stars = [];
    function initStars() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; stars = []; for(let i=0; i<80; i++) stars.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, size: Math.random() * 1.2, speed: Math.random() * 0.15 }); }
    function drawStars() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "rgba(201, 209, 217, 0.3)"; stars.forEach(s => { ctx.beginPath(); ctx.arc(s.x, s.y, s.size, 0, Math.PI*2); ctx.fill(); s.y += s.speed; if(s.y > canvas.height) s.y = 0; }); requestAnimationFrame(drawStars); }
    initStars(); drawStars(); window.addEventListener('resize', initStars);
    $('.cyber-modal-overlay').on('click', function(e) { if ($(e.target).hasClass('cyber-modal-overlay')) { $(this).fadeOut(200); } });
});
</script>