Files
goldenChart/frontend/src/components/AppDownloadModal.tsx
T
2026-05-28 14:44:19 +09:00

152 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 모바일 앱 설치 — APK 다운로드 URL·QR 코드
*/
import React, { useEffect, useMemo, useState } from 'react';
import QRCode from 'qrcode';
import DraggableModalFrame from './DraggableModalFrame';
import { loadMobileAppReleaseInfo, type MobileAppReleaseInfoDto } from '../utils/backendApi';
interface Props {
onClose: () => void;
}
function formatBytes(n?: number): string {
if (n == null || !Number.isFinite(n)) return '—';
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}
/** APK·QR은 항상 서버가 내려준 절대 URL 사용 (exdev.co.kr, localhost 아님) */
function resolveDownloadUrl(info: MobileAppReleaseInfoDto | null): string {
if (!info?.available) return '';
return info.downloadUrl?.trim() ?? '';
}
const AppDownloadModal: React.FC<Props> = ({ onClose }) => {
const [info, setInfo] = useState<MobileAppReleaseInfoDto | null>(null);
const [loading, setLoading] = useState(true);
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const downloadUrl = useMemo(() => resolveDownloadUrl(info), [info]);
useEffect(() => {
let cancelled = false;
void loadMobileAppReleaseInfo()
.then(data => {
if (!cancelled) setInfo(data);
})
.catch(() => {
if (!cancelled) setError('앱 정보를 불러오지 못했습니다.');
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
}, []);
useEffect(() => {
if (!downloadUrl) {
setQrDataUrl(null);
return;
}
let cancelled = false;
void QRCode.toDataURL(downloadUrl, {
width: 220,
margin: 2,
color: { dark: '#111827', light: '#ffffff' },
}).then(url => {
if (!cancelled) setQrDataUrl(url);
}).catch(() => {
if (!cancelled) setQrDataUrl(null);
});
return () => { cancelled = true; };
}, [downloadUrl]);
const handleDownload = () => {
if (!downloadUrl) return;
const a = document.createElement('a');
a.href = downloadUrl;
a.download = info?.fileName ?? 'goldenchart-android.apk';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
a.remove();
};
return (
<DraggableModalFrame
onClose={onClose}
title="Mobile App"
titleKo="GoldenChart 앱 설치"
badge="Android"
width={440}
dialogClassName="sp-modal app-download-modal"
>
<div className="app-download-body">
{loading && <p className="app-download-muted"> 정보 불러오는 중…</p>}
{!loading && error && <p className="app-download-error">{error}</p>}
{!loading && !error && !info?.available && (
<div className="app-download-empty">
<div className="app-download-empty-icon" aria-hidden>📱</div>
<h3>설치 파일 준비 </h3>
<p>
Android APK가 아직 서버에 업로드되지 않았습니다.
<br />
<code>data/mobile-releases/goldenchart-android.apk</code> 경로에 빌드 파일을 배치하세요.
</p>
</div>
)}
{!loading && info?.available && (
<>
<div className="app-download-hero">
<div className="app-download-app-icon" aria-hidden>GC</div>
<div>
<h3 className="app-download-app-name">GoldenChart</h3>
<p className="app-download-meta">
{info.version && <span>v{info.version}</span>}
{info.sizeBytes != null && <span>{formatBytes(info.sizeBytes)}</span>}
{info.updatedAt && <span>{info.updatedAt}</span>}
</p>
</div>
</div>
<div className="app-download-qr-block">
{qrDataUrl ? (
<img src={qrDataUrl} alt="앱 설치 QR 코드" className="app-download-qr" width={220} height={220} />
) : (
<div className="app-download-qr app-download-qr--placeholder">QR 생성 중…</div>
)}
<div className="app-download-qr-help">
<strong>휴대폰 카메라로 QR 스캔</strong>
<p>Android에서 APK 다운로드 페이지가 열립니다. 다운로드 설치하세요.</p>
<p className="app-download-muted app-download-url" title={downloadUrl}>{downloadUrl}</p>
</div>
</div>
<button type="button" className="app-download-btn-primary" onClick={handleDownload}>
Android APK 다운로드
</button>
<details className="app-download-steps">
<summary>설치 방법 (Android)</summary>
<ol>
<li>QR 코드를 스캔하거나 버튼으로 APK를 다운로드합니다.</li>
<li>「출처를 없는 앱」 설치 허용이 필요할 있습니다.</li>
<li>다운로드 완료 APK 파일을 탭하여 설치합니다.</li>
<li>설치 앱에서 API 서버 URL을 설정하세요 (설정 네트워크).</li>
</ol>
</details>
<p className="app-download-ios-note">
iOS는 App Store / TestFlight 배포가 필요합니다. 현재 QR·APK 설치는 Android 전용입니다.
</p>
</>
)}
</div>
</DraggableModalFrame>
);
};
export default AppDownloadModal;