feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped

This commit is contained in:
Yun Chan 2026-08-20 11:12:05 +09:00
parent 5cd1de6859
commit 708e20f747
406 changed files with 42464 additions and 6199 deletions

View file

@ -1,12 +1,11 @@
// src/renderer/components/LicenseTab.tsx
// 구독 기반 라이선스 탭 — 클라우드 인증 + Payple 결제 연동
import { useState } from 'react'
import {
Box,
Divider,
LinearProgress,
TextField,
} from '@mui/material'
import { CircleCheck, XCircle } from 'lucide-react'
import { CircleCheck, XCircle, KeyRound, Sparkles } from 'lucide-react'
import { Led, PhosphorText, PhysicalButton, MetalCard } from '@d3ro/ui/components/ds'
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
import { useI18n } from '@d3ro/i18n'
@ -45,6 +44,23 @@ export function LicenseTab(): React.ReactElement {
</Box>
</MetalCard>
{/* ── 14일 Reverse-Trial 상태 배너 (활성화 시) ── */}
{licenseInfo?.isTrial && licenseInfo.trialExpiresAt && (
<MetalCard sx={{ bgcolor: 'rgba(255, 170, 0, 0.08)', borderColor: d3roPalette.accent.main }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Sparkles size={18} color={d3roPalette.accent.main} />
<Box sx={{ flex: 1 }}>
<PhosphorText variant="compact" sx={{ color: d3roPalette.accent.main, fontWeight: 'bold' }}>
14-Day Reverse Trial (Pro+)
</PhosphorText>
<PhosphorText variant="small" sx={{ color: d3roPalette.text.secondary }}>
{Math.max(0, Math.ceil((licenseInfo.trialExpiresAt - Date.now()) / (24 * 60 * 60 * 1000)))} 100% .
</PhosphorText>
</Box>
</Box>
</MetalCard>
)}
{/* ── 구독 관리 / 업그레이드 ── */}
<MetalCard>
<SubscriptionSection
@ -59,6 +75,11 @@ export function LicenseTab(): React.ReactElement {
/>
</MetalCard>
{/* ── 오프라인 라이선스 키 등록 ── */}
<MetalCard>
<LicenseKeySection t={t} currentTier={currentTier} />
</MetalCard>
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
{/* ── 일일 사용량 ── */}
@ -73,15 +94,115 @@ export function LicenseTab(): React.ReactElement {
// ── 하위 컴포넌트 ──────────────────────────────────────
function TierLabel({ tier, t }: { tier: string; t: (k: string) => string }): React.ReactElement {
const color = tier === 'pro_plus'
? d3roPalette.tag.purple
: tier === 'pro'
? d3roPalette.tag.green
: d3roPalette.accent.main
const label = tier === 'pro_plus' ? t('license.proPlus') : tier === 'pro' ? t('license.pro') : t('license.free')
const color = tier === 'enterprise' || tier === 'team'
? d3roPalette.accent.main
: tier === 'pro_plus'
? d3roPalette.tag.purple
: tier === 'pro'
? d3roPalette.tag.green
: d3roPalette.accent.main
const label = tier === 'enterprise'
? 'Enterprise'
: tier === 'team'
? 'Team'
: tier === 'pro_plus'
? t('license.proPlus')
: tier === 'pro'
? t('license.pro')
: t('license.free')
return <PhosphorText variant="value" sx={{ color }}>{label}</PhosphorText>
}
function LicenseKeySection({ t, currentTier }: { t: (k: string) => string; currentTier: string }): React.ReactElement {
const [licenseKeyInput, setLicenseKeyInput] = useState('')
const [feedback, setFeedback] = useState<{ success: boolean; message: string } | null>(null)
const [loading, setLoading] = useState(false)
const handleActivateKey = async () => {
if (!licenseKeyInput.trim()) return
setLoading(true)
setFeedback(null)
try {
const res = await window.electronAPI.license.activate({ licenseKey: licenseKeyInput.trim() })
if (res.success && res.data.success) {
setFeedback({ success: true, message: res.data.message || '라이센스가 성공적으로 활성화되었습니다.' })
setLicenseKeyInput('')
} else {
setFeedback({ success: false, message: (res.success && res.data.message) ? res.data.message : '유효하지 않은 라이센스 키입니다.' })
}
} catch (err) {
setFeedback({ success: false, message: `활성화 실패: ${err}` })
} finally {
setLoading(false)
}
}
const handleDeactivate = async () => {
setLoading(true)
try {
await window.electronAPI.license.deactivate()
setFeedback({ success: true, message: '라이센스가 비활성화되어 Free 티어로 전환되었습니다.' })
} finally {
setLoading(false)
}
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<KeyRound size={16} color={d3roPalette.text.label} />
<PhosphorText variant="label">
(Ed25519)
</PhosphorText>
</Box>
<PhosphorText variant="dim" sx={{ fontSize: d3roTypo.small.size }}>
(D3RO-LIC-... D3RO-PRO-...) .
</PhosphorText>
<Box sx={{ display: 'flex', gap: 1 }}>
<TextField
size="small"
fullWidth
placeholder="D3RO-LIC-..."
value={licenseKeyInput}
onChange={(e) => setLicenseKeyInput(e.target.value)}
disabled={loading}
sx={{
fontFamily: d3roFontMono,
'& .MuiOutlinedInput-root': {
bgcolor: d3roPalette.bg.inset,
borderRadius: d3roRadius.button,
fontSize: d3roTypo.small.size,
fontFamily: d3roFontMono,
color: d3roPalette.text.primary,
'& fieldset': { borderColor: d3roPalette.border.subtle },
'&:hover fieldset': { borderColor: d3roPalette.accent.main },
},
}}
/>
<PhysicalButton onClick={handleActivateKey} disabled={loading || !licenseKeyInput.trim()} sx={{ px: 2.5, whiteSpace: 'nowrap' }}>
<PhosphorText variant="compact"></PhosphorText>
</PhysicalButton>
{currentTier !== 'free' && (
<PhysicalButton onClick={handleDeactivate} disabled={loading} sx={{ px: 2, whiteSpace: 'nowrap' }}>
<PhosphorText variant="compact" sx={{ color: d3roPalette.tag.red }}></PhosphorText>
</PhysicalButton>
)}
</Box>
{feedback && (
<PhosphorText
variant="small"
sx={{ color: feedback.success ? d3roPalette.tag.green : d3roPalette.tag.red }}
>
{feedback.message}
</PhosphorText>
)}
</Box>
)
}
interface SubscriptionSectionProps {
t: (k: string) => string
authenticated: boolean
@ -134,6 +255,28 @@ function SubscriptionSection({ t, authenticated, isFree, isPro, onUpgrade, onMan
)
}
if (currentTier === 'team') {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<ActivePlanIndicator label="Team Workspace Plan Active" color={d3roPalette.accent.main} />
<PhysicalButton onClick={onManage} sx={{ width: '100%' }}>
<PhosphorText variant="compact">{t('license.subscriptionManage')}</PhosphorText>
</PhysicalButton>
</Box>
)
}
if (currentTier === 'enterprise') {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<ActivePlanIndicator label="Enterprise Dedicated Plan Active" color={d3roPalette.accent.main} />
<PhysicalButton onClick={onManage} sx={{ width: '100%' }}>
<PhosphorText variant="compact">{t('license.subscriptionManage')}</PhosphorText>
</PhysicalButton>
</Box>
)
}
// Pro+
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>