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
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:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
|
|
@ -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 }}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue