커피 시장 위젯 개발 후기 - 라이믹스 기반 실시간 환율·선물 지수 표시
라이프오브파이
$fx_url = 'https://api.fxratesapi.com/latest?api_key=' . FXRATESAPI_KEY . '&base=USD¤cies=KRW,EUR';
$fx_pair = coffee_market_widget_fetch_json_with_raw($fx_url);// EUR/USD 비율을 EUR/KRW로 변환
$eur_usd = (float)$fx['rates']['EUR'];
if ($usd_rate !== null) {
$eur_rate = $usd_rate / $eur_usd; // 환율 계산
$data['eurkrw'] = $eur_rate;
}// Arabica 데이터
$stooq_kc_url = 'https://stooq.com/q/l/?s=kc.f&f=sd2t2ohlcv&h&e=csv';
$csv_kc = coffee_market_widget_http_get($stooq_kc_url);
// Robusta 데이터
$stooq_rm_url = 'https://stooq.com/q/l/?s=rm.f&f=sd2t2ohlcv&h&e=csv';
$csv_rm = coffee_market_widget_http_get($stooq_rm_url);$lines = preg_split('/\r?\n/', trim($csv_kc));
foreach ($lines as $idx => $line) {
if ($line === '') { continue; }
if ($idx === 0 && stripos($line, 'symbol') !== false) { continue; } // 헤더 스킵
$cols = str_getcsv($line);
if (count($cols) < 7) { continue; }
$close = is_numeric($cols[6] ?? null) ? (float)$cols[6] : null; // 종가 추출
// ...
}if (!defined('__XE__') && !defined('__RHYMIX__')) {
exit('This script must be executed within Rhymix.');
}function coffee_market_widget_http_get(string $url, array &$debug = null): ?string
{
$accept = (stripos($url, 'stooq.com') !== false) ? 'text/csv' : 'application/json';
// 1단계: Rhymix HTTP (라이믹스 내장 HTTP 클라이언트)
if (class_exists('Rhymix\\Framework\\Http')) {
try {
$response = \Rhymix\Framework\Http::get($url, [], [
'timeout' => 30,
'headers' => [
'Accept' => $accept,
'User-Agent' => 'CoffeeMarketWidget/1.2',
],
]);
if (is_string($response) && $response !== '') {
return $response;
}
} catch (\Throwable $e) {
$last_error = 'Rhymix HTTP: ' . $e->getMessage();
}
}
// 2단계: cURL
if (function_exists('curl_init')) {
$handle = curl_init($url);
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_FOLLOWLOCATION => true,
// ...
]);
$body = curl_exec($handle);
if ($body !== false && $body !== '') {
return $body;
}
}
// 3단계: file_get_contents (최후의 수단)
$context = stream_context_create([/* ... */]);
$body = @file_get_contents($url, false, $context);
return $body ?: null;
}function coffee_market_widget_cache_dir(): string
{
if (defined('_XE_PATH_')) {
$base = _XE_PATH_; // 라이믹스 환경
} else {
$base = dirname(__DIR__); // 독립 실행 환경
}
$dir = rtrim($base, '/\\') . '/files/cache/' . COFFEE_MARKET_WIDGET_CACHE_SUBDIR;
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
return $dir;
}function coffee_market_widget_cache_file(): string
{
return coffee_market_widget_cache_dir() . '/snapshot_' . date('Ymd') . '.json';
}// 오늘자 캐시가 있고 디버그가 아니면 사용
if (!COFFEE_MARKET_WIDGET_DEBUG && coffee_market_widget_nonempty_file($cache_file)) {
$snap = coffee_market_widget_read_json_file($cache_file);
if (is_array($snap)) {
$snap['debug']['cache_hit'] = true;
return $snap;
}
}$debug_log = ['cache_hit' => false];
$errors = [];
// FX API 호출
if (is_array($fx) && isset($fx['success']) && $fx['success'] === true) {
// 성공 처리
} else {
$errors[] = 'FXRatesAPI 응답 오류';
$debug_log['fx_parse_error'] = 'Invalid response structure';
}
// 최종 에러 메시지 통합
if ($errors) {
$data['error'] = implode(' / ', array_unique($errors));
}
// 디버그 정보 포함
$data['debug'] = $debug_log;{
"debug": {
"cache_hit": false,
"fx_url": "https://api.fxratesapi.com/latest?...",
"fx_source": "fxratesapi.com",
"fx_raw": "{...}",
"fx_http_method": "cURL",
"stooq_kc_preview": "Exceeded the daily hits limit"
}
}@keyframes ticker-scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.ticker-content {
display: inline-flex;
animation: ticker-scroll 30s linear infinite;
}
.ticker-content:hover {
animation-play-state: paused; /* 마우스 호버 시 정지 */
}<!-- 첫 번째 세트 -->
<span class="widget-title">커피 시장 현황</span>
<div class="market-item">...</div>
<!-- ... -->
<!-- 반복 콘텐츠 (무한 스크롤 효과) -->
<span class="widget-title">커피 시장 현황</span>
<div class="market-item">...</div>
<!-- ... -->.coffee-market-widget {
background: #121212;
color: #f8f5ef;
border-radius: 0px;
padding: 10px 0;
}<?= htmlspecialchars(coffee_market_widget_format_number($data['usdkrw']), ENT_QUOTES, 'UTF-8'); ?>