OLDBOY/skin/board/stock_schedule_dividend/chart.php
<?php
if (!defined('_GNUBOARD_')) exit;

/**
 * 일봉 차트 데이터 호출 함수 (한국투자증권 API)
 */
function get_stock_chart_data($stock_code, $access_token, $appkey, $appsecret) {
    // 1. 종목코드 정제 (공백 제거 및 6자리 맞춤)
    $stock_code = str_pad(trim($stock_code), 6, "0", STR_PAD_LEFT);
    
    // API URL (실운영 주소)
    $url = "https://openapi.koreainvestment.com:9443/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice";
    
    // [검증 포인트] 날짜 강제 고정 (2026-03-27 금요일 기준)
    $end_date = "20260327"; 
    $start_date = date("Ymd", strtotime("20260327 -90 days")); // 넉넉하게 90일치 데이터

    $params = [
        "FID_COND_MRKT_DIV_CODE" => "J",
        "FID_INPUT_ISCD"         => $stock_code,
        "FID_INPUT_DATE_1"       => $start_date,
        "FID_INPUT_DATE_2"       => $end_date,
        "FID_PERIOD_DIV_CODE"    => "D",
        "FID_ORG_ADJ_PRC"        => "1" // 수정주가 적용
    ];

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url . "?" . http_build_query($params));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Content-Type: application/json; charset=utf-8",
        "Authorization: Bearer " . $access_token,
        "appkey: " . $appkey,
        "appsecret: " . $appsecret,
        "tr_id: FHKST03010100",
        "custtype: P"
    ]);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    $res = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if (!$res || $http_code !== 200) {
        return ['error' => 'API 통신 실패 (HTTP '.$http_code.')', 'debug' => $res];
    }
    
    $data = json_decode($res, true);
    
    // KIS API 자체 응답 코드 확인
    if (!isset($data['rt_cd']) || $data['rt_cd'] !== '0') {
        return ['error' => 'KIS API 오류: ' . ($data['rt_msg'] ?? '알 수 없는 오류'), 'code' => $data['rt_cd'] ?? 'None'];
    }

    $output = [];
    if (!empty($data['output2']) && is_array($data['output2'])) {
        // KIS API는 최신순(역순)이므로 차트를 위해 다시 뒤집음(오름차순)
        $raw_data = array_reverse($data['output2']);
        
        foreach ($raw_data as $day) {
            $date_str = isset($day['stck_bsop_date']) ? trim($day['stck_bsop_date']) : '';
            if (strlen($date_str) !== 8) continue;
            
            // 시/고/저/종 데이터 추출 및 숫자형 변환 (필수)
            $o = (float)$day['stck_oprc'];
            $h = (float)$day['stck_hgpr'];
            $l = (float)$day['stck_lwpr'];
            $c = (float)$day['stck_clpr'];

            if ($c <= 0) continue; // 데이터가 없는 날 제외

            $output[] = [
                'time'  => substr($date_str, 0, 4) . "-" . substr($date_str, 4, 2) . "-" . substr($date_str, 6, 2),
                'open'  => $o,
                'high'  => $h,
                'low'   => $l,
                'close' => $c
            ];
        }
    }
    return $output;
}

// API 키 로드
include_once('/home/www/DB/key_stock_api.php');
if (function_exists('get_access_token')) {
    $access_token = get_access_token($STOCK_ACCESS_KEY, $STOCK_SECRET_KEY);
    $chart_result = get_stock_chart_data($view['wr_subject'], $access_token, $STOCK_ACCESS_KEY, $STOCK_SECRET_KEY);
} else {
    $chart_result = ['error' => 'get_access_token 함수가 존재하지 않습니다.'];
}

$has_error = isset($chart_result['error']);
// JSON_NUMERIC_CHECK을 사용하여 전송 시 숫자 형식을 유지함
$chart_json = $has_error ? "[]" : json_encode($chart_result, JSON_NUMERIC_CHECK);
?>

<style>
    /* 그누보드 테마와의 충돌을 피하기 위한 명시적 스타일 설정 */
    #chart_container_wrap { width: 100%; background: #080c14; border: 1px solid rgba(148, 163, 184, 0.2); border-radius: 12px; overflow: hidden; margin-bottom: 24px; position: relative; box-shadow: 0 4px 20px rgba(0,0,0,0.3); }
    #stock_candle_chart { width: 100%; height: 450px; min-height: 450px; background: #080c14; }
    .chart-info-overlay { position: absolute; top: 20px; left: 20px; z-index: 20; color: #fff; pointer-events: none; }
    .chart-info-overlay h4 { margin: 0; font-size: 15px; font-weight: 900; color: #38bdf8; letter-spacing: 1.5px; text-transform: uppercase; text-shadow: 0 2px 4px rgba(0,0,0,0.5); }
    .chart-info-overlay p { margin: 6px 0 0; font-size: 12px; font-family: 'JetBrains Mono', monospace; color: #94a3b8; font-weight: 500; }
    .api-error-box { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #94a3b8; font-size: 14px; text-align: center; padding: 20px; }
    .api-error-box i { font-size: 32px; color: #f43f5e; margin-bottom: 16px; }
</style>

<div id="chart_container_wrap">
    <div class="chart-info-overlay">
        <h4>Daily Candle Chart</h4>
        <p><?php echo get_text($view['wr_subject_krw']); ?> (<?php echo $view['wr_subject']; ?>) | Fixed: 2026-03-27</p>
    </div>
    <div id="stock_candle_chart">
        <?php if ($has_error) { ?>
            <div class="api-error-box">
                <i class="fa-solid fa-triangle-exclamation"></i>
                <div style="font-weight:700; color:#e2e8f0;"><?php echo $chart_result['error']; ?></div>
                <div style="font-size:11px; margin-top:10px; opacity:0.6;">API 호출 결과가 올바르지 않습니다.</div>
            </div>
        <?php } ?>
    </div>
</div>

<!-- Lightweight Charts CDN - 안정적인 최신 버전 사용 -->
<script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const chartData = <?php echo $chart_json; ?>;
    const container = document.getElementById('stock_candle_chart');
    
    // PHP 레벨에서 에러가 있었는지 확인
    if (<?php echo $has_error ? 'true' : 'false'; ?> || !chartData || chartData.length === 0) {
        console.error("차트 데이터 없음:", <?php echo json_encode($chart_result); ?>);
        return;
    }

    // 1. 차트 인스턴스 초기화 (컨테이너 크기 명시)
    const chart = LightweightCharts.createChart(container, {
        width: container.clientWidth,
        height: 450,
        layout: {
            background: { type: 'solid', color: 'transparent' },
            textColor: '#94a3b8',
            fontFamily: 'Inter, sans-serif',
        },
        grid: {
            vertLines: { color: 'rgba(30, 41, 59, 0.1)' },
            horzLines: { color: 'rgba(30, 41, 59, 0.1)' },
        },
        rightPriceScale: {
            borderColor: 'rgba(148, 163, 184, 0.2)',
            scaleMargins: { top: 0.1, bottom: 0.1 },
        },
        timeScale: {
            borderColor: 'rgba(148, 163, 184, 0.2)',
            timeVisible: true,
        },
    });

    // 2. 캔들 시리즈 추가
    const candleSeries = chart.addCandlestickSeries({
        upColor: '#f43f5e',     // 한국식 상승 (빨강)
        downColor: '#3b82f6',   // 한국식 하락 (파랑)
        borderVisible: false,
        wickUpColor: '#f43f5e',
        wickDownColor: '#3b82f6',
    });

    // 3. 데이터 주입 시도
    try {
        console.log("차트 데이터 주입 시도:", chartData.length, "건");
        candleSeries.setData(chartData);
        chart.timeScale().fitContent();
    } catch (err) {
        console.error("차트 렌더링 오류:", err);
        container.innerHTML += '<div style="color:red; font-size:11px; position:absolute; bottom:10px; width:100%; text-align:center;">렌더링 실패: ' + err.message + '</div>';
    }

    // 반응형 대응
    const resizeObserver = new ResizeObserver(entries => {
        if (entries.length === 0) return;
        const newWidth = entries[0].contentRect.width;
        chart.applyOptions({ width: newWidth });
    });
    resizeObserver.observe(container);
});
</script>