feat(desktop): PC·업데이트 팝업 Apple/Windows 브랜드 아이콘

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-06-15 17:03:07 +09:00
parent a5e14ba676
commit 0768ec37b9
5 changed files with 130 additions and 9 deletions
+13
View File
@@ -5,6 +5,8 @@ import {
type DesktopUpdateProgress, type DesktopUpdateProgress,
} from './bridge/updater'; } from './bridge/updater';
import { scheduleMainWindowTitleSync } from './setMainWindowTitle'; import { scheduleMainWindowTitleSync } from './setMainWindowTitle';
import { PlatformBrandIcon, type PlatformBrandIconId } from '@frontend/components/icons/PlatformBrandIcons';
import { isMacDesktop, isWindowsDesktop } from '@frontend/utils/platform';
import '@frontend/App.css'; import '@frontend/App.css';
const PHASE_LABEL: Record<DesktopUpdatePhase, string> = { const PHASE_LABEL: Record<DesktopUpdatePhase, string> = {
@@ -35,9 +37,20 @@ function StartupUpdateSplash({ progress }: { progress: DesktopUpdateProgress | n
? 'desktop-update-progress--active' ? 'desktop-update-progress--active'
: ''; : '';
const platformBrand: PlatformBrandIconId | null = isMacDesktop()
? 'mac'
: isWindowsDesktop()
? 'windows'
: null;
return ( return (
<div className="desktop-startup-update"> <div className="desktop-startup-update">
<div className="desktop-startup-update-card"> <div className="desktop-startup-update-card">
{platformBrand && (
<div className="desktop-update-platform-brand desktop-update-platform-brand--startup">
<PlatformBrandIcon platform={platformBrand} />
</div>
)}
<h1 className="desktop-startup-update-title">GoldenChart</h1> <h1 className="desktop-startup-update-title">GoldenChart</h1>
<p className="desktop-startup-update-sub"> <p className="desktop-startup-update-sub">
{progress?.currentVersion ? `현재 v${progress.currentVersion}` : '시작 중…'} {progress?.currentVersion ? `현재 v${progress.currentVersion}` : '시작 중…'}
+31 -1
View File
@@ -13429,6 +13429,14 @@ html.theme-light .tam-disclaimer { color: #90a4ae; }
margin: 8px 0 0; font-size: 12px; color: #4ade80; line-height: 1.45; margin: 8px 0 0; font-size: 12px; color: #4ade80; line-height: 1.45;
} }
.desktop-update-modal-body { padding: 4px 2px 0; } .desktop-update-modal-body { padding: 4px 2px 0; }
.desktop-update-platform-brand {
display: flex;
justify-content: center;
margin-bottom: 12px;
}
.desktop-update-platform-brand--startup {
margin-bottom: 10px;
}
.desktop-update-version { .desktop-update-version {
margin: 0 0 12px; font-size: 13px; color: var(--text2); margin: 0 0 12px; font-size: 13px; color: var(--text2);
} }
@@ -13598,7 +13606,29 @@ html.theme-light .tam-disclaimer { color: #90a4ae; }
background: var(--bg2); border: 1px solid var(--border); background: var(--bg2); border: 1px solid var(--border);
} }
.app-download-pc-card--empty { opacity: 0.75; } .app-download-pc-card--empty { opacity: 0.75; }
.app-download-pc-icon { font-size: 28px; flex-shrink: 0; width: 36px; text-align: center; } .app-download-pc-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 10px;
box-sizing: border-box;
}
.app-download-pc-icon--mac {
background: #1d1d1f;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.app-download-pc-icon--windows {
background: #0078d4;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
.app-download-pc-icon--android {
font-size: 24px;
background: var(--bg3);
border: 1px solid var(--border);
}
.app-download-pc-body { flex: 1; min-width: 0; } .app-download-pc-body { flex: 1; min-width: 0; }
.app-download-pc-body strong { display: block; font-size: 14px; margin-bottom: 4px; } .app-download-pc-body strong { display: block; font-size: 14px; margin-bottom: 4px; }
.app-download-pc-btn { .app-download-pc-btn {
+10 -8
View File
@@ -4,6 +4,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import DraggableModalFrame from './DraggableModalFrame'; import DraggableModalFrame from './DraggableModalFrame';
import { PlatformBrandIcon } from './icons/PlatformBrandIcons';
import { import {
loadDesktopAppBuildReadiness, loadDesktopAppBuildReadiness,
loadDesktopAppBuildStatus, loadDesktopAppBuildStatus,
@@ -161,17 +162,18 @@ const PlatformCard: React.FC<{
mobile?: MobileAppReleaseInfoDto | null; mobile?: MobileAppReleaseInfoDto | null;
}> = ({ platform, release, mobile }) => { }> = ({ platform, release, mobile }) => {
const configs = { const configs = {
mac: { label: 'macOS (Apple Silicon · Intel)', icon: '🍎', ext: '.dmg', isLink: false }, mac: { label: 'macOS (Apple Silicon · Intel)', ext: '.dmg', isLink: false },
windows: { label: 'Windows (64-bit)', icon: '🪟', ext: '.exe', isLink: false }, windows: { label: 'Windows (64-bit)', ext: '.exe', isLink: false },
android: { label: 'Android (패드 · 폰 · 별도 버전)', icon: '🤖', ext: '.apk', isLink: false }, android: { label: 'Android (패드 · 폰 · 별도 버전)', ext: '.apk', isLink: false },
} as const; } as const;
const { label, icon, ext } = configs[platform]; const { label, ext } = configs[platform];
const brandIcon = <PlatformBrandIcon platform={platform} />;
if (platform === 'android') { if (platform === 'android') {
if (!mobile?.available || !mobile.downloadUrl) { if (!mobile?.available || !mobile.downloadUrl) {
return ( return (
<div className="app-download-pc-card app-download-pc-card--empty"> <div className="app-download-pc-card app-download-pc-card--empty">
<div className="app-download-pc-icon" aria-hidden>{icon}</div> {brandIcon}
<div> <div>
<strong>{label}</strong> <strong>{label}</strong>
<p className="app-download-muted"> ({ext})</p> <p className="app-download-muted"> ({ext})</p>
@@ -181,7 +183,7 @@ const PlatformCard: React.FC<{
} }
return ( return (
<div className="app-download-pc-card"> <div className="app-download-pc-card">
<div className="app-download-pc-icon" aria-hidden>{icon}</div> {brandIcon}
<div className="app-download-pc-body"> <div className="app-download-pc-body">
<strong>{label}</strong> <strong>{label}</strong>
<p className="app-download-meta"> <p className="app-download-meta">
@@ -207,7 +209,7 @@ const PlatformCard: React.FC<{
if (!release?.available || !release.downloadUrl) { if (!release?.available || !release.downloadUrl) {
return ( return (
<div className="app-download-pc-card app-download-pc-card--empty"> <div className="app-download-pc-card app-download-pc-card--empty">
<div className="app-download-pc-icon" aria-hidden>{icon}</div> {brandIcon}
<div> <div>
<strong>{label}</strong> <strong>{label}</strong>
<p className="app-download-muted"> ({ext})</p> <p className="app-download-muted"> ({ext})</p>
@@ -218,7 +220,7 @@ const PlatformCard: React.FC<{
return ( return (
<div className="app-download-pc-card"> <div className="app-download-pc-card">
<div className="app-download-pc-icon" aria-hidden>{icon}</div> {brandIcon}
<div className="app-download-pc-body"> <div className="app-download-pc-body">
<strong>{label}</strong> <strong>{label}</strong>
<p className="app-download-meta"> <p className="app-download-meta">
@@ -10,6 +10,7 @@ import {
type DesktopUpdateProgress, type DesktopUpdateProgress,
} from '../utils/desktopBridge'; } from '../utils/desktopBridge';
import { isMacDesktop, isWindowsDesktop } from '../utils/platform'; import { isMacDesktop, isWindowsDesktop } from '../utils/platform';
import { PlatformBrandIcon, type PlatformBrandIconId } from './icons/PlatformBrandIcons';
interface Props { interface Props {
open: boolean; open: boolean;
@@ -84,6 +85,12 @@ const DesktopUpdateModal: React.FC<Props> = ({ open, onClose, autoStart = true }
? 100 ? 100
: progress?.downloadPercent; : progress?.downloadPercent;
const platformBrand: PlatformBrandIconId | null = isMacDesktop()
? 'mac'
: isWindowsDesktop()
? 'windows'
: null;
return ( return (
<DraggableModalFrame <DraggableModalFrame
title="PC 프로그램 업데이트" title="PC 프로그램 업데이트"
@@ -93,6 +100,11 @@ const DesktopUpdateModal: React.FC<Props> = ({ open, onClose, autoStart = true }
dialogClassName="sp-modal desktop-update-modal" dialogClassName="sp-modal desktop-update-modal"
> >
<div className="desktop-update-modal-body"> <div className="desktop-update-modal-body">
{platformBrand && (
<div className="desktop-update-platform-brand">
<PlatformBrandIcon platform={platformBrand} />
</div>
)}
<p className="desktop-update-version"> <p className="desktop-update-version">
<strong>v{progress?.currentVersion ?? appVersion}</strong> <strong>v{progress?.currentVersion ?? appVersion}</strong>
{progress?.newVersion && progress.newVersion !== progress.currentVersion && ( {progress?.newVersion && progress.newVersion !== progress.currentVersion && (
@@ -0,0 +1,64 @@
import React from 'react';
/** Apple 실루엣 로고 — 흰색 (다크 배경용) */
export const AppleLogoWhite: React.FC<{ size?: number; className?: string }> = ({
size = 22,
className,
}) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="#ffffff"
className={className}
aria-hidden
>
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
</svg>
);
/** Microsoft Windows 4-pane 로고 */
export const WindowsLogo: React.FC<{ size?: number; className?: string }> = ({
size = 22,
className,
}) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
className={className}
aria-hidden
>
<path fill="#f25022" d="M3 3h8.5v8.5H3V3z" />
<path fill="#7fba00" d="M12.5 3H21v8.5h-8.5V3z" />
<path fill="#00a4ef" d="M3 12.5h8.5V21H3v-8.5z" />
<path fill="#ffb900" d="M12.5 12.5H21V21h-8.5v-8.5z" />
</svg>
);
export type PlatformBrandIconId = 'mac' | 'windows' | 'android';
export const PlatformBrandIcon: React.FC<{
platform: PlatformBrandIconId;
className?: string;
}> = ({ platform, className }) => {
if (platform === 'mac') {
return (
<span className={`app-download-pc-icon app-download-pc-icon--mac${className ? ` ${className}` : ''}`}>
<AppleLogoWhite size={22} />
</span>
);
}
if (platform === 'windows') {
return (
<span className={`app-download-pc-icon app-download-pc-icon--windows${className ? ` ${className}` : ''}`}>
<WindowsLogo size={22} />
</span>
);
}
return (
<span className={`app-download-pc-icon app-download-pc-icon--android${className ? ` ${className}` : ''}`} aria-hidden>
🤖
</span>
);
};