<?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>