| Server IP : 158.247.231.215 / Your IP : 216.73.216.159 Web Server : Apache/2.4.41 (Ubuntu) System : Linux CTMS 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64 User : www-data ( 33) PHP Version : 8.0.30 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /mnt/blockstorage/ctms/wp-content/themes/zakra/ |
Upload File : |
<?php
/**
* Template Name: APK Detail
* Template Post Type: page
*/
if ( ! is_user_logged_in() ) {
wp_redirect( wp_login_url( get_permalink() ) );
exit;
}
global $wpdb;
$content_id = isset( $_GET['id'] ) ? max( 0, intval( $_GET['id'] ) ) : 0;
if ( ! $content_id ) {
wp_redirect( home_url( '/apk-down/' ) );
exit;
}
// Content
$content = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM vr_contents WHERE id = %d AND status = 'active'", $content_id )
);
if ( ! $content ) {
wp_redirect( home_url( '/apk-down/' ) );
exit;
}
// Versions
$versions = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM vr_content_versions WHERE content_id = %d ORDER BY created_at DESC",
$content_id
)
);
// Images
$images = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM vr_content_images WHERE content_id = %d ORDER BY display_order ASC",
$content_id
)
);
$thumbnail_url = '';
$gallery_urls = array();
foreach ( $images as $img ) {
$url = 'https://training.contentsda.kr/vr-content-uploads/' . $img->image_path;
if ( 'thumbnail' === $img->image_type && ! $thumbnail_url ) {
$thumbnail_url = $url;
} else {
$gallery_urls[] = $url;
}
}
if ( ! $thumbnail_url && $gallery_urls ) {
$thumbnail_url = array_shift( $gallery_urls );
}
// Current user's download requests for this content
$current_user_id = get_current_user_id();
$requests = $wpdb->get_results(
$wpdb->prepare(
"SELECT r.*, v.version_number FROM vr_download_requests r
INNER JOIN vr_content_versions v ON r.version_id = v.id
WHERE r.user_id = %d AND r.content_id = %d
ORDER BY r.created_at DESC",
$current_user_id,
$content_id
)
);
// Build requests map: [version_id][device_type] = latest request
$req_map = array();
foreach ( $requests as $req ) {
$key = $req->version_id . '_' . $req->device_type;
if ( ! isset( $req_map[ $key ] ) ) {
$req_map[ $key ] = $req;
}
}
// Generate JWT for JavaScript API calls
$secret_key = defined( 'JWT_AUTH_SECRET_KEY' ) ? JWT_AUTH_SECRET_KEY : '';
$now = time();
$jwt_header = rtrim( strtr( base64_encode( wp_json_encode( array( 'typ' => 'JWT', 'alg' => 'HS256' ) ) ), '+/', '-_' ), '=' );
$jwt_payload = rtrim(
strtr(
base64_encode(
wp_json_encode(
array(
'iss' => get_bloginfo( 'url' ),
'iat' => $now,
'nbf' => $now,
'exp' => $now + ( 7 * DAY_IN_SECONDS ),
'data' => array( 'user' => array( 'id' => $current_user_id ) ),
)
)
),
'+/',
'-_'
),
'='
);
$jwt_sig = rtrim( strtr( base64_encode( hash_hmac( 'sha256', "$jwt_header.$jwt_payload", $secret_key, true ) ), '+/', '-_' ), '=' );
$jwt_token = "$jwt_header.$jwt_payload.$jwt_sig";
// Platform stats
$total_users = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->users}" );
$total_contents = (int) $wpdb->get_var( "SELECT COUNT(*) FROM vr_contents WHERE status = 'active'" );
// Latest version for default selection
$latest_version = $versions ? $versions[0] : null;
$list_page_url = home_url( '/apk-down/' );
// Remove default .zak-container/.zak-row so hero can span full viewport width.
remove_action( 'zakra_action_before_content', 'zakra_content_start', 20 );
remove_action( 'zakra_action_after_content', 'zakra_content_end', 10 );
get_header();
?>
<style>
.apkd-hero {
background: #7B3FBE;
padding: 36px 20px;
text-align: center;
}
.apkd-hero h1 { font-size: 26px; font-weight: 700; margin: 0; color: #fff; }
.apkd-breadcrumb {
max-width: 1200px;
margin: 16px auto 0;
padding: 0 20px;
font-size: 13px;
color: #888;
}
.apkd-breadcrumb a { color: #7B3FBE; text-decoration: none; }
.apkd-breadcrumb a:hover { text-decoration: underline; }
.apkd-wrap {
max-width: 1200px;
margin: 30px auto 50px;
padding: 0 20px;
display: grid;
grid-template-columns: 1fr 360px;
gap: 30px;
align-items: start;
}
@media (max-width: 900px) {
.apkd-wrap { grid-template-columns: 1fr; }
}
/* ── Left column ── */
.apkd-left {}
.apkd-tag {
display: inline-block;
background: #d4edda;
color: #1b5e20;
font-size: 12px;
font-weight: 700;
padding: 4px 12px;
border-radius: 20px;
margin-bottom: 14px;
}
.apkd-content-title {
font-size: 24px;
font-weight: 800;
margin: 0 0 16px;
color: #1a1a1a;
}
.apkd-img-wrap {
position: relative;
border-radius: 16px;
overflow: hidden;
margin-bottom: 20px;
background: #f0e8ff;
min-height: 240px;
display: flex;
align-items: center;
justify-content: center;
}
.apkd-img-wrap img {
width: 100%;
display: block;
border-radius: 16px;
}
.apkd-img-placeholder { font-size: 80px; }
.apkd-stats {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.apkd-stat {
background: rgba(123,63,190,.08);
border-radius: 10px;
padding: 12px 20px;
text-align: center;
min-width: 100px;
}
.apkd-stat-num { font-size: 22px; font-weight: 800; color: #7B3FBE; }
.apkd-stat-label { font-size: 12px; color: #666; margin-top: 2px; }
.apkd-desc {
font-size: 15px;
color: #444;
line-height: 1.7;
white-space: pre-line;
}
/* ── Right column: APK card ── */
.apkd-card {
background: #fff;
border-radius: 14px;
box-shadow: 0 4px 20px rgba(0,0,0,.1);
overflow: hidden;
position: sticky;
top: 80px;
}
.apkd-card-header {
background: #7B3FBE;
color: #fff;
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.apkd-card-header h3 { margin: 0; font-size: 16px; font-weight: 700; }
.apkd-version-badge {
background: rgba(255,255,255,.25);
border-radius: 20px;
padding: 3px 12px;
font-size: 13px;
font-weight: 600;
}
.apkd-card-body { padding: 20px; }
.apkd-select-label {
font-size: 13px;
font-weight: 600;
color: #555;
margin-bottom: 8px;
}
.apkd-version-select {
width: 100%;
padding: 10px 14px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
color: #1a1a1a;
outline: none;
cursor: pointer;
margin-bottom: 16px;
transition: border-color .2s;
}
.apkd-version-select:focus { border-color: #7B3FBE; }
.apkd-device-group { display: flex; gap: 0; margin-bottom: 16px; border-radius: 8px; overflow: hidden; border: 2px solid #7B3FBE; }
.apkd-device-btn {
flex: 1;
padding: 10px;
border: none;
background: #fff;
color: #7B3FBE;
font-weight: 700;
font-size: 14px;
cursor: pointer;
transition: background .2s, color .2s;
}
.apkd-device-btn.active { background: #7B3FBE; color: #fff; }
.apkd-device-btn:first-child { border-right: 1px solid #7B3FBE; }
.apkd-dl-btn {
width: 100%;
padding: 13px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: background .2s, opacity .2s;
margin-bottom: 16px;
}
.apkd-dl-btn.action-request { background: #00897B; color: #fff; }
.apkd-dl-btn.action-download { background: #2E7D32; color: #fff; }
.apkd-dl-btn.action-pending { background: #EF6C00; color: #fff; cursor: not-allowed; }
.apkd-dl-btn.action-done { background: #546E7A; color: #fff; }
.apkd-dl-btn:disabled { opacity: .7; cursor: not-allowed; }
.apkd-status-box {
padding: 10px 14px;
border-radius: 8px;
font-size: 13px;
margin-bottom: 16px;
display: none;
}
.apkd-status-box.show { display: block; }
.apkd-status-pending { background: #FFF3E0; color: #E65100; border: 1px solid #FFCC80; }
.apkd-status-approved { background: #E8F5E9; color: #1B5E20; border: 1px solid #A5D6A7; }
.apkd-status-done { background: #ECEFF1; color: #37474F; border: 1px solid #B0BEC5; }
.apkd-status-rejected { background: #FFEBEE; color: #B71C1C; border: 1px solid #FFCDD2; }
.apkd-divider { border: none; border-top: 1px solid #eee; margin: 16px 0; }
.apkd-section-title { font-size: 13px; font-weight: 700; color: #555; margin-bottom: 10px; }
.apkd-avail-list { list-style: none; margin: 0 0 16px; padding: 0; }
.apkd-avail-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 7px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 13px;
}
.apkd-avail-list li:last-child { border-bottom: none; }
.apkd-avail-dot { width: 8px; height: 8px; border-radius: 50%; background: #7B3FBE; margin-right: 8px; flex-shrink: 0; }
/* Version history accordion */
.apkd-history {}
.apkd-history-item { border: 1px solid #eee; border-radius: 8px; margin-bottom: 8px; overflow: hidden; }
.apkd-history-toggle {
width: 100%;
text-align: left;
padding: 12px 14px;
background: #fafafa;
border: none;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
font-weight: 700;
color: #333;
}
.apkd-history-toggle:hover { background: #f0e8ff; }
.apkd-history-toggle .arrow { transition: transform .2s; }
.apkd-history-toggle.open .arrow { transform: rotate(180deg); }
.apkd-history-body { display: none; padding: 10px 14px; background: #fff; }
.apkd-history-body.open { display: block; }
.apkd-dl-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 7px 0;
border-bottom: 1px solid #f5f5f5;
font-size: 13px;
}
.apkd-dl-row:last-child { border-bottom: none; }
.apkd-dl-row-label { display: flex; align-items: center; gap: 8px; color: #444; }
.apkd-dl-row-label .device-icon { font-size: 16px; }
.apkd-history-dl-btn {
padding: 5px 12px;
border-radius: 6px;
border: none;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: background .2s;
}
.apkd-history-dl-btn.req { background: #7B3FBE; color: #fff; }
.apkd-history-dl-btn.dl { background: #2E7D32; color: #fff; }
.apkd-history-dl-btn.wait { background: #EF6C00; color: #fff; cursor: not-allowed; }
.apkd-history-dl-btn.done { background: #90A4AE; color: #fff; cursor: not-allowed; }
.apkd-history-dl-btn:disabled { opacity: .7; }
.apkd-no-apk { font-size: 12px; color: #aaa; font-style: italic; }
/* Contents info section */
.apkd-info-section {
max-width: 1200px;
margin: 0 auto 50px;
padding: 0 20px;
}
.apkd-info-box {
background: #fff;
border-radius: 14px;
box-shadow: 0 2px 10px rgba(0,0,0,.07);
padding: 30px;
}
.apkd-info-box h2 { font-size: 20px; font-weight: 700; margin: 0 0 16px; color: #1a1a1a; }
.apkd-info-body { font-size: 15px; color: #444; line-height: 1.8; white-space: pre-line; }
</style>
<div class="apkd-hero">
<h1>Learn with Nuguna VR World</h1>
</div>
<div class="apkd-breadcrumb">
<a href="<?php echo esc_url( $list_page_url ); ?>">APK Down</a> › <?php echo esc_html( $content->title ); ?>
</div>
<div class="apkd-wrap">
<!-- ── Left: Content Info ── -->
<div class="apkd-left">
<span class="apkd-tag">누구나 VR 교육 콘텐츠</span>
<h1 class="apkd-content-title"><?php echo esc_html( $content->title ); ?></h1>
<div class="apkd-img-wrap">
<?php if ( $thumbnail_url ) : ?>
<img src="<?php echo esc_url( $thumbnail_url ); ?>" alt="<?php echo esc_attr( $content->title ); ?>" />
<?php else : ?>
<span class="apkd-img-placeholder">📱</span>
<?php endif; ?>
</div>
<div class="apkd-stats">
<div class="apkd-stat">
<div class="apkd-stat-num"><?php echo number_format( $total_users ); ?>+</div>
<div class="apkd-stat-label">사용자</div>
</div>
<div class="apkd-stat">
<div class="apkd-stat-num"><?php echo $total_contents; ?>+</div>
<div class="apkd-stat-label">콘텐츠</div>
</div>
<?php if ( $versions ) : ?>
<div class="apkd-stat">
<div class="apkd-stat-num"><?php echo count( $versions ); ?></div>
<div class="apkd-stat-label">버전</div>
</div>
<?php endif; ?>
</div>
<div class="apkd-desc"><?php echo esc_html( $content->description ); ?></div>
</div>
<!-- ── Right: APK Download Card ── -->
<div class="apkd-right">
<div class="apkd-card">
<div class="apkd-card-header">
<h3>APK Download</h3>
<span class="apkd-version-badge" id="selected-version-badge">
<?php echo $latest_version ? 'v' . esc_html( $latest_version->version_number ) : '-'; ?>
</span>
</div>
<div class="apkd-card-body">
<?php if ( $versions ) : ?>
<div class="apkd-select-label">Select Version</div>
<select class="apkd-version-select" id="version-select" onchange="onVersionChange()">
<?php foreach ( $versions as $ver ) : ?>
<option value="<?php echo intval( $ver->id ); ?>"
data-version="<?php echo esc_attr( $ver->version_number ); ?>"
data-has-pico="<?php echo $ver->pico_apk_path ? '1' : '0'; ?>"
data-has-quest="<?php echo $ver->quest_apk_path ? '1' : '0'; ?>">
v<?php echo esc_html( $ver->version_number ); ?>
<?php if ( $ver->release_notes ) : ?>
– <?php echo esc_html( mb_substr( $ver->release_notes, 0, 30 ) ); ?>
<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
<?php else : ?>
<p style="color:#999;font-size:14px;margin-bottom:16px;">등록된 버전이 없습니다.</p>
<?php endif; ?>
<div class="apkd-device-group">
<button class="apkd-device-btn active" id="btn-pico" onclick="selectDevice('pico')">Pico</button>
<button class="apkd-device-btn" id="btn-quest" onclick="selectDevice('quest')">Quest</button>
</div>
<div class="apkd-status-box" id="status-box"></div>
<button
class="apkd-dl-btn action-request"
id="main-dl-btn"
onclick="handleDownload()"
<?php echo ! $versions ? 'disabled' : ''; ?>
>
<?php echo $versions ? 'Download Free APK' : '버전 없음'; ?>
</button>
<hr class="apkd-divider" />
<!-- Available Versions list -->
<?php if ( $versions ) : ?>
<div class="apkd-section-title">Available Versions</div>
<ul class="apkd-avail-list">
<?php foreach ( $versions as $ver ) : ?>
<li>
<span style="display:flex;align-items:center;">
<span class="apkd-avail-dot"></span>
v<?php echo esc_html( $ver->version_number ); ?>
</span>
<span style="color:#aaa;font-size:11px;"><?php echo esc_html( substr( $ver->created_at, 0, 10 ) ); ?></span>
</li>
<?php endforeach; ?>
</ul>
<hr class="apkd-divider" />
<!-- Version History -->
<div class="apkd-section-title">Version History</div>
<div class="apkd-history">
<?php foreach ( $versions as $ver ) : ?>
<?php
$pico_req = isset( $req_map[ $ver->id . '_pico' ] ) ? $req_map[ $ver->id . '_pico' ] : null;
$quest_req = isset( $req_map[ $ver->id . '_quest' ] ) ? $req_map[ $ver->id . '_quest' ] : null;
?>
<div class="apkd-history-item">
<button
class="apkd-history-toggle"
onclick="toggleHistory(this)"
aria-expanded="false"
>
<span>v<?php echo esc_html( $ver->version_number ); ?></span>
<span class="arrow">▼</span>
</button>
<div class="apkd-history-body">
<!-- Pico row -->
<div class="apkd-dl-row">
<span class="apkd-dl-row-label">
<span class="device-icon">🥽</span> Pico
</span>
<?php if ( $ver->pico_apk_path ) : ?>
<?php
$btn_class = 'req';
$btn_label = '요청하기';
$btn_extra = '';
$btn_id = 'hist-' . $ver->id . '-pico';
if ( $pico_req ) {
if ( 'approved' === $pico_req->status && ! $pico_req->is_downloaded ) {
$btn_class = 'dl';
$btn_label = '다운로드';
} elseif ( 'pending' === $pico_req->status ) {
$btn_class = 'wait';
$btn_label = '대기중';
$btn_extra = 'disabled';
} elseif ( 'approved' === $pico_req->status && $pico_req->is_downloaded ) {
$btn_class = 'done';
$btn_label = '완료';
$btn_extra = 'disabled';
}
}
?>
<button
id="<?php echo esc_attr( $btn_id ); ?>"
class="apkd-history-dl-btn <?php echo esc_attr( $btn_class ); ?>"
data-version-id="<?php echo intval( $ver->id ); ?>"
data-device="pico"
data-request-id="<?php echo $pico_req ? intval( $pico_req->id ) : '0'; ?>"
onclick="handleHistoryDownload(this)"
<?php echo $btn_extra; ?>
><?php echo esc_html( $btn_label ); ?></button>
<?php else : ?>
<span class="apkd-no-apk">미제공</span>
<?php endif; ?>
</div>
<!-- Quest row -->
<div class="apkd-dl-row">
<span class="apkd-dl-row-label">
<span class="device-icon">🎮</span> Quest
</span>
<?php if ( $ver->quest_apk_path ) : ?>
<?php
$btn_class = 'req';
$btn_label = '요청하기';
$btn_extra = '';
$btn_id = 'hist-' . $ver->id . '-quest';
if ( $quest_req ) {
if ( 'approved' === $quest_req->status && ! $quest_req->is_downloaded ) {
$btn_class = 'dl';
$btn_label = '다운로드';
} elseif ( 'pending' === $quest_req->status ) {
$btn_class = 'wait';
$btn_label = '대기중';
$btn_extra = 'disabled';
} elseif ( 'approved' === $quest_req->status && $quest_req->is_downloaded ) {
$btn_class = 'done';
$btn_label = '완료';
$btn_extra = 'disabled';
}
}
?>
<button
id="<?php echo esc_attr( $btn_id ); ?>"
class="apkd-history-dl-btn <?php echo esc_attr( $btn_class ); ?>"
data-version-id="<?php echo intval( $ver->id ); ?>"
data-device="quest"
data-request-id="<?php echo $quest_req ? intval( $quest_req->id ) : '0'; ?>"
onclick="handleHistoryDownload(this)"
<?php echo $btn_extra; ?>
><?php echo esc_html( $btn_label ); ?></button>
<?php else : ?>
<span class="apkd-no-apk">미제공</span>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div><!-- /card-body -->
</div><!-- /card -->
</div><!-- /right -->
</div><!-- /wrap -->
<!-- Contents Info -->
<div class="apkd-info-section">
<div class="apkd-info-box">
<h2>Contents Info</h2>
<div class="apkd-info-body"><?php echo esc_html( $content->description ); ?></div>
</div>
</div>
<script>
const API_BASE = '<?php echo esc_js( home_url( '/api/custom-api' ) ); ?>';
const JWT_TOKEN = '<?php echo esc_js( $jwt_token ); ?>';
const USER_ID = <?php echo intval( $current_user_id ); ?>;
const CONTENT_ID = <?php echo intval( $content_id ); ?>;
let selectedDevice = 'pico';
// Map of [versionId + '_' + device] => request data from PHP
const requestMap = <?php echo wp_json_encode( $req_map ); ?>;
function getSelectedVersion() {
const sel = document.getElementById('version-select');
if (!sel) return null;
const opt = sel.options[sel.selectedIndex];
return {
id: parseInt(sel.value),
number: opt.dataset.version,
hasPico: opt.dataset.hasPico === '1',
hasQuest: opt.dataset.hasQuest === '1',
};
}
function onVersionChange() {
const ver = getSelectedVersion();
if (!ver) return;
document.getElementById('selected-version-badge').textContent = 'v' + ver.number;
updateMainBtn();
}
function selectDevice(device) {
selectedDevice = device;
document.getElementById('btn-pico').classList.toggle('active', device === 'pico');
document.getElementById('btn-quest').classList.toggle('active', device === 'quest');
updateMainBtn();
}
function updateMainBtn() {
const ver = getSelectedVersion();
if (!ver) return;
const btn = document.getElementById('main-dl-btn');
const statusBox = document.getElementById('status-box');
const key = ver.id + '_' + selectedDevice;
const req = requestMap[key];
const hasApk = (selectedDevice === 'pico' ? ver.hasPico : ver.hasQuest);
statusBox.className = 'apkd-status-box';
statusBox.textContent = '';
if (!hasApk) {
btn.textContent = selectedDevice.charAt(0).toUpperCase() + selectedDevice.slice(1) + ' APK 미제공';
btn.className = 'apkd-dl-btn action-done';
btn.disabled = true;
return;
}
btn.disabled = false;
if (!req) {
btn.textContent = 'Download Free APK';
btn.className = 'apkd-dl-btn action-request';
} else if (req.status === 'pending') {
btn.textContent = '승인 대기중';
btn.className = 'apkd-dl-btn action-pending';
btn.disabled = true;
showStatus('pending', '다운로드 요청이 접수되었습니다. 관리자 승인 후 다운로드 가능합니다.');
} else if (req.status === 'approved' && !req.is_downloaded) {
btn.textContent = '다운로드 ↓';
btn.className = 'apkd-dl-btn action-download';
showStatus('approved', '다운로드가 승인되었습니다!');
} else if (req.status === 'approved' && req.is_downloaded) {
btn.textContent = '다운로드 완료';
btn.className = 'apkd-dl-btn action-done';
showStatus('done', '이미 다운로드하셨습니다. 새 요청을 하시려면 버튼을 클릭하세요.');
btn.disabled = false;
} else if (req.status === 'rejected') {
btn.textContent = '다시 요청하기';
btn.className = 'apkd-dl-btn action-request';
showStatus('rejected', '요청이 반려되었습니다. 다시 요청할 수 있습니다.');
}
}
function showStatus(type, msg) {
const box = document.getElementById('status-box');
box.textContent = msg;
box.className = 'apkd-status-box show apkd-status-' + type;
}
async function handleDownload() {
const ver = getSelectedVersion();
if (!ver) return;
const key = ver.id + '_' + selectedDevice;
const req = requestMap[key];
if (req && req.status === 'approved' && !req.is_downloaded) {
await executeDownload(req.id, ver.number);
} else {
await requestDownload(ver.id, selectedDevice);
}
}
async function requestDownload(versionId, device) {
const btn = document.getElementById('main-dl-btn');
btn.disabled = true;
btn.textContent = '요청 중...';
try {
const fd = new FormData();
fd.append('user_id', USER_ID);
fd.append('content_id', CONTENT_ID);
fd.append('version_id', versionId);
fd.append('device_type', device);
const resp = await fetch(API_BASE + '/vr-download-request.php', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + JWT_TOKEN },
body: fd,
});
const data = await resp.json();
if (data.status === 'success') {
const key = versionId + '_' + device;
requestMap[key] = {
id: data.data.request_id,
status: 'pending',
is_downloaded: false,
version_id: versionId,
device_type: device,
};
showStatus('pending', '다운로드 요청이 접수되었습니다. 관리자 승인 후 다운로드 가능합니다.');
btn.textContent = '승인 대기중';
btn.className = 'apkd-dl-btn action-pending';
btn.disabled = true;
} else {
alert(data.message || '요청에 실패했습니다.');
btn.disabled = false;
updateMainBtn();
}
} catch (e) {
alert('오류가 발생했습니다: ' + e.message);
btn.disabled = false;
updateMainBtn();
}
}
async function executeDownload(requestId, versionNumber) {
const btn = document.getElementById('main-dl-btn');
btn.disabled = true;
btn.textContent = '다운로드 중...';
try {
const url = API_BASE + '/vr-download-execute.php?request_id=' + requestId + '&user_id=' + USER_ID;
const resp = await fetch(url, {
headers: { 'Authorization': 'Bearer ' + JWT_TOKEN },
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({ message: '다운로드 실패' }));
throw new Error(err.message);
}
const blob = await resp.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = 'NugunaVR_' + versionNumber + '_' + selectedDevice + '.apk';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
// Update state
const key = document.getElementById('version-select').value + '_' + selectedDevice;
if (requestMap[key]) requestMap[key].is_downloaded = true;
showStatus('done', '다운로드가 완료되었습니다!');
btn.textContent = '다운로드 완료';
btn.className = 'apkd-dl-btn action-done';
btn.disabled = false;
} catch (e) {
alert('다운로드 오류: ' + e.message);
btn.disabled = false;
updateMainBtn();
}
}
async function handleHistoryDownload(btn) {
const versionId = parseInt(btn.dataset.versionId);
const device = btn.dataset.device;
const requestId = parseInt(btn.dataset.requestId);
const key = versionId + '_' + device;
const req = requestMap[key];
if (req && req.status === 'approved' && !req.is_downloaded) {
btn.disabled = true;
btn.textContent = '다운로드 중...';
try {
const url = API_BASE + '/vr-download-execute.php?request_id=' + req.id + '&user_id=' + USER_ID;
const resp = await fetch(url, { headers: { 'Authorization': 'Bearer ' + JWT_TOKEN } });
if (!resp.ok) {
const err = await resp.json().catch(() => ({ message: '실패' }));
throw new Error(err.message);
}
const blob = await resp.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = 'NugunaVR_' + device + '.apk';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
requestMap[key].is_downloaded = true;
btn.className = 'apkd-history-dl-btn done';
btn.textContent = '완료';
} catch (e) {
alert('오류: ' + e.message);
btn.disabled = false;
}
} else {
// Request download
btn.disabled = true;
btn.textContent = '요청 중...';
try {
const fd = new FormData();
fd.append('user_id', USER_ID);
fd.append('content_id', CONTENT_ID);
fd.append('version_id', versionId);
fd.append('device_type', device);
const resp = await fetch(API_BASE + '/vr-download-request.php', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + JWT_TOKEN },
body: fd,
});
const data = await resp.json();
if (data.status === 'success') {
requestMap[key] = {
id: data.data.request_id,
status: 'pending',
is_downloaded: false,
};
btn.id = 'hist-' + versionId + '-' + device;
btn.dataset.requestId = data.data.request_id;
btn.className = 'apkd-history-dl-btn wait';
btn.textContent = '대기중';
} else {
alert(data.message || '요청 실패');
btn.disabled = false;
btn.textContent = '요청하기';
}
} catch (e) {
alert('오류: ' + e.message);
btn.disabled = false;
btn.textContent = '요청하기';
}
}
}
function toggleHistory(toggleBtn) {
const body = toggleBtn.nextElementSibling;
const isOpen = body.classList.contains('open');
body.classList.toggle('open', !isOpen);
toggleBtn.classList.toggle('open', !isOpen);
toggleBtn.setAttribute('aria-expanded', !isOpen);
}
// Init
document.addEventListener('DOMContentLoaded', () => {
updateMainBtn();
// Open first history item by default
const firstToggle = document.querySelector('.apkd-history-toggle');
if (firstToggle) toggleHistory(firstToggle);
});
</script>
<?php
// Close #zak-content opened by zakra_main_start in header.php
echo '</div><!-- /#zak-content -->';
get_footer();