feat(mobile): Phase M-1 모바일 프로젝트 기반 + V3 마스터 플랜
- V3 모바일 마스터 플랜 작성 (docs/v3/00-mobile-master-plan.md) 11개 Phase, 5주 로드맵, 전략 결정사항 확정 - 7개 디자인 HTML 레퍼런스 저장 (docs/v3/designs/) - app.json → app.config.ts 전환 + 환경변수 체계 - 5탭 네비게이션 (DASH/HIST/REC FAB/TALK/SET) - metro.config.js monorepo 격리 (root RN 0.84 충돌 해결) - ui-native 테마 디자인 목업 색상 동기화 - SafeArea 적용 (useSafeAreaInsets) - DEV 로그인 우회 (__DEV__ 전용) - Supabase .env 연동 - iOS 시뮬레이터 검증 완료
This commit is contained in:
parent
ffca07d120
commit
211673bc6c
30 changed files with 15010 additions and 525 deletions
22
apps/mobile/.gitignore
vendored
Normal file
22
apps/mobile/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Expo
|
||||||
|
.expo/
|
||||||
|
dist/
|
||||||
|
expo-env.d.ts
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Build
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
64
apps/mobile/app.config.ts
Normal file
64
apps/mobile/app.config.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
// apps/mobile/app.config.ts
|
||||||
|
// Expo 설정 — 환경변수 관리, 플러그인, 권한
|
||||||
|
|
||||||
|
import { type ExpoConfig, type ConfigContext } from 'expo/config'
|
||||||
|
|
||||||
|
export default ({ config }: ConfigContext): ExpoConfig => ({
|
||||||
|
...config,
|
||||||
|
name: 'D3RO Voice',
|
||||||
|
slug: 'd3ro-voice',
|
||||||
|
scheme: 'd3ro-voice',
|
||||||
|
version: '1.0.0',
|
||||||
|
orientation: 'portrait',
|
||||||
|
icon: './assets/icon.png',
|
||||||
|
userInterfaceStyle: 'dark',
|
||||||
|
splash: {
|
||||||
|
image: './assets/splash.png',
|
||||||
|
resizeMode: 'contain',
|
||||||
|
backgroundColor: '#19191b'
|
||||||
|
},
|
||||||
|
ios: {
|
||||||
|
supportsTablet: true,
|
||||||
|
bundleIdentifier: 'com.d3ro.voice',
|
||||||
|
infoPlist: {
|
||||||
|
NSMicrophoneUsageDescription:
|
||||||
|
'D3RO Voice uses the microphone for voice recognition and recording.',
|
||||||
|
UIBackgroundModes: ['audio', 'fetch', 'remote-notification']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
package: 'com.d3ro.voice',
|
||||||
|
permissions: [
|
||||||
|
'RECORD_AUDIO',
|
||||||
|
'FOREGROUND_SERVICE',
|
||||||
|
'FOREGROUND_SERVICE_MICROPHONE',
|
||||||
|
'POST_NOTIFICATIONS'
|
||||||
|
],
|
||||||
|
adaptiveIcon: {
|
||||||
|
foregroundImage: './assets/adaptive-icon.png',
|
||||||
|
backgroundColor: '#19191b'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
'expo-router',
|
||||||
|
'expo-secure-store',
|
||||||
|
'expo-av',
|
||||||
|
[
|
||||||
|
'expo-notifications',
|
||||||
|
{
|
||||||
|
icon: './assets/icon.png',
|
||||||
|
color: '#ff5c35'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
experiments: {
|
||||||
|
typedRoutes: true
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
supabaseUrl: process.env.EXPO_PUBLIC_SUPABASE_URL ?? '',
|
||||||
|
supabaseAnonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '',
|
||||||
|
eas: {
|
||||||
|
projectId: process.env.EAS_PROJECT_ID ?? ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,10 +1,26 @@
|
||||||
// apps/mobile/app/(tabs)/_layout.tsx
|
// apps/mobile/app/(tabs)/_layout.tsx
|
||||||
// 탭 네비게이션 — Meetings / Record / Profile
|
// 5탭 네비게이션: DASH / HIST / REC(FAB) / TALK / SET
|
||||||
|
|
||||||
import { Tabs } from 'expo-router'
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useRouter } from 'expo-router'
|
import { View, StyleSheet, Pressable, Platform } from 'react-native'
|
||||||
|
import { Tabs, useRouter } from 'expo-router'
|
||||||
import { useAuth } from '../../lib/auth-context'
|
import { useAuth } from '../../lib/auth-context'
|
||||||
|
import { d3roNativePalette } from '@d3ro/ui-native'
|
||||||
|
|
||||||
|
const MONO_FONT = Platform.OS === 'ios' ? 'Menlo' : 'monospace'
|
||||||
|
|
||||||
|
function RecordFAB({ onPress }: { onPress: () => void }): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Pressable onPress={onPress} style={styles.fab}>
|
||||||
|
<View style={styles.fabInner}>
|
||||||
|
<View style={styles.micIcon}>
|
||||||
|
<View style={styles.micBody} />
|
||||||
|
<View style={styles.micBase} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function TabsLayout(): React.ReactElement {
|
export default function TabsLayout(): React.ReactElement {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -19,17 +35,169 @@ export default function TabsLayout(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
tabBarStyle: { backgroundColor: '#19191b', borderTopColor: '#2a2a2d' },
|
tabBarStyle: {
|
||||||
tabBarActiveTintColor: '#f25b29',
|
backgroundColor: 'rgba(25, 25, 27, 0.95)',
|
||||||
tabBarInactiveTintColor: '#8e8e93',
|
borderTopColor: d3roNativePalette.border.default,
|
||||||
headerStyle: { backgroundColor: '#19191b' },
|
borderTopWidth: 1,
|
||||||
headerTintColor: '#f25b29',
|
height: 80,
|
||||||
headerTitleStyle: { fontWeight: '300', letterSpacing: 1 }
|
paddingBottom: 20,
|
||||||
|
paddingTop: 8
|
||||||
|
},
|
||||||
|
tabBarActiveTintColor: d3roNativePalette.accent.amber,
|
||||||
|
tabBarInactiveTintColor: d3roNativePalette.text.muted,
|
||||||
|
tabBarLabelStyle: {
|
||||||
|
fontFamily: MONO_FONT,
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: '500',
|
||||||
|
letterSpacing: 0.5
|
||||||
|
},
|
||||||
|
headerStyle: { backgroundColor: d3roNativePalette.bg.app },
|
||||||
|
headerTintColor: d3roNativePalette.accent.amber,
|
||||||
|
headerTitleStyle: { fontWeight: '300', letterSpacing: 1, fontFamily: MONO_FONT },
|
||||||
|
headerShown: false
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs.Screen name="meetings" options={{ title: 'Meetings' }} />
|
<Tabs.Screen
|
||||||
<Tabs.Screen name="record" options={{ title: 'Record' }} />
|
name="dash"
|
||||||
<Tabs.Screen name="profile" options={{ title: 'Profile' }} />
|
options={{
|
||||||
|
title: 'DASH',
|
||||||
|
tabBarIcon: ({ color }: { color: string }) => <TabIcon type="dash" color={color} />
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="history"
|
||||||
|
options={{
|
||||||
|
title: 'HIST',
|
||||||
|
tabBarIcon: ({ color }: { color: string }) => <TabIcon type="hist" color={color} />
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="record"
|
||||||
|
options={{
|
||||||
|
title: '',
|
||||||
|
tabBarIcon: () => <RecordFAB onPress={() => router.push('/(tabs)/record')} />,
|
||||||
|
tabBarLabel: () => null
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="talk"
|
||||||
|
options={{
|
||||||
|
title: 'TALK',
|
||||||
|
tabBarIcon: ({ color }: { color: string }) => <TabIcon type="talk" color={color} />
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="settings"
|
||||||
|
options={{
|
||||||
|
title: 'SET',
|
||||||
|
tabBarIcon: ({ color }: { color: string }) => <TabIcon type="set" color={color} />
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 단순 아이콘 대용 (Phase M-2에서 SVG 아이콘으로 교체)
|
||||||
|
function TabIcon({ type, color }: { type: string; color: string }): React.ReactElement {
|
||||||
|
const iconStyles: Record<string, React.ReactElement> = {
|
||||||
|
dash: (
|
||||||
|
<View style={[styles.iconGrid, { borderColor: color }]}>
|
||||||
|
{[0, 1, 2, 3].map((i) => (
|
||||||
|
<View key={i} style={[styles.iconGridCell, { borderColor: color }]} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
hist: (
|
||||||
|
<View style={[styles.iconCircle, { borderColor: color }]}>
|
||||||
|
<View style={[styles.iconClockHand, { backgroundColor: color }]} />
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
talk: (
|
||||||
|
<View style={[styles.iconChat, { borderColor: color }]} />
|
||||||
|
),
|
||||||
|
set: (
|
||||||
|
<View style={[styles.iconGear, { borderColor: color }]} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return iconStyles[type] ?? <View />
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
fab: {
|
||||||
|
position: 'relative',
|
||||||
|
top: -20,
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 28,
|
||||||
|
backgroundColor: d3roNativePalette.accent.amber,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
shadowColor: d3roNativePalette.accent.amber,
|
||||||
|
shadowOffset: { width: 0, height: 0 },
|
||||||
|
shadowOpacity: 0.4,
|
||||||
|
shadowRadius: 10,
|
||||||
|
elevation: 8,
|
||||||
|
borderWidth: 4,
|
||||||
|
borderColor: d3roNativePalette.bg.app
|
||||||
|
},
|
||||||
|
fabInner: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
micIcon: {
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
micBody: {
|
||||||
|
width: 8,
|
||||||
|
height: 14,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: d3roNativePalette.bg.app,
|
||||||
|
marginBottom: 2
|
||||||
|
},
|
||||||
|
micBase: {
|
||||||
|
width: 14,
|
||||||
|
height: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
backgroundColor: d3roNativePalette.bg.app
|
||||||
|
},
|
||||||
|
// Tab icons (placeholder — Phase M-2에서 SVG로 교체)
|
||||||
|
iconGrid: {
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 2
|
||||||
|
},
|
||||||
|
iconGridCell: {
|
||||||
|
width: 9,
|
||||||
|
height: 9,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderRadius: 2
|
||||||
|
},
|
||||||
|
iconCircle: {
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
borderRadius: 11,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
iconClockHand: {
|
||||||
|
width: 1.5,
|
||||||
|
height: 7,
|
||||||
|
position: 'absolute',
|
||||||
|
top: 3
|
||||||
|
},
|
||||||
|
iconChat: {
|
||||||
|
width: 22,
|
||||||
|
height: 18,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderRadius: 4
|
||||||
|
},
|
||||||
|
iconGear: {
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderRadius: 11
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
||||||
155
apps/mobile/app/(tabs)/dash.tsx
Normal file
155
apps/mobile/app/(tabs)/dash.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
// apps/mobile/app/(tabs)/dash.tsx
|
||||||
|
// 대시보드 탭 — 세션 통계, 사용량, 시스템 상태
|
||||||
|
// Phase M-2에서 디자인(docs/v3/designs/dashboard.html) 정밀 적용
|
||||||
|
|
||||||
|
import { View, ScrollView, StyleSheet } from 'react-native'
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import { MetalCard, PhosphorText, Led, d3roNativePalette } from '@d3ro/ui-native'
|
||||||
|
|
||||||
|
export default function DashScreen(): React.ReactElement {
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
style={styles.container}
|
||||||
|
contentContainerStyle={[styles.content, { paddingTop: insets.top + 8, paddingBottom: insets.bottom + 100 }]}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<View style={styles.headerLeft}>
|
||||||
|
<Led color="amber" size={8} />
|
||||||
|
<PhosphorText variant="label" color="muted" style={styles.headerLabel}>
|
||||||
|
D3RO-VOICE
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
<View style={styles.headerRight}>
|
||||||
|
<PhosphorText variant="label" color="muted">
|
||||||
|
v1.0.0
|
||||||
|
</PhosphorText>
|
||||||
|
<Led color="green" size={6} />
|
||||||
|
<Led color="amber" size={6} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Session Overview */}
|
||||||
|
<MetalCard inset style={styles.overviewCard}>
|
||||||
|
<View style={styles.overviewHeader}>
|
||||||
|
<PhosphorText variant="small" color="muted">
|
||||||
|
SESSION OVERVIEW
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
<View style={styles.bigNumber}>
|
||||||
|
<PhosphorText variant="hero" color="amber">
|
||||||
|
0
|
||||||
|
</PhosphorText>
|
||||||
|
<PhosphorText variant="body" color="muted" style={{ marginLeft: 12 }}>
|
||||||
|
TODAY
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
<View style={styles.overviewFooter}>
|
||||||
|
<View>
|
||||||
|
<PhosphorText variant="label" color="muted">WORDS</PhosphorText>
|
||||||
|
<PhosphorText variant="value" color="amber">0</PhosphorText>
|
||||||
|
</View>
|
||||||
|
<View style={{ alignItems: 'flex-end' }}>
|
||||||
|
<PhosphorText variant="label" color="muted">STREAK</PhosphorText>
|
||||||
|
<PhosphorText variant="value" color="amber">0</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</MetalCard>
|
||||||
|
|
||||||
|
{/* 2x2 Stats Grid */}
|
||||||
|
<View style={styles.statsGrid}>
|
||||||
|
<MetalCard style={styles.statCard}>
|
||||||
|
<PhosphorText variant="label" color="muted">REC</PhosphorText>
|
||||||
|
<PhosphorText variant="value" color="amber">0</PhosphorText>
|
||||||
|
<PhosphorText variant="label" color="muted">MIN</PhosphorText>
|
||||||
|
</MetalCard>
|
||||||
|
<MetalCard style={styles.statCard}>
|
||||||
|
<PhosphorText variant="label" color="muted">WORDS</PhosphorText>
|
||||||
|
<PhosphorText variant="value" color="amber">0</PhosphorText>
|
||||||
|
</MetalCard>
|
||||||
|
<MetalCard style={styles.statCard}>
|
||||||
|
<PhosphorText variant="label" color="muted">TODAY</PhosphorText>
|
||||||
|
<PhosphorText variant="value" color="amber">0</PhosphorText>
|
||||||
|
</MetalCard>
|
||||||
|
<MetalCard style={styles.statCard}>
|
||||||
|
<PhosphorText variant="label" color="muted">STREAK</PhosphorText>
|
||||||
|
<PhosphorText variant="value" color="amber">0</PhosphorText>
|
||||||
|
</MetalCard>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Backend Info */}
|
||||||
|
<MetalCard style={styles.infoCard}>
|
||||||
|
<PhosphorText variant="small" color="muted">BACKEND</PhosphorText>
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Led color="green" size={6} />
|
||||||
|
<PhosphorText variant="body" color="amber" style={{ marginLeft: 8 }}>
|
||||||
|
CLOUD (CLAUDE)
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</MetalCard>
|
||||||
|
|
||||||
|
<MetalCard style={styles.infoCard}>
|
||||||
|
<PhosphorText variant="small" color="muted">TIER</PhosphorText>
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Led color="amber" size={6} />
|
||||||
|
<PhosphorText variant="body" color="amber" style={{ marginLeft: 8 }}>
|
||||||
|
FREE
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</MetalCard>
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1, backgroundColor: d3roNativePalette.bg.app },
|
||||||
|
content: { padding: 20 },
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12
|
||||||
|
},
|
||||||
|
headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||||
|
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||||
|
headerLabel: { letterSpacing: 3 },
|
||||||
|
overviewCard: {
|
||||||
|
padding: 20,
|
||||||
|
marginBottom: 12
|
||||||
|
},
|
||||||
|
overviewHeader: { marginBottom: 16 },
|
||||||
|
bigNumber: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
marginBottom: 24
|
||||||
|
},
|
||||||
|
overviewFooter: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: 'rgba(255,255,255,0.04)',
|
||||||
|
paddingTop: 12
|
||||||
|
},
|
||||||
|
statsGrid: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 12,
|
||||||
|
marginBottom: 12
|
||||||
|
},
|
||||||
|
statCard: {
|
||||||
|
width: '47%',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 16,
|
||||||
|
gap: 4
|
||||||
|
},
|
||||||
|
infoCard: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
paddingVertical: 14
|
||||||
|
},
|
||||||
|
infoRow: { flexDirection: 'row', alignItems: 'center' }
|
||||||
|
})
|
||||||
208
apps/mobile/app/(tabs)/history.tsx
Normal file
208
apps/mobile/app/(tabs)/history.tsx
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
// apps/mobile/app/(tabs)/history.tsx
|
||||||
|
// 히스토리 탭 — 전사 기록 목록
|
||||||
|
// Phase M-2에서 디자인(docs/v3/designs/history.html) 정밀 적용
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { View, FlatList, StyleSheet, ActivityIndicator, RefreshControl, Pressable } from 'react-native'
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import { MetalCard, PhosphorText, Led, d3roNativePalette } from '@d3ro/ui-native'
|
||||||
|
import { supabase, isSupabaseConfigured } from '../../lib/supabase'
|
||||||
|
|
||||||
|
interface HistoryEntry {
|
||||||
|
id: string
|
||||||
|
original_text: string | null
|
||||||
|
polished_text: string | null
|
||||||
|
mode: string
|
||||||
|
status: string
|
||||||
|
created_at: string
|
||||||
|
stt_model: string | null
|
||||||
|
word_count: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterType = 'all' | 'favorites' | 'processing'
|
||||||
|
|
||||||
|
export default function HistoryScreen(): React.ReactElement {
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const [entries, setEntries] = useState<HistoryEntry[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [filter, setFilter] = useState<FilterType>('all')
|
||||||
|
|
||||||
|
async function load(): Promise<void> {
|
||||||
|
if (!isSupabaseConfigured()) {
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const query = supabase
|
||||||
|
.from('history')
|
||||||
|
.select('id, original_text, polished_text, mode, status, created_at, stt_model, word_count')
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(50)
|
||||||
|
|
||||||
|
const { data, error } = await query
|
||||||
|
if (!error && data) {
|
||||||
|
setEntries(data as HistoryEntry[])
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
setRefreshing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<View style={styles.center}>
|
||||||
|
<ActivityIndicator size="large" color={d3roNativePalette.accent.amber} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={[styles.header, { paddingTop: insets.top + 8 }]}>
|
||||||
|
<View style={styles.headerLeft}>
|
||||||
|
<Led color="amber" size={8} />
|
||||||
|
<PhosphorText variant="label" color="muted" style={{ letterSpacing: 3 }}>
|
||||||
|
HISTORY
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
<Led color="green" size={6} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Filter Chips */}
|
||||||
|
<View style={styles.filters}>
|
||||||
|
{(['all', 'favorites', 'processing'] as const).map((f) => (
|
||||||
|
<Pressable
|
||||||
|
key={f}
|
||||||
|
style={[styles.chip, filter === f && styles.chipActive]}
|
||||||
|
onPress={() => setFilter(f)}
|
||||||
|
>
|
||||||
|
<PhosphorText
|
||||||
|
variant="small"
|
||||||
|
color={filter === f ? 'amber' : 'muted'}
|
||||||
|
>
|
||||||
|
{f === 'all' ? 'ALL' : f === 'favorites' ? 'SAVED' : 'PROC'}
|
||||||
|
</PhosphorText>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
data={entries}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
contentContainerStyle={entries.length === 0 ? styles.center : styles.listContent}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={() => { setRefreshing(true); void load() }}
|
||||||
|
tintColor={d3roNativePalette.accent.amber}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<PhosphorText variant="body" color="muted" style={{ textAlign: 'center' }}>
|
||||||
|
No history yet. Start recording!
|
||||||
|
</PhosphorText>
|
||||||
|
}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<Pressable>
|
||||||
|
<MetalCard style={styles.card}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<View style={styles.cardHeaderLeft}>
|
||||||
|
<Led
|
||||||
|
color={item.status === 'completed' ? 'green' : 'amber'}
|
||||||
|
size={6}
|
||||||
|
/>
|
||||||
|
<PhosphorText variant="label" color="primary" style={{ marginLeft: 8 }}>
|
||||||
|
{new Date(item.created_at).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
{item.word_count != null && (
|
||||||
|
<View style={styles.wordBadge}>
|
||||||
|
<PhosphorText variant="label" color="amber">
|
||||||
|
{item.word_count} W
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<PhosphorText
|
||||||
|
variant="body"
|
||||||
|
color="primary"
|
||||||
|
style={styles.transcriptText}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
|
{item.polished_text ?? item.original_text ?? '(empty)'}
|
||||||
|
</PhosphorText>
|
||||||
|
<View style={styles.cardMeta}>
|
||||||
|
<PhosphorText variant="label" color="muted">
|
||||||
|
{item.stt_model?.toUpperCase() ?? 'LOCAL'}
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</MetalCard>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1, backgroundColor: d3roNativePalette.bg.app },
|
||||||
|
center: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 32,
|
||||||
|
backgroundColor: d3roNativePalette.bg.app
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 12
|
||||||
|
},
|
||||||
|
headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||||
|
filters: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 12,
|
||||||
|
gap: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: 'rgba(46, 46, 50, 0.5)'
|
||||||
|
},
|
||||||
|
chip: {
|
||||||
|
backgroundColor: d3roNativePalette.bg.card,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: d3roNativePalette.border.default,
|
||||||
|
borderRadius: 999,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 6
|
||||||
|
},
|
||||||
|
chipActive: {
|
||||||
|
backgroundColor: d3roNativePalette.accent.amber,
|
||||||
|
borderColor: d3roNativePalette.accent.amber
|
||||||
|
},
|
||||||
|
listContent: { padding: 20, paddingBottom: 120 },
|
||||||
|
card: { marginBottom: 12 },
|
||||||
|
cardHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8
|
||||||
|
},
|
||||||
|
cardHeaderLeft: { flexDirection: 'row', alignItems: 'center' },
|
||||||
|
wordBadge: {
|
||||||
|
backgroundColor: d3roNativePalette.accent.amberDim,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 4
|
||||||
|
},
|
||||||
|
transcriptText: { marginBottom: 8 },
|
||||||
|
cardMeta: { flexDirection: 'row', gap: 12 }
|
||||||
|
})
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
// apps/mobile/app/(tabs)/meetings.tsx
|
|
||||||
// 회의 리스트 — @d3ro/ui-native 사용
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { View, FlatList, StyleSheet, ActivityIndicator, RefreshControl, Pressable } from 'react-native'
|
|
||||||
import { MetalCard, PhosphorText, Led, d3roNativePalette } from '@d3ro/ui-native'
|
|
||||||
import { supabase } from '../../lib/supabase'
|
|
||||||
|
|
||||||
interface Meeting {
|
|
||||||
id: string
|
|
||||||
title: string | null
|
|
||||||
started_at: string
|
|
||||||
status: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusLedColor(status: string): 'amber' | 'green' | 'red' | 'orange' {
|
|
||||||
if (status === 'recording') return 'red'
|
|
||||||
if (status === 'processing') return 'orange'
|
|
||||||
if (status === 'completed') return 'green'
|
|
||||||
return 'amber'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MeetingsScreen(): React.ReactElement {
|
|
||||||
const [meetings, setMeetings] = useState<Meeting[]>([])
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('meetings')
|
|
||||||
.select('id, title, started_at, status')
|
|
||||||
.order('started_at', { ascending: false })
|
|
||||||
.limit(50)
|
|
||||||
|
|
||||||
if (!error && data) {
|
|
||||||
setMeetings(data as Meeting[])
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
setRefreshing(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void load()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<View style={styles.center}>
|
|
||||||
<ActivityIndicator size="large" color={d3roNativePalette.accent.amber} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
data={meetings}
|
|
||||||
keyExtractor={(item) => item.id}
|
|
||||||
contentContainerStyle={meetings.length === 0 ? styles.center : styles.listContent}
|
|
||||||
style={styles.list}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={() => {
|
|
||||||
setRefreshing(true)
|
|
||||||
void load()
|
|
||||||
}}
|
|
||||||
tintColor={d3roNativePalette.accent.amber}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
ListEmptyComponent={
|
|
||||||
<PhosphorText variant="small" color="muted" style={styles.empty}>
|
|
||||||
아직 회의가 없습니다. Record 탭에서 새 녹음을 시작하세요.
|
|
||||||
</PhosphorText>
|
|
||||||
}
|
|
||||||
renderItem={({ item }) => (
|
|
||||||
<Pressable>
|
|
||||||
<MetalCard style={styles.card}>
|
|
||||||
<View style={styles.cardHeader}>
|
|
||||||
<Led color={statusLedColor(item.status)} size={8} />
|
|
||||||
<PhosphorText variant="label" color="label" style={{ marginLeft: 8 }}>
|
|
||||||
{item.status.toUpperCase()}
|
|
||||||
</PhosphorText>
|
|
||||||
</View>
|
|
||||||
<PhosphorText variant="body" color="primary" style={{ marginTop: 6 }}>
|
|
||||||
{item.title ?? '(제목 없음)'}
|
|
||||||
</PhosphorText>
|
|
||||||
<PhosphorText variant="meta" color="muted" style={{ marginTop: 4 }}>
|
|
||||||
{new Date(item.started_at).toLocaleString('ko-KR')}
|
|
||||||
</PhosphorText>
|
|
||||||
</MetalCard>
|
|
||||||
</Pressable>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
list: { backgroundColor: d3roNativePalette.bg.app },
|
|
||||||
center: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 32,
|
|
||||||
backgroundColor: d3roNativePalette.bg.app
|
|
||||||
},
|
|
||||||
listContent: { padding: 16 },
|
|
||||||
empty: { textAlign: 'center' },
|
|
||||||
card: { marginBottom: 12 },
|
|
||||||
cardHeader: { flexDirection: 'row', alignItems: 'center' }
|
|
||||||
})
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
// apps/mobile/app/(tabs)/profile.tsx
|
|
||||||
// 프로필 — 사용자 정보 + 로그아웃. @d3ro/ui-native 사용
|
|
||||||
|
|
||||||
import { View, StyleSheet, Alert, ScrollView } from 'react-native'
|
|
||||||
import { MetalCard, PhosphorText, PhysicalButton, d3roNativePalette } from '@d3ro/ui-native'
|
|
||||||
import { useAuth } from '../../lib/auth-context'
|
|
||||||
import { supabase } from '../../lib/supabase'
|
|
||||||
|
|
||||||
export default function ProfileScreen(): React.ReactElement {
|
|
||||||
const { user } = useAuth()
|
|
||||||
|
|
||||||
async function handleLogout(): Promise<void> {
|
|
||||||
Alert.alert('로그아웃', '정말 로그아웃하시겠습니까?', [
|
|
||||||
{ text: '취소', style: 'cancel' },
|
|
||||||
{
|
|
||||||
text: '로그아웃',
|
|
||||||
style: 'destructive',
|
|
||||||
onPress: async () => {
|
|
||||||
await supabase.auth.signOut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|
||||||
<MetalCard style={styles.section}>
|
|
||||||
<PhosphorText variant="label" color="label">
|
|
||||||
이메일
|
|
||||||
</PhosphorText>
|
|
||||||
<PhosphorText variant="body" color="primary" style={styles.value}>
|
|
||||||
{user?.email ?? '—'}
|
|
||||||
</PhosphorText>
|
|
||||||
</MetalCard>
|
|
||||||
|
|
||||||
<MetalCard style={styles.section}>
|
|
||||||
<PhosphorText variant="label" color="label">
|
|
||||||
USER ID
|
|
||||||
</PhosphorText>
|
|
||||||
<PhosphorText variant="small" color="primary" style={styles.value}>
|
|
||||||
{user?.id ?? '—'}
|
|
||||||
</PhosphorText>
|
|
||||||
</MetalCard>
|
|
||||||
|
|
||||||
<View style={styles.logoutWrap}>
|
|
||||||
<PhysicalButton label="로그아웃" variant="danger" onPress={() => void handleLogout()} />
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: { flex: 1, backgroundColor: d3roNativePalette.bg.app },
|
|
||||||
content: { padding: 24 },
|
|
||||||
section: { marginBottom: 16 },
|
|
||||||
value: { marginTop: 6 },
|
|
||||||
logoutWrap: { marginTop: 8 }
|
|
||||||
})
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
// apps/mobile/app/(tabs)/record.tsx
|
// apps/mobile/app/(tabs)/record.tsx
|
||||||
// 녹음 — expo-av + @d3ro/ui-native
|
// 녹음 탭 — expo-av 기반 녹음 + STT 파이프라인
|
||||||
|
// Phase M-2에서 디자인(docs/v3/designs/recording.html) 정밀 적용
|
||||||
|
|
||||||
import { useState, useRef } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
import { View, StyleSheet, ActivityIndicator, ScrollView } from 'react-native'
|
import { View, StyleSheet, ActivityIndicator, ScrollView } from 'react-native'
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import { Audio } from 'expo-av'
|
import { Audio } from 'expo-av'
|
||||||
import Constants from 'expo-constants'
|
import Constants from 'expo-constants'
|
||||||
import {
|
import {
|
||||||
|
|
@ -17,19 +19,23 @@ import { supabase, isSupabaseConfigured } from '../../lib/supabase'
|
||||||
type RecordingState = 'idle' | 'recording' | 'processing' | 'done' | 'error'
|
type RecordingState = 'idle' | 'recording' | 'processing' | 'done' | 'error'
|
||||||
|
|
||||||
export default function RecordScreen(): React.ReactElement {
|
export default function RecordScreen(): React.ReactElement {
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
const [state, setState] = useState<RecordingState>('idle')
|
const [state, setState] = useState<RecordingState>('idle')
|
||||||
const [transcript, setTranscript] = useState<string>('')
|
const [transcript, setTranscript] = useState<string>('')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [duration, setDuration] = useState(0)
|
||||||
const recordingRef = useRef<Audio.Recording | null>(null)
|
const recordingRef = useRef<Audio.Recording | null>(null)
|
||||||
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
|
||||||
async function startRecording(): Promise<void> {
|
async function startRecording(): Promise<void> {
|
||||||
setError(null)
|
setError(null)
|
||||||
setTranscript('')
|
setTranscript('')
|
||||||
|
setDuration(0)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const perm = await Audio.requestPermissionsAsync()
|
const perm = await Audio.requestPermissionsAsync()
|
||||||
if (perm.status !== 'granted') {
|
if (perm.status !== 'granted') {
|
||||||
setError('마이크 권한이 거부되었습니다')
|
setError('Microphone permission denied')
|
||||||
setState('error')
|
setState('error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +50,10 @@ export default function RecordScreen(): React.ReactElement {
|
||||||
await recording.startAsync()
|
await recording.startAsync()
|
||||||
recordingRef.current = recording
|
recordingRef.current = recording
|
||||||
setState('recording')
|
setState('recording')
|
||||||
|
|
||||||
|
timerRef.current = setInterval(() => {
|
||||||
|
setDuration((d) => d + 1)
|
||||||
|
}, 1000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : 'Failed to start')
|
setError(e instanceof Error ? e.message : 'Failed to start')
|
||||||
setState('error')
|
setState('error')
|
||||||
|
|
@ -52,6 +62,10 @@ export default function RecordScreen(): React.ReactElement {
|
||||||
|
|
||||||
async function stopRecording(): Promise<void> {
|
async function stopRecording(): Promise<void> {
|
||||||
if (!recordingRef.current) return
|
if (!recordingRef.current) return
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current)
|
||||||
|
timerRef.current = null
|
||||||
|
}
|
||||||
setState('processing')
|
setState('processing')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -60,21 +74,18 @@ export default function RecordScreen(): React.ReactElement {
|
||||||
recordingRef.current = null
|
recordingRef.current = null
|
||||||
|
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
throw new Error('녹음 파일 URI를 받지 못했습니다')
|
throw new Error('No recording URI')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSupabaseConfigured()) {
|
if (!isSupabaseConfigured()) {
|
||||||
setError('Supabase가 설정되지 않아 전사할 수 없습니다')
|
setError('Supabase not configured')
|
||||||
setState('error')
|
setState('error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { data: { session } } = await supabase.auth.getSession()
|
||||||
data: { session }
|
|
||||||
} = await supabase.auth.getSession()
|
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
setError('로그인이 필요합니다')
|
setError('Login required')
|
||||||
setState('error')
|
setState('error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +106,7 @@ export default function RecordScreen(): React.ReactElement {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`STT 실패: ${response.status}`)
|
throw new Error(`STT failed: ${response.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = (await response.json()) as { transcript: string }
|
const result = (await response.json()) as { transcript: string }
|
||||||
|
|
@ -108,36 +119,79 @@ export default function RecordScreen(): React.ReactElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelRecording(): Promise<void> {
|
async function cancelRecording(): Promise<void> {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current)
|
||||||
|
timerRef.current = null
|
||||||
|
}
|
||||||
if (recordingRef.current) {
|
if (recordingRef.current) {
|
||||||
try {
|
try { await recordingRef.current.stopAndUnloadAsync() } catch { /* ignore */ }
|
||||||
await recordingRef.current.stopAndUnloadAsync()
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
recordingRef.current = null
|
recordingRef.current = null
|
||||||
}
|
}
|
||||||
setState('idle')
|
setState('idle')
|
||||||
setTranscript('')
|
setTranscript('')
|
||||||
setError(null)
|
setError(null)
|
||||||
|
setDuration(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusLed: 'amber' | 'red' | 'orange' | 'green' =
|
const formatTime = (s: number): string => {
|
||||||
state === 'recording' ? 'red' : state === 'processing' ? 'orange' : state === 'done' ? 'green' : 'amber'
|
const m = Math.floor(s / 60)
|
||||||
|
const sec = s % 60
|
||||||
|
return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={[styles.header, { paddingTop: insets.top + 8 }]}>
|
||||||
|
<View style={styles.headerLeft}>
|
||||||
|
<Led color="amber" size={8} on={state === 'recording'} />
|
||||||
|
<PhosphorText
|
||||||
|
variant="label"
|
||||||
|
color={state === 'recording' ? 'amber' : 'muted'}
|
||||||
|
style={{ letterSpacing: 3 }}
|
||||||
|
>
|
||||||
|
{state === 'recording' ? 'REC_SESSION' : 'RECORDER'}
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
{state === 'recording' && (
|
||||||
|
<PhosphorText variant="label" color="amber">
|
||||||
|
{formatTime(duration)}
|
||||||
|
</PhosphorText>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Wave Bars Placeholder */}
|
||||||
|
{state === 'recording' && (
|
||||||
|
<View style={styles.waveContainer}>
|
||||||
|
{Array.from({ length: 11 }).map((_, i) => (
|
||||||
|
<View
|
||||||
|
key={i}
|
||||||
|
style={[
|
||||||
|
styles.waveBar,
|
||||||
|
{ height: 8 + Math.random() * 24 }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<MetalCard style={styles.card}>
|
<MetalCard style={styles.card}>
|
||||||
<View style={styles.statusRow}>
|
<View style={styles.statusRow}>
|
||||||
<Led color={statusLed} size={10} on={state !== 'idle'} />
|
<Led
|
||||||
|
color={
|
||||||
|
state === 'recording' ? 'red'
|
||||||
|
: state === 'processing' ? 'orange'
|
||||||
|
: state === 'done' ? 'green'
|
||||||
|
: 'amber'
|
||||||
|
}
|
||||||
|
size={10}
|
||||||
|
on={state !== 'idle'}
|
||||||
|
/>
|
||||||
<PhosphorText variant="label" color="label" style={styles.statusLabel}>
|
<PhosphorText variant="label" color="label" style={styles.statusLabel}>
|
||||||
{state === 'recording'
|
{state === 'recording' ? 'RECORDING'
|
||||||
? 'RECORDING'
|
: state === 'processing' ? 'PROCESSING'
|
||||||
: state === 'processing'
|
: state === 'done' ? 'DONE'
|
||||||
? 'PROCESSING'
|
: state === 'error' ? 'ERROR'
|
||||||
: state === 'done'
|
|
||||||
? 'DONE'
|
|
||||||
: state === 'error'
|
|
||||||
? 'ERROR'
|
|
||||||
: 'READY'}
|
: 'READY'}
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -167,17 +221,17 @@ export default function RecordScreen(): React.ReactElement {
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
{(state === 'idle' || state === 'done' || state === 'error') && (
|
{(state === 'idle' || state === 'done' || state === 'error') && (
|
||||||
<PhysicalButton
|
<PhysicalButton
|
||||||
label={state === 'done' ? '새 녹음' : '녹음 시작'}
|
label={state === 'done' ? 'NEW RECORDING' : 'START RECORDING'}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onPress={() => void startRecording()}
|
onPress={() => void startRecording()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{state === 'recording' && (
|
{state === 'recording' && (
|
||||||
<>
|
<>
|
||||||
<PhysicalButton label="정지" variant="danger" onPress={() => void stopRecording()} />
|
<PhysicalButton label="STOP" variant="danger" onPress={() => void stopRecording()} />
|
||||||
<View style={{ height: 12 }} />
|
<View style={{ height: 12 }} />
|
||||||
<PhysicalButton
|
<PhysicalButton
|
||||||
label="취소"
|
label="CANCEL"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onPress={() => void cancelRecording()}
|
onPress={() => void cancelRecording()}
|
||||||
/>
|
/>
|
||||||
|
|
@ -191,8 +245,33 @@ export default function RecordScreen(): React.ReactElement {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: d3roNativePalette.bg.app },
|
container: { flex: 1, backgroundColor: d3roNativePalette.bg.app },
|
||||||
content: { padding: 24 },
|
content: { paddingBottom: 100 },
|
||||||
card: { padding: 24 },
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: 'rgba(46, 46, 50, 0.5)'
|
||||||
|
},
|
||||||
|
headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||||
|
waveContainer: {
|
||||||
|
height: 128,
|
||||||
|
backgroundColor: d3roNativePalette.bg.inset,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 6,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: 'rgba(26, 26, 28, 1)'
|
||||||
|
},
|
||||||
|
waveBar: {
|
||||||
|
width: 6,
|
||||||
|
backgroundColor: d3roNativePalette.accent.amber,
|
||||||
|
borderRadius: 3
|
||||||
|
},
|
||||||
|
card: { margin: 20, padding: 24 },
|
||||||
statusRow: { flexDirection: 'row', alignItems: 'center' },
|
statusRow: { flexDirection: 'row', alignItems: 'center' },
|
||||||
statusLabel: { marginLeft: 8 },
|
statusLabel: { marginLeft: 8 },
|
||||||
center: { alignItems: 'center', minHeight: 140, justifyContent: 'center', marginVertical: 16 },
|
center: { alignItems: 'center', minHeight: 140, justifyContent: 'center', marginVertical: 16 },
|
||||||
|
|
|
||||||
204
apps/mobile/app/(tabs)/settings.tsx
Normal file
204
apps/mobile/app/(tabs)/settings.tsx
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
// apps/mobile/app/(tabs)/settings.tsx
|
||||||
|
// 설정 탭 — 계정, 백엔드, 환경설정
|
||||||
|
// Phase M-2에서 디자인(docs/v3/designs/settings.html) 정밀 적용
|
||||||
|
|
||||||
|
import { View, ScrollView, StyleSheet, Alert, Switch } from 'react-native'
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import { MetalCard, PhosphorText, PhysicalButton, Led, d3roNativePalette } from '@d3ro/ui-native'
|
||||||
|
import { useAuth } from '../../lib/auth-context'
|
||||||
|
import { supabase } from '../../lib/supabase'
|
||||||
|
|
||||||
|
export default function SettingsScreen(): React.ReactElement {
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const { user } = useAuth()
|
||||||
|
|
||||||
|
async function handleLogout(): Promise<void> {
|
||||||
|
Alert.alert('Logout', 'Are you sure you want to log out?', [
|
||||||
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
|
{
|
||||||
|
text: 'Logout',
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: async () => {
|
||||||
|
await supabase.auth.signOut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const initials = user?.email
|
||||||
|
? user.email.substring(0, 2).toUpperCase()
|
||||||
|
: 'US'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||||
|
{/* Header */}
|
||||||
|
<PhosphorText variant="title" color="primary" style={[styles.title, { paddingTop: insets.top + 8 }]}>
|
||||||
|
Settings
|
||||||
|
</PhosphorText>
|
||||||
|
|
||||||
|
{/* Account Section */}
|
||||||
|
<PhosphorText variant="label" color="muted" style={styles.sectionLabel}>
|
||||||
|
ACCOUNT & PLAN
|
||||||
|
</PhosphorText>
|
||||||
|
<MetalCard style={styles.section}>
|
||||||
|
<View style={styles.profileRow}>
|
||||||
|
<View style={styles.avatar}>
|
||||||
|
<PhosphorText variant="body" color="amber">{initials}</PhosphorText>
|
||||||
|
</View>
|
||||||
|
<View style={styles.profileInfo}>
|
||||||
|
<PhosphorText variant="body" color="primary">
|
||||||
|
{user?.email?.split('@')[0] ?? 'User'}
|
||||||
|
</PhosphorText>
|
||||||
|
<PhosphorText variant="label" color="muted">
|
||||||
|
{user?.email ?? '—'}
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.row, { backgroundColor: d3roNativePalette.bg.inset }]}>
|
||||||
|
<PhosphorText variant="small" color="muted">TIER</PhosphorText>
|
||||||
|
<View style={styles.rowRight}>
|
||||||
|
<Led color="amber" size={6} />
|
||||||
|
<PhosphorText variant="body" color="amber" style={{ marginLeft: 8 }}>
|
||||||
|
FREE
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</MetalCard>
|
||||||
|
|
||||||
|
{/* Backend Section */}
|
||||||
|
<PhosphorText variant="label" color="muted" style={styles.sectionLabel}>
|
||||||
|
BACKEND CONFIG
|
||||||
|
</PhosphorText>
|
||||||
|
<MetalCard style={styles.section}>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<View>
|
||||||
|
<PhosphorText variant="body" color="primary">LLM Model</PhosphorText>
|
||||||
|
<PhosphorText variant="label" color="muted">AI processing backend</PhosphorText>
|
||||||
|
</View>
|
||||||
|
<PhosphorText variant="body" color="amber">CLAUDE</PhosphorText>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.row, styles.rowBorder]}>
|
||||||
|
<View>
|
||||||
|
<PhosphorText variant="body" color="primary">Cloud STT</PhosphorText>
|
||||||
|
<PhosphorText variant="label" color="muted">Google Cloud Speech</PhosphorText>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={true}
|
||||||
|
trackColor={{
|
||||||
|
false: d3roNativePalette.bg.inset,
|
||||||
|
true: d3roNativePalette.accent.amber
|
||||||
|
}}
|
||||||
|
thumbColor="#ffffff"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</MetalCard>
|
||||||
|
|
||||||
|
{/* Preferences Section */}
|
||||||
|
<PhosphorText variant="label" color="muted" style={styles.sectionLabel}>
|
||||||
|
PREFERENCES
|
||||||
|
</PhosphorText>
|
||||||
|
<MetalCard style={styles.section}>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<PhosphorText variant="body" color="primary">Language</PhosphorText>
|
||||||
|
<PhosphorText variant="body" color="muted">Korean</PhosphorText>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.row, styles.rowBorder]}>
|
||||||
|
<PhosphorText variant="body" color="primary">Auto Polish</PhosphorText>
|
||||||
|
<Switch
|
||||||
|
value={true}
|
||||||
|
trackColor={{
|
||||||
|
false: d3roNativePalette.bg.inset,
|
||||||
|
true: d3roNativePalette.accent.amber
|
||||||
|
}}
|
||||||
|
thumbColor="#ffffff"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.row, styles.rowBorder]}>
|
||||||
|
<PhosphorText variant="body" color="primary">Haptic Feedback</PhosphorText>
|
||||||
|
<Switch
|
||||||
|
value={true}
|
||||||
|
trackColor={{
|
||||||
|
false: d3roNativePalette.bg.inset,
|
||||||
|
true: d3roNativePalette.accent.amber
|
||||||
|
}}
|
||||||
|
thumbColor="#ffffff"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</MetalCard>
|
||||||
|
|
||||||
|
{/* Logout */}
|
||||||
|
<View style={styles.logoutWrap}>
|
||||||
|
<PhysicalButton label="LOGOUT" variant="secondary" onPress={() => void handleLogout()} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Status Footer */}
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<Led color="green" size={6} />
|
||||||
|
<PhosphorText variant="label" color="muted" style={{ marginLeft: 6, letterSpacing: 1 }}>
|
||||||
|
CLOUD CONNECTED
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1, backgroundColor: d3roNativePalette.bg.app },
|
||||||
|
content: { paddingBottom: 120 },
|
||||||
|
title: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: 'rgba(46, 46, 50, 0.5)',
|
||||||
|
fontFamily: undefined
|
||||||
|
},
|
||||||
|
sectionLabel: {
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingTop: 20,
|
||||||
|
paddingBottom: 8,
|
||||||
|
letterSpacing: 3
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
marginHorizontal: 20,
|
||||||
|
padding: 0,
|
||||||
|
overflow: 'hidden'
|
||||||
|
},
|
||||||
|
profileRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 16,
|
||||||
|
gap: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: 'rgba(46, 46, 50, 0.5)'
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: d3roNativePalette.bg.inset,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: d3roNativePalette.border.default,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
profileInfo: { gap: 2 },
|
||||||
|
row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 16
|
||||||
|
},
|
||||||
|
rowBorder: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: 'rgba(46, 46, 50, 0.5)'
|
||||||
|
},
|
||||||
|
rowRight: { flexDirection: 'row', alignItems: 'center' },
|
||||||
|
logoutWrap: { margin: 20 },
|
||||||
|
footer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
opacity: 0.5,
|
||||||
|
marginTop: 16
|
||||||
|
}
|
||||||
|
})
|
||||||
188
apps/mobile/app/(tabs)/talk.tsx
Normal file
188
apps/mobile/app/(tabs)/talk.tsx
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
// apps/mobile/app/(tabs)/talk.tsx
|
||||||
|
// AI 대화 탭 — 텍스트+음성 채팅
|
||||||
|
// Phase M-2에서 디자인(docs/v3/designs/talk.html) 정밀 적용
|
||||||
|
// Phase M-5에서 음성 파이프라인 구현
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { View, ScrollView, TextInput, StyleSheet, Pressable, KeyboardAvoidingView, Platform } from 'react-native'
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import { MetalCard, PhosphorText, Led, d3roNativePalette } from '@d3ro/ui-native'
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
id: string
|
||||||
|
role: 'user' | 'assistant'
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TalkScreen(): React.ReactElement {
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'Hello! How can I help you today?'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const [input, setInput] = useState('')
|
||||||
|
|
||||||
|
function handleSend(): void {
|
||||||
|
if (!input.trim()) return
|
||||||
|
const userMsg: ChatMessage = {
|
||||||
|
id: String(Date.now()),
|
||||||
|
role: 'user',
|
||||||
|
content: input.trim()
|
||||||
|
}
|
||||||
|
setMessages((prev) => [...prev, userMsg])
|
||||||
|
setInput('')
|
||||||
|
// Phase M-5: 실제 AI 응답 구현
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.container}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
keyboardVerticalOffset={80}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={[styles.header, { paddingTop: insets.top + 8 }]}>
|
||||||
|
<View style={styles.headerLeft}>
|
||||||
|
<Led color="amber" size={8} />
|
||||||
|
<PhosphorText variant="label" color="muted" style={{ letterSpacing: 3 }}>
|
||||||
|
TALK
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
<View style={styles.headerRight}>
|
||||||
|
<PhosphorText variant="label" color="amber">LIVE</PhosphorText>
|
||||||
|
<Led color="green" size={6} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Chat Messages */}
|
||||||
|
<ScrollView style={styles.chatArea} contentContainerStyle={styles.chatContent}>
|
||||||
|
{messages.map((msg) => (
|
||||||
|
<View
|
||||||
|
key={msg.id}
|
||||||
|
style={[
|
||||||
|
styles.bubble,
|
||||||
|
msg.role === 'user' ? styles.userBubble : styles.aiBubble
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<PhosphorText
|
||||||
|
variant="body"
|
||||||
|
color={msg.role === 'user' ? 'amber' : 'primary'}
|
||||||
|
style={{ fontFamily: undefined }}
|
||||||
|
>
|
||||||
|
{msg.content}
|
||||||
|
</PhosphorText>
|
||||||
|
<PhosphorText variant="label" color="muted" style={styles.bubbleLabel}>
|
||||||
|
{msg.role === 'user' ? 'YOU' : 'CLAUDE'}
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Input Area */}
|
||||||
|
<View style={styles.inputArea}>
|
||||||
|
<View style={styles.inputRow}>
|
||||||
|
<TextInput
|
||||||
|
style={styles.textInput}
|
||||||
|
placeholder="Type or speak..."
|
||||||
|
placeholderTextColor={d3roNativePalette.text.muted}
|
||||||
|
value={input}
|
||||||
|
onChangeText={setInput}
|
||||||
|
onSubmitEditing={handleSend}
|
||||||
|
returnKeyType="send"
|
||||||
|
/>
|
||||||
|
<Pressable style={styles.sendBtn} onPress={handleSend}>
|
||||||
|
<View style={styles.sendArrow} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1, backgroundColor: d3roNativePalette.bg.app },
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: d3roNativePalette.border.default
|
||||||
|
},
|
||||||
|
headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||||
|
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||||
|
chatArea: { flex: 1 },
|
||||||
|
chatContent: { padding: 20, paddingBottom: 20, gap: 16 },
|
||||||
|
bubble: {
|
||||||
|
maxWidth: '85%',
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 16,
|
||||||
|
position: 'relative',
|
||||||
|
marginBottom: 12
|
||||||
|
},
|
||||||
|
aiBubble: {
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
backgroundColor: d3roNativePalette.bg.card,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: d3roNativePalette.border.default,
|
||||||
|
borderTopLeftRadius: 4
|
||||||
|
},
|
||||||
|
userBubble: {
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
backgroundColor: d3roNativePalette.accent.amberDim,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 92, 53, 0.3)',
|
||||||
|
borderTopRightRadius: 4
|
||||||
|
},
|
||||||
|
bubbleLabel: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -16,
|
||||||
|
fontSize: 9
|
||||||
|
},
|
||||||
|
inputArea: {
|
||||||
|
backgroundColor: 'rgba(25, 25, 27, 0.95)',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: d3roNativePalette.border.default,
|
||||||
|
padding: 12,
|
||||||
|
paddingBottom: 24
|
||||||
|
},
|
||||||
|
inputRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: d3roNativePalette.bg.inset,
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: d3roNativePalette.border.default,
|
||||||
|
padding: 8
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
flex: 1,
|
||||||
|
color: d3roNativePalette.text.primary,
|
||||||
|
fontSize: 14,
|
||||||
|
paddingHorizontal: 8
|
||||||
|
},
|
||||||
|
sendBtn: {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: d3roNativePalette.accent.amberDim,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 92, 53, 0.3)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
sendArrow: {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
borderLeftWidth: 5,
|
||||||
|
borderRightWidth: 5,
|
||||||
|
borderBottomWidth: 8,
|
||||||
|
borderLeftColor: 'transparent',
|
||||||
|
borderRightColor: 'transparent',
|
||||||
|
borderBottomColor: d3roNativePalette.accent.amber
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -21,7 +21,7 @@ export default function RootLayout(): React.ReactElement {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="login" options={{ title: '로그인' }} />
|
<Stack.Screen name="login" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default function Index(): React.ReactElement {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
if (user) {
|
if (user) {
|
||||||
router.replace('/(tabs)/meetings')
|
router.replace('/(tabs)/dash')
|
||||||
} else {
|
} else {
|
||||||
router.replace('/login')
|
router.replace('/login')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,33 @@
|
||||||
// apps/mobile/app/login.tsx
|
// apps/mobile/app/login.tsx
|
||||||
// OAuth 로그인 화면 — @d3ro/ui-native 사용
|
// 로그인 화면 — 디자인: docs/v3/designs/login.html
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { View, StyleSheet, Alert } from 'react-native'
|
import { View, Text, TextInput, StyleSheet, Alert, ScrollView, Pressable, Platform } from 'react-native'
|
||||||
import * as WebBrowser from 'expo-web-browser'
|
import * as WebBrowser from 'expo-web-browser'
|
||||||
import * as Linking from 'expo-linking'
|
import * as Linking from 'expo-linking'
|
||||||
import { MetalCard, PhosphorText, PhysicalButton, d3roNativePalette } from '@d3ro/ui-native'
|
import { d3roNativePalette, d3roNativeFonts, Led } from '@d3ro/ui-native'
|
||||||
import { supabase, isSupabaseConfigured } from '../lib/supabase'
|
import { supabase, isSupabaseConfigured } from '../lib/supabase'
|
||||||
|
import { useAuth } from '../lib/auth-context'
|
||||||
|
import { useRouter } from 'expo-router'
|
||||||
|
|
||||||
WebBrowser.maybeCompleteAuthSession()
|
WebBrowser.maybeCompleteAuthSession()
|
||||||
|
|
||||||
export default function LoginScreen(): React.ReactElement {
|
export default function LoginScreen(): React.ReactElement {
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
const configured = isSupabaseConfigured()
|
const configured = isSupabaseConfigured()
|
||||||
|
const { devBypass } = useAuth()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
async function signInWithProvider(provider: 'google' | 'github'): Promise<void> {
|
function handleDevSkip(): void {
|
||||||
|
devBypass()
|
||||||
|
router.replace('/(tabs)/dash')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signInWithProvider(provider: 'google' | 'github' | 'apple'): Promise<void> {
|
||||||
if (!configured) {
|
if (!configured) {
|
||||||
Alert.alert('미설정', 'Supabase 환경변수가 설정되지 않았습니다.')
|
Alert.alert('Not Configured', 'Supabase environment variables are not set.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
|
|
@ -28,7 +39,7 @@ export default function LoginScreen(): React.ReactElement {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (error || !data.url) {
|
if (error || !data.url) {
|
||||||
Alert.alert('로그인 실패', error?.message ?? 'OAuth URL을 받지 못했습니다')
|
Alert.alert('Login Failed', error?.message ?? 'Could not get OAuth URL')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,7 +50,7 @@ export default function LoginScreen(): React.ReactElement {
|
||||||
if (code) {
|
if (code) {
|
||||||
const { error: exchangeErr } = await supabase.auth.exchangeCodeForSession(code)
|
const { error: exchangeErr } = await supabase.auth.exchangeCodeForSession(code)
|
||||||
if (exchangeErr) {
|
if (exchangeErr) {
|
||||||
Alert.alert('세션 교환 실패', exchangeErr.message)
|
Alert.alert('Session Error', exchangeErr.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,68 +59,323 @@ export default function LoginScreen(): React.ReactElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function signInWithEmail(): Promise<void> {
|
||||||
|
if (!configured) {
|
||||||
|
Alert.alert('Not Configured', 'Supabase environment variables are not set.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!email.trim() || !password.trim()) return
|
||||||
|
setBusy(true)
|
||||||
|
try {
|
||||||
|
const { error } = await supabase.auth.signInWithPassword({ email, password })
|
||||||
|
if (error) {
|
||||||
|
Alert.alert('Login Failed', error.message)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const monoFont = d3roNativeFonts.mono
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<ScrollView
|
||||||
<MetalCard style={styles.card}>
|
style={styles.container}
|
||||||
<View style={styles.header}>
|
contentContainerStyle={styles.content}
|
||||||
<PhosphorText variant="hero">D3RO VOICE</PhosphorText>
|
keyboardShouldPersistTaps="handled"
|
||||||
<PhosphorText variant="label" color="secondary" style={styles.subtitle}>
|
>
|
||||||
AI 음성 어시스턴트
|
{/* Logo & Branding */}
|
||||||
</PhosphorText>
|
<View style={styles.branding}>
|
||||||
|
<View style={styles.leds}>
|
||||||
|
<Led color="amber" size={10} />
|
||||||
|
<Led color="green" size={10} />
|
||||||
</View>
|
</View>
|
||||||
|
<Text style={[styles.title, { fontFamily: monoFont }]}>D3RO-VOICE</Text>
|
||||||
|
<Text style={[styles.subtitle, { fontFamily: monoFont }]}>
|
||||||
|
PRECISION VOICE INTELLIGENCE
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
{!configured && (
|
{/* OAuth Buttons */}
|
||||||
<View style={styles.warningBox}>
|
<View style={styles.oauthSection}>
|
||||||
<PhosphorText variant="small" color="label">
|
<Pressable
|
||||||
Supabase가 설정되지 않았습니다. app.json의 extra 필드를 확인하세요.
|
style={({ pressed }) => [styles.oauthBtn, pressed && styles.oauthBtnPressed]}
|
||||||
</PhosphorText>
|
onPress={() => void signInWithProvider('google')}
|
||||||
</View>
|
disabled={busy}
|
||||||
)}
|
>
|
||||||
|
<Text style={styles.oauthIcon}>G</Text>
|
||||||
|
<Text style={styles.oauthLabel}>Google로 로그인</Text>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
<View style={styles.buttons}>
|
<Pressable
|
||||||
<PhysicalButton
|
style={({ pressed }) => [styles.oauthBtn, pressed && styles.oauthBtnPressed]}
|
||||||
label="Google로 계속하기"
|
onPress={() => void signInWithProvider('apple')}
|
||||||
variant="primary"
|
disabled={busy}
|
||||||
disabled={!configured || busy}
|
>
|
||||||
onPress={() => void signInWithProvider('google')}
|
<Text style={[styles.oauthIcon, { fontSize: 18 }]}></Text>
|
||||||
/>
|
<Text style={styles.oauthLabel}>Apple로 로그인</Text>
|
||||||
<PhysicalButton
|
</Pressable>
|
||||||
label="GitHub로 계속하기"
|
|
||||||
variant="secondary"
|
<Pressable
|
||||||
disabled={!configured || busy}
|
style={({ pressed }) => [styles.oauthBtn, pressed && styles.oauthBtnPressed]}
|
||||||
onPress={() => void signInWithProvider('github')}
|
onPress={() => void signInWithProvider('github')}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
<Text style={styles.oauthIcon}>⌘</Text>
|
||||||
|
<Text style={styles.oauthLabel}>GitHub로 로그인</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<View style={styles.divider}>
|
||||||
|
<View style={styles.dividerLine} />
|
||||||
|
<Text style={[styles.dividerText, { fontFamily: monoFont }]}>또는</Text>
|
||||||
|
<View style={styles.dividerLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Email/Password Fields */}
|
||||||
|
<View style={styles.formSection}>
|
||||||
|
<View style={styles.fieldGroup}>
|
||||||
|
<Text style={[styles.fieldLabel, { fontFamily: monoFont }]}>EMAIL</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="user@studio.com"
|
||||||
|
placeholderTextColor="rgba(113, 113, 122, 0.3)"
|
||||||
|
value={email}
|
||||||
|
onChangeText={setEmail}
|
||||||
|
keyboardType="email-address"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</MetalCard>
|
<View style={styles.fieldGroup}>
|
||||||
</View>
|
<Text style={[styles.fieldLabel, { fontFamily: monoFont }]}>PASSWORD</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="••••••••"
|
||||||
|
placeholderTextColor="rgba(113, 113, 122, 0.3)"
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
secureTextEntry
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Login Button */}
|
||||||
|
<Pressable
|
||||||
|
style={({ pressed }) => [styles.loginBtn, pressed && { opacity: 0.9 }]}
|
||||||
|
onPress={() => void signInWithEmail()}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
<Text style={styles.loginBtnText}>로그인</Text>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
{/* Sign Up Link */}
|
||||||
|
<View style={styles.signupRow}>
|
||||||
|
<Text style={styles.signupText}>계정이 없으신가요?</Text>
|
||||||
|
<Pressable>
|
||||||
|
<Text style={styles.signupLink}>회원가입</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* DEV Skip (개발 전용) */}
|
||||||
|
{__DEV__ && (
|
||||||
|
<Pressable style={styles.devSkipBtn} onPress={handleDevSkip}>
|
||||||
|
<Text style={[styles.devSkipText, { fontFamily: monoFont }]}>
|
||||||
|
⚡ DEV SKIP LOGIN
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Supabase Warning */}
|
||||||
|
{!configured && (
|
||||||
|
<View style={styles.warningBox}>
|
||||||
|
<Text style={[styles.warningText, { fontFamily: monoFont }]}>
|
||||||
|
⚠ Supabase not configured. Set EXPO_PUBLIC_SUPABASE_URL in .env
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const P = d3roNativePalette
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: d3roNativePalette.bg.app,
|
backgroundColor: P.bg.app
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flexGrow: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: 24
|
paddingHorizontal: 24,
|
||||||
|
paddingBottom: 40,
|
||||||
|
paddingTop: 60
|
||||||
},
|
},
|
||||||
card: {
|
|
||||||
padding: 32
|
// Branding
|
||||||
},
|
branding: {
|
||||||
header: {
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 32
|
marginBottom: 40
|
||||||
|
},
|
||||||
|
leds: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 10,
|
||||||
|
marginBottom: 20
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 22,
|
||||||
|
letterSpacing: 5,
|
||||||
|
color: P.text.primary
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
marginTop: 8
|
fontSize: 9,
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: P.text.muted,
|
||||||
|
marginTop: 8,
|
||||||
|
textTransform: 'uppercase'
|
||||||
},
|
},
|
||||||
warningBox: {
|
|
||||||
|
// OAuth
|
||||||
|
oauthSection: {
|
||||||
|
gap: 12,
|
||||||
|
marginBottom: 24
|
||||||
|
},
|
||||||
|
oauthBtn: {
|
||||||
|
backgroundColor: P.bg.card,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: d3roNativePalette.tag.orange,
|
borderColor: P.border.default,
|
||||||
padding: 12,
|
borderRadius: 12,
|
||||||
borderRadius: 8,
|
paddingVertical: 14,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 12
|
||||||
|
},
|
||||||
|
oauthBtnPressed: {
|
||||||
|
backgroundColor: P.bg.cardHover,
|
||||||
|
borderColor: 'rgba(113, 113, 122, 0.4)'
|
||||||
|
},
|
||||||
|
oauthIcon: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: P.text.primary
|
||||||
|
},
|
||||||
|
oauthLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: P.text.primary
|
||||||
|
},
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
divider: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 16,
|
||||||
|
marginBottom: 24
|
||||||
|
},
|
||||||
|
dividerLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: P.border.default
|
||||||
|
},
|
||||||
|
dividerText: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: P.text.muted,
|
||||||
|
letterSpacing: 2
|
||||||
|
},
|
||||||
|
|
||||||
|
// Form
|
||||||
|
formSection: {
|
||||||
|
gap: 16,
|
||||||
|
marginBottom: 24
|
||||||
|
},
|
||||||
|
fieldGroup: {},
|
||||||
|
fieldLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: P.text.muted,
|
||||||
|
letterSpacing: 4,
|
||||||
|
marginBottom: 6,
|
||||||
|
marginLeft: 4
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
backgroundColor: P.bg.inset,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: P.border.default,
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
fontSize: 14,
|
||||||
|
color: P.text.primary,
|
||||||
|
fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Login Button
|
||||||
|
loginBtn: {
|
||||||
|
backgroundColor: P.accent.amber,
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingVertical: 14,
|
||||||
|
alignItems: 'center',
|
||||||
|
shadowColor: P.accent.amber,
|
||||||
|
shadowOffset: { width: 0, height: 0 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 10,
|
||||||
|
elevation: 6,
|
||||||
|
marginBottom: 24
|
||||||
|
},
|
||||||
|
loginBtnText: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: P.bg.app,
|
||||||
|
letterSpacing: 1
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sign Up
|
||||||
|
signupRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
},
|
},
|
||||||
buttons: {
|
signupText: {
|
||||||
gap: 12
|
fontSize: 11,
|
||||||
|
color: P.text.muted
|
||||||
|
},
|
||||||
|
signupLink: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: P.accent.amber,
|
||||||
|
fontWeight: '500'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Warning
|
||||||
|
warningBox: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 92, 53, 0.3)',
|
||||||
|
backgroundColor: P.accent.amberDim,
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginTop: 8
|
||||||
|
},
|
||||||
|
warningText: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: P.accent.amber,
|
||||||
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
|
||||||
|
// DEV Skip
|
||||||
|
devSkipBtn: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: P.accent.green,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingVertical: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
backgroundColor: 'rgba(74, 222, 128, 0.08)'
|
||||||
|
},
|
||||||
|
devSkipText: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: P.accent.green,
|
||||||
|
letterSpacing: 2
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,26 @@ interface AuthContextValue {
|
||||||
session: Session | null
|
session: Session | null
|
||||||
user: User | null
|
user: User | null
|
||||||
loading: boolean
|
loading: boolean
|
||||||
|
devBypass: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextValue>({
|
const AuthContext = createContext<AuthContextValue>({
|
||||||
session: null,
|
session: null,
|
||||||
user: null,
|
user: null,
|
||||||
loading: true
|
loading: true,
|
||||||
|
devBypass: () => {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// DEV 전용 — 가짜 유저로 로그인 우회
|
||||||
|
const DEV_USER: User = {
|
||||||
|
id: 'dev-user-00000',
|
||||||
|
email: 'dev@d3ro.local',
|
||||||
|
app_metadata: {},
|
||||||
|
user_metadata: {},
|
||||||
|
aud: 'authenticated',
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
} as User
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }): React.ReactElement {
|
export function AuthProvider({ children }: { children: ReactNode }): React.ReactElement {
|
||||||
const [session, setSession] = useState<Session | null>(null)
|
const [session, setSession] = useState<Session | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
@ -47,8 +59,16 @@ export function AuthProvider({ children }: { children: ReactNode }): React.React
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const [devMode, setDevMode] = useState(false)
|
||||||
|
|
||||||
|
function devBypass(): void {
|
||||||
|
setDevMode(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveUser = devMode ? DEV_USER : (session?.user ?? null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ session, user: session?.user ?? null, loading }}>
|
<AuthContext.Provider value={{ session, user: effectiveUser, loading, devBypass }}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,29 +3,33 @@
|
||||||
|
|
||||||
import 'react-native-url-polyfill/auto'
|
import 'react-native-url-polyfill/auto'
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
import { createClient } from '@supabase/supabase-js'
|
import { createClient, type SupabaseClient } from '@supabase/supabase-js'
|
||||||
import Constants from 'expo-constants'
|
import Constants from 'expo-constants'
|
||||||
|
|
||||||
const supabaseUrl = (Constants.expoConfig?.extra?.supabaseUrl as string | undefined) ?? ''
|
const supabaseUrl = (Constants.expoConfig?.extra?.supabaseUrl as string | undefined) ?? ''
|
||||||
const supabaseAnonKey = (Constants.expoConfig?.extra?.supabaseAnonKey as string | undefined) ?? ''
|
const supabaseAnonKey = (Constants.expoConfig?.extra?.supabaseAnonKey as string | undefined) ?? ''
|
||||||
|
|
||||||
if (!supabaseUrl || !supabaseAnonKey) {
|
export function isSupabaseConfigured(): boolean {
|
||||||
// env 미설정 — placeholder로 client 생성, 런타임에 isConfigured 체크
|
return Boolean(supabaseUrl && supabaseAnonKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const supabase = createClient(
|
function createSupabaseClient(): SupabaseClient {
|
||||||
supabaseUrl || 'https://placeholder.supabase.co',
|
if (!isSupabaseConfigured()) {
|
||||||
supabaseAnonKey || 'placeholder-anon-key',
|
// env 미설정 시에도 크래시 방지용 더미 클라이언트 생성
|
||||||
{
|
// 런타임에서 isSupabaseConfigured()로 체크 후 사용
|
||||||
|
return createClient('https://localhost.invalid', 'no-key', {
|
||||||
|
auth: { storage: AsyncStorage, persistSession: false, detectSessionInUrl: false }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return createClient(supabaseUrl, supabaseAnonKey, {
|
||||||
auth: {
|
auth: {
|
||||||
storage: AsyncStorage,
|
storage: AsyncStorage,
|
||||||
autoRefreshToken: true,
|
autoRefreshToken: true,
|
||||||
persistSession: true,
|
persistSession: true,
|
||||||
detectSessionInUrl: false
|
detectSessionInUrl: false
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
export function isSupabaseConfigured(): boolean {
|
|
||||||
return Boolean(supabaseUrl && supabaseAnonKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const supabase = createSupabaseClient()
|
||||||
|
|
|
||||||
36
apps/mobile/metro.config.js
Normal file
36
apps/mobile/metro.config.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// apps/mobile/metro.config.js
|
||||||
|
// Monorepo 호환 Metro 설정
|
||||||
|
// 핵심: root node_modules의 react-native 0.84를 절대 참조하지 않도록 격리
|
||||||
|
|
||||||
|
const { getDefaultConfig } = require('expo/metro-config')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const projectRoot = __dirname
|
||||||
|
const monorepoRoot = path.resolve(projectRoot, '../..')
|
||||||
|
|
||||||
|
const config = getDefaultConfig(projectRoot)
|
||||||
|
|
||||||
|
// packages 디렉토리만 watch (root node_modules는 watch하지 않음!)
|
||||||
|
config.watchFolders = [
|
||||||
|
path.resolve(monorepoRoot, 'packages/core'),
|
||||||
|
path.resolve(monorepoRoot, 'packages/ui-native'),
|
||||||
|
path.resolve(monorepoRoot, 'packages/i18n'),
|
||||||
|
path.resolve(monorepoRoot, 'packages/api-client')
|
||||||
|
]
|
||||||
|
|
||||||
|
// node_modules: 로컬만 사용
|
||||||
|
config.resolver.nodeModulesPaths = [
|
||||||
|
path.resolve(projectRoot, 'node_modules')
|
||||||
|
]
|
||||||
|
|
||||||
|
// packages 내에서 import하는 모든 모듈을 로컬 node_modules로 강제
|
||||||
|
config.resolver.extraNodeModules = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: (_target, name) => {
|
||||||
|
return path.resolve(projectRoot, 'node_modules', String(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = config
|
||||||
|
|
@ -11,6 +11,9 @@
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@d3ro/api-client": "file:../../packages/api-client",
|
||||||
|
"@d3ro/core": "file:../../packages/core",
|
||||||
|
"@d3ro/i18n": "file:../../packages/i18n",
|
||||||
"@d3ro/ui-native": "file:../../packages/ui-native",
|
"@d3ro/ui-native": "file:../../packages/ui-native",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@supabase/supabase-js": "^2.45.0",
|
"@supabase/supabase-js": "^2.45.0",
|
||||||
|
|
@ -35,5 +38,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~18.2.0",
|
"@types/react": "~18.2.0",
|
||||||
"typescript": "^5.7.0"
|
"typescript": "^5.7.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "~18.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,19 @@
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
"skipLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
}
|
},
|
||||||
|
"typeRoots": ["./node_modules/@types"]
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
|
"include": [
|
||||||
|
"app/**/*.ts",
|
||||||
|
"app/**/*.tsx",
|
||||||
|
"lib/**/*.ts",
|
||||||
|
"lib/**/*.tsx",
|
||||||
|
".expo/types/**/*.ts",
|
||||||
|
"expo-env.d.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
838
docs/v3/00-mobile-master-plan.md
Normal file
838
docs/v3/00-mobile-master-plan.md
Normal file
|
|
@ -0,0 +1,838 @@
|
||||||
|
# D3RO-VOICE V3 — 모바일 앱 마스터 플랜
|
||||||
|
|
||||||
|
> React Native (Expo) 기반 iOS/Android 동시 출시
|
||||||
|
> 데스크톱↔웹↔모바일 완전 동기화 + D3RO 인스트루먼트 미학
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 전략 결정 요약
|
||||||
|
|
||||||
|
| 항목 | 결정 |
|
||||||
|
|------|------|
|
||||||
|
| **타겟 사용자** | 올인원 — 비즈니스(회의), 교육(강의), 일반(메모) |
|
||||||
|
| **기술 스택** | React Native (Expo 51+) + TypeScript strict |
|
||||||
|
| **STT 엔진** | 클라우드 전용 — Google Cloud STT via Edge Function |
|
||||||
|
| **LLM** | Anthropic Claude API via Edge Function |
|
||||||
|
| **오디오 플로우** | 로컬 녹음 → 완료 후 업로드 → 서버 전사 → AI 후처리 |
|
||||||
|
| **데이터** | Supabase PostgreSQL (SSOT) — 로컬 캐시 없음 |
|
||||||
|
| **동기화** | 전체 동기화 — 히스토리, 회의록, 설정, 사전, RAG |
|
||||||
|
| **디자인** | D3RO 인스트루먼트 미학 유지 (packages/ui-native) |
|
||||||
|
| **수익** | 기존 Free/Pro/Pro+ 크로스 플랫폼 구독 통합 |
|
||||||
|
| **출시** | iOS + Android 동시 (Expo EAS Build) |
|
||||||
|
| **팀 기능** | 초기부터 포함 |
|
||||||
|
| **모바일 특화** | 백그라운드 녹음 + 푸시 알림 + 홈 위젯 |
|
||||||
|
| **앱 이름** | D3RO Voice (데스크톱/웹과 통일) |
|
||||||
|
| **출시 지역** | 글로벌 동시 출시 (12개 언어) |
|
||||||
|
| **최소 OS** | iOS 16+ / Android 12+ |
|
||||||
|
| **화자 구분** | 필수 — 초기부터 (서버사이드 pyannote) |
|
||||||
|
| **오디오 보관** | 클라우드 영구 저장 (Supabase Storage) |
|
||||||
|
| **편집** | 전사 원본 + AI 요약 + 회의록 모바일에서 전체 편집 가능 |
|
||||||
|
| **사용 시나리오** | 회의/강의/음성메모 모두 동등 우선순위 |
|
||||||
|
| **개발 방식** | 바이브코딩 (Claude Code) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 현재 상태 (V2-6 MVP 잔존물)
|
||||||
|
|
||||||
|
apps/mobile에 Expo 스켈레톤이 존재:
|
||||||
|
|
||||||
|
| 항목 | 상태 | 비고 |
|
||||||
|
|------|------|------|
|
||||||
|
| 프로젝트 설정 | ✅ 완료 | Expo 51, expo-av, expo-notifications |
|
||||||
|
| OAuth 로그인 | ✅ 완료 | Google + GitHub |
|
||||||
|
| 녹음→STT 파이프라인 | ✅ 완료 | expo-av → stt-proxy Edge Function |
|
||||||
|
| 회의 목록 | ⚠️ 60% | FlatList 조회만, 상세 화면 없음 |
|
||||||
|
| 프로필/로그아웃 | ✅ 완료 | |
|
||||||
|
| 푸시 알림 | ⚠️ 40% | 토큰 등록만, 수신 미구현 |
|
||||||
|
| D3RO 디자인 | ❌ 미적용 | 기본 RN 스타일만 사용중 |
|
||||||
|
| 팀 기능 | ❌ 없음 | |
|
||||||
|
| AI 후처리 | ❌ 없음 | STT만 있고 요약/회의록 없음 |
|
||||||
|
| 오프라인 캐시 | ❌ 없음 | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 핵심 아키텍처
|
||||||
|
|
||||||
|
### 3.1 오디오 파이프라인 (핵심 수익 기능)
|
||||||
|
|
||||||
|
```
|
||||||
|
[모바일 앱]
|
||||||
|
마이크 (expo-av) → 로컬 녹음 파일 (.m4a)
|
||||||
|
↓ 녹음 완료
|
||||||
|
Supabase Storage 업로드 (audio/{user_id}/{session_id}.m4a)
|
||||||
|
↓ 업로드 완료
|
||||||
|
Edge Function: stt-proxy
|
||||||
|
→ Google Cloud STT (audio → transcript segments)
|
||||||
|
→ 결과를 transcripts 테이블에 저장
|
||||||
|
↓ 전사 완료
|
||||||
|
Edge Function: ai-process
|
||||||
|
→ Claude API (transcript → summary/minutes/action-items)
|
||||||
|
→ 결과를 meetings 테이블에 저장
|
||||||
|
↓ 처리 완료
|
||||||
|
푸시 알림 → 모바일에 "전사 완료" 알림
|
||||||
|
모바일에서 결과 조회 + 편집
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 실시간 회의 모드 (장시간 녹음)
|
||||||
|
|
||||||
|
```
|
||||||
|
[모바일 앱 — 백그라운드 녹음]
|
||||||
|
마이크 → expo-av (foreground service / background audio)
|
||||||
|
↓ N분 단위 청크 분할
|
||||||
|
청크별 업로드 → stt-proxy → 부분 전사 결과
|
||||||
|
↓ Supabase Realtime
|
||||||
|
모바일 UI에 실시간 전사 텍스트 표시
|
||||||
|
↓ 녹음 종료
|
||||||
|
전체 병합 → AI 후처리 → 회의록 생성
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 AI 음성 대화
|
||||||
|
|
||||||
|
```
|
||||||
|
[모바일 앱]
|
||||||
|
마이크 → 짧은 녹음 (1~30초)
|
||||||
|
↓
|
||||||
|
stt-proxy → 텍스트
|
||||||
|
↓
|
||||||
|
llm-proxy → Claude 응답 (스트리밍)
|
||||||
|
↓
|
||||||
|
tts-proxy → 음성 합성 (Google Cloud TTS / Edge TTS)
|
||||||
|
↓
|
||||||
|
expo-av → 재생
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 데이터 동기화
|
||||||
|
|
||||||
|
```
|
||||||
|
Supabase PostgreSQL (SSOT)
|
||||||
|
├── meetings (회의 세션)
|
||||||
|
├── meeting_documents (생성 문서: 회의록/요약/액션아이템)
|
||||||
|
├── transcripts (전사 세그먼트)
|
||||||
|
├── history (전사 히스토리)
|
||||||
|
├── dictionary (커스텀 사전)
|
||||||
|
├── user_settings (설정 동기화)
|
||||||
|
├── rag_documents / rag_chunks (RAG)
|
||||||
|
├── teams / team_members (팀)
|
||||||
|
└── subscriptions (구독)
|
||||||
|
|
||||||
|
모바일은 Supabase 직접 쿼리 (로컬 DB 없음)
|
||||||
|
데스크톱 SQLite ↔ Supabase 양방향 동기화 (기존 CloudSyncService)
|
||||||
|
웹은 Supabase 직접 쿼리 (기존 apps/web)
|
||||||
|
|
||||||
|
→ 어디서 녹음하든 모든 플랫폼에서 조회/편집 가능
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Phase 로드맵
|
||||||
|
|
||||||
|
> 바이브코딩(Claude Code) 기준 일정 산정
|
||||||
|
> 각 Phase는 독립 커밋 + 테스트 가능 단위
|
||||||
|
|
||||||
|
### Phase M-1: 프로젝트 기반 정비 (1~2일)
|
||||||
|
|
||||||
|
**목표:** 기존 MVP 스켈레톤을 정상 구동 가능한 상태로 정비
|
||||||
|
|
||||||
|
- [ ] Expo SDK 버전 확인 및 의존성 정리
|
||||||
|
- [ ] Supabase 환경변수 실제 값 연결 (placeholder 제거)
|
||||||
|
- [ ] app.config.ts로 전환 (환경변수 관리)
|
||||||
|
- [ ] @d3ro/ui-native 패키지 연결 검증
|
||||||
|
- [ ] @d3ro/core, @d3ro/i18n, @d3ro/api-client 연결 검증
|
||||||
|
- [ ] iOS 시뮬레이터 + Android 에뮬레이터 빌드 확인
|
||||||
|
- [ ] 기존 record.tsx 녹음→STT 파이프라인 동작 확인
|
||||||
|
|
||||||
|
**산출물:** 모바일 앱이 실제 Supabase에 연결되어 녹음→전사 가능
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase M-2: D3RO 디자인 시스템 적용 (2~3일)
|
||||||
|
|
||||||
|
**목표:** packages/ui-native 완성 + 전체 앱에 D3RO 인스트루먼트 미학 적용
|
||||||
|
|
||||||
|
- [ ] ui-native 테마 토큰 정의 (d3roPalette, d3roShadow, d3roRadius, d3roTypo)
|
||||||
|
- [ ] 핵심 DS 컴포넌트 구현:
|
||||||
|
- MetalCard (금속 질감 카드)
|
||||||
|
- PhosphorText (13가지 변형 — 녹색 형광 텍스트)
|
||||||
|
- Led (상태 LED 인디케이터)
|
||||||
|
- PhysicalButton (물리 버튼 느낌)
|
||||||
|
- ScreenPanel (CRT 스크린 패널)
|
||||||
|
- WaveBars (9개 웨이브 바 애니메이션)
|
||||||
|
- [ ] 공통 레이아웃 컴포넌트 (AppShell, TabBar, Header)
|
||||||
|
- [ ] 로그인 화면 D3RO 디자인 적용
|
||||||
|
- [ ] 탭 네비게이션 D3RO 스타일 적용
|
||||||
|
- [ ] i18n 연결 (t() 함수, 12개 locale)
|
||||||
|
- [ ] 다크 모드 전용 (D3RO 미학은 어두운 테마)
|
||||||
|
|
||||||
|
**산출물:** 앱 전체가 D3RO 인스트루먼트 미학으로 통일
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase M-3: 녹음→전사→요약 핵심 플로우 (3~4일)
|
||||||
|
|
||||||
|
**목표:** 핵심 수익 기능 완성 — 녹음부터 AI 요약까지 풀 파이프라인
|
||||||
|
|
||||||
|
- [ ] 녹음 화면 고도화:
|
||||||
|
- WaveBars 실시간 오디오 레벨 시각화
|
||||||
|
- 녹음 시간 표시 (타이머)
|
||||||
|
- 일시정지/재개 지원
|
||||||
|
- 녹음 품질 설정 (고/중/저)
|
||||||
|
- [ ] 오디오 파일 Supabase Storage 업로드:
|
||||||
|
- 진행률 표시
|
||||||
|
- 네트워크 끊김 시 재시도
|
||||||
|
- 백그라운드 업로드 (앱 나가도 계속)
|
||||||
|
- [ ] Edge Function 확장:
|
||||||
|
- stt-proxy: 긴 오디오 분할 처리 (15초 단위)
|
||||||
|
- diarize-proxy: pyannote 기반 화자 구분 (서버사이드)
|
||||||
|
- ai-process: 전사 결과 → Claude API → 요약/키포인트/액션아이템
|
||||||
|
- 처리 상태 업데이트 (Supabase Realtime → 모바일 구독)
|
||||||
|
- [ ] 오디오 원본 클라우드 보관:
|
||||||
|
- Supabase Storage에 영구 저장
|
||||||
|
- 모든 기기에서 재생 가능
|
||||||
|
- [ ] 결과 화면:
|
||||||
|
- 전사 원본 텍스트 뷰어 (화자별 색상 구분)
|
||||||
|
- AI 요약 (구조화된 마크다운)
|
||||||
|
- 전사 원본 + 요약 + 회의록 전체 편집 가능
|
||||||
|
- 복사/공유 기능
|
||||||
|
- [ ] 히스토리 목록 (최근 전사 기록)
|
||||||
|
- [ ] 푸시 알림: 전사 완료 시 알림
|
||||||
|
|
||||||
|
**산출물:** 녹음 → 업로드 → 전사 → AI 요약 → 알림 → 결과 조회 풀 플로우
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase M-4: 회의 모드 (3~4일)
|
||||||
|
|
||||||
|
**목표:** 장시간 회의 녹음 + 실시간 전사 + 회의록 자동 생성
|
||||||
|
|
||||||
|
- [ ] 회의 세션 생성/관리:
|
||||||
|
- 회의 제목, 참석자 입력
|
||||||
|
- 회의 시작/종료
|
||||||
|
- 회의 중 메모 추가
|
||||||
|
- [ ] 장시간 녹음 엔진:
|
||||||
|
- 백그라운드 녹음 (iOS: background audio mode, Android: foreground service)
|
||||||
|
- N분 단위 청크 분할 + 순차 업로드
|
||||||
|
- 녹음 중 배터리/저장공간 모니터링
|
||||||
|
- [ ] 실시간 전사 표시:
|
||||||
|
- 청크별 전사 결과를 Supabase Realtime으로 수신
|
||||||
|
- 스크롤 가능한 전사 타임라인
|
||||||
|
- 타임스탬프 표시
|
||||||
|
- [ ] 회의록 자동 생성:
|
||||||
|
- 회의 종료 후 전체 전사 병합
|
||||||
|
- Claude API로 회의록 생성 (요약/결정사항/액션아이템/참석자별 발언)
|
||||||
|
- 다중 문서 템플릿 (회의록/요약/이메일 초안)
|
||||||
|
- [ ] 회의 상세 화면:
|
||||||
|
- 탭: 전사 원본 / 회의록 / 문서
|
||||||
|
- 마크다운 렌더러 + 편집기
|
||||||
|
- 내보내기 (PDF/DOCX/클립보드)
|
||||||
|
- [ ] 회의 목록 고도화 (검색, 필터, 정렬)
|
||||||
|
|
||||||
|
**산출물:** 완전한 회의 모드 — 녹음부터 회의록 생성까지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase M-5: AI 음성 대화 (2~3일)
|
||||||
|
|
||||||
|
**목표:** ChatGPT Voice 스타일 음성 AI 대화 기능
|
||||||
|
|
||||||
|
- [ ] 대화 UI:
|
||||||
|
- 음성 대화 모드 화면 (전용)
|
||||||
|
- 말하기 버튼 (Push-to-talk) + 자동 감지 모드
|
||||||
|
- 대화 히스토리 표시 (말풍선)
|
||||||
|
- AI 응답 중 WaveBars 애니메이션
|
||||||
|
- [ ] 음성 파이프라인:
|
||||||
|
- 짧은 녹음 → stt-proxy → 텍스트
|
||||||
|
- 텍스트 → llm-proxy (Claude) → 응답 스트리밍
|
||||||
|
- 응답 → tts-proxy → 음성 합성 → 재생
|
||||||
|
- [ ] Edge Function 추가:
|
||||||
|
- tts-proxy: Google Cloud TTS 또는 Edge TTS
|
||||||
|
- 음성 선택 (남/여, 언어별)
|
||||||
|
- [ ] 대화 컨텍스트 유지 (세션 기반)
|
||||||
|
- [ ] 커스텀 인스트럭션 (시스템 프롬프트 설정)
|
||||||
|
- [ ] 대화 기록 저장 + 검색
|
||||||
|
|
||||||
|
**산출물:** 음성으로 AI와 대화, 텍스트+음성 모두 지원
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase M-6: 파일 전사 + 오프라인 캐시 (2~3일)
|
||||||
|
|
||||||
|
**목표:** 외부 오디오/동영상 파일 업로드 전사 + 오프라인 대응
|
||||||
|
|
||||||
|
- [ ] 파일 업로드 전사:
|
||||||
|
- 기기 갤러리/파일에서 오디오/비디오 선택
|
||||||
|
- 지원 포맷: m4a, mp3, wav, mp4, mov, webm
|
||||||
|
- 대용량 파일 분할 업로드 (청크)
|
||||||
|
- 백그라운드 처리 + 푸시 알림
|
||||||
|
- [ ] 오프라인 캐시:
|
||||||
|
- 최근 N개 전사 결과 로컬 캐시 (AsyncStorage / MMKV)
|
||||||
|
- 오프라인에서 캐시된 결과 조회 가능
|
||||||
|
- 오프라인 녹음 → 온라인 복귀 시 자동 업로드 큐
|
||||||
|
- [ ] 네트워크 상태 감지 + UI 표시
|
||||||
|
|
||||||
|
**산출물:** 파일 전사 기능 + 기본 오프라인 지원
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase M-7: 팀 기능 (2~3일)
|
||||||
|
|
||||||
|
**목표:** 팀 공유 회의록 + 협업 기능
|
||||||
|
|
||||||
|
- [ ] 팀 관리:
|
||||||
|
- 팀 생성/편집/삭제
|
||||||
|
- 팀원 초대 (이메일/링크)
|
||||||
|
- 팀원 역할 관리 (admin/member/viewer)
|
||||||
|
- [ ] 팀 회의록 공유:
|
||||||
|
- 회의록을 팀에 공유
|
||||||
|
- 팀 전용 회의 목록 (RLS 기반)
|
||||||
|
- 팀원 간 코멘트
|
||||||
|
- [ ] 팀 대시보드:
|
||||||
|
- 팀 전체 회의 통계
|
||||||
|
- 최근 활동 피드
|
||||||
|
- [ ] Supabase RLS 정책 확장:
|
||||||
|
- team_members 기반 접근 제어
|
||||||
|
- 팀 내 문서 CRUD 권한
|
||||||
|
|
||||||
|
**산출물:** 팀 단위 회의록 공유/협업
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase M-8: 데스크톱↔모바일 크로스 플랫폼 연동 (2~3일)
|
||||||
|
|
||||||
|
**목표:** 핵심 차별화 — 플랫폼 간 완벽한 데이터 연동
|
||||||
|
|
||||||
|
- [ ] 크로스 플랫폼 동기화 검증:
|
||||||
|
- 데스크톱에서 녹음 → 모바일에서 조회/편집
|
||||||
|
- 모바일에서 녹음 → 데스크톱/웹에서 조회/편집
|
||||||
|
- 설정 동기화 (커스텀 사전, 인스트럭션 등)
|
||||||
|
- [ ] 기기 관리:
|
||||||
|
- 연결된 기기 목록 (모바일/데스크톱/웹)
|
||||||
|
- 기기별 마지막 활동 시각
|
||||||
|
- 기기 연결 해제
|
||||||
|
- [ ] 알림 동기화:
|
||||||
|
- 데스크톱에서 처리 완료 → 모바일 푸시
|
||||||
|
- 모바일에서 업로드 완료 → 데스크톱 알림
|
||||||
|
- [ ] 딥링크:
|
||||||
|
- 웹/데스크톱에서 "모바일에서 보기" → 앱 딥링크
|
||||||
|
- 공유 링크 → 앱 열기
|
||||||
|
|
||||||
|
**산출물:** 어디서 녹음해도 모든 기기에서 동일 경험
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase M-9: 인앱결제 + 구독 통합 (2~3일)
|
||||||
|
|
||||||
|
**목표:** 모바일 인앱결제와 기존 Payple 구독 통합
|
||||||
|
|
||||||
|
- [ ] 인앱결제 (IAP):
|
||||||
|
- iOS: StoreKit 2 (expo-in-app-purchases)
|
||||||
|
- Android: Google Play Billing (expo-in-app-purchases)
|
||||||
|
- Pro / Pro+ 구독 상품 등록
|
||||||
|
- [ ] 구독 통합:
|
||||||
|
- 모바일 IAP ↔ Supabase subscriptions 동기화
|
||||||
|
- 기존 Payple(웹) 구독자는 모바일에서 자동 인식
|
||||||
|
- 모바일에서 결제한 구독도 웹/데스크톱에서 유효
|
||||||
|
- 중복 결제 방지 로직
|
||||||
|
- [ ] 구독 관리 UI:
|
||||||
|
- 현재 플랜 표시
|
||||||
|
- 업그레이드/다운그레이드
|
||||||
|
- 결제 히스토리
|
||||||
|
- 사용량 (전사 시간, AI 호출 수)
|
||||||
|
- [ ] 무료 사용량 관리:
|
||||||
|
- Free: 월 5시간 전사, 월 50회 AI
|
||||||
|
- 사용량 초과 시 업그레이드 프롬프트
|
||||||
|
|
||||||
|
**산출물:** 모바일 인앱결제 + 크로스 플랫폼 구독 통합
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase M-10: 모바일 특화 기능 (2~3일)
|
||||||
|
|
||||||
|
**목표:** 모바일만의 강점을 살린 특화 기능
|
||||||
|
|
||||||
|
- [ ] 백그라운드 녹음 고도화:
|
||||||
|
- iOS: Background Audio + Silent Push
|
||||||
|
- Android: Foreground Service + Notification
|
||||||
|
- 백그라운드에서 장시간 녹음 안정성
|
||||||
|
- 배터리 최적화 (doze mode 대응)
|
||||||
|
- [ ] 홈 위젯:
|
||||||
|
- iOS: WidgetKit (Expo Widgets)
|
||||||
|
- Android: App Widget
|
||||||
|
- 위젯 기능: 빠른 녹음 시작, 최근 전사 요약, 사용량 표시
|
||||||
|
- [ ] 푸시 알림 고도화:
|
||||||
|
- 전사 완료 알림
|
||||||
|
- 팀 코멘트 알림
|
||||||
|
- 주간 리포트 알림
|
||||||
|
- 알림 설정 (카테고리별 on/off)
|
||||||
|
- [ ] 퀵 액션:
|
||||||
|
- 3D Touch / Long Press → 빠른 녹음 시작
|
||||||
|
- 공유 시트 → 오디오 파일 전사
|
||||||
|
- Siri Shortcuts / Google Assistant 연동 (선택)
|
||||||
|
|
||||||
|
**산출물:** 모바일 네이티브 경험 극대화
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase M-11: 품질 + 스토어 출시 (3~4일)
|
||||||
|
|
||||||
|
**목표:** 앱스토어 심사 통과 + 안정적 출시
|
||||||
|
|
||||||
|
- [ ] 품질 보증:
|
||||||
|
- E2E 테스트 (Detox / Maestro)
|
||||||
|
- 성능 프로파일링 (메모리, 배터리, 네트워크)
|
||||||
|
- 크래시 리포팅 (Sentry)
|
||||||
|
- 분석 (Amplitude / Mixpanel)
|
||||||
|
- [ ] 앱스토어 준비:
|
||||||
|
- iOS: App Store Connect 세팅
|
||||||
|
- 스크린샷 (6.7", 6.5", 5.5", iPad)
|
||||||
|
- 앱 설명/키워드 (12개 언어)
|
||||||
|
- 개인정보 처리방침
|
||||||
|
- 앱 심사 가이드라인 준수 확인
|
||||||
|
- Android: Google Play Console 세팅
|
||||||
|
- 스크린샷 + 기능 그래픽
|
||||||
|
- 스토어 등록 정보 (12개 언어)
|
||||||
|
- 데이터 안전 양식
|
||||||
|
- 콘텐츠 등급
|
||||||
|
- [ ] EAS Build + Submit 파이프라인:
|
||||||
|
- eas build → iOS .ipa + Android .aab
|
||||||
|
- eas submit → 각 스토어 자동 제출
|
||||||
|
- OTA 업데이트 설정 (expo-updates)
|
||||||
|
- [ ] 랜딩 페이지 업데이트:
|
||||||
|
- 모바일 앱 다운로드 링크 추가
|
||||||
|
- 앱스토어 배지 (App Store / Google Play)
|
||||||
|
- 모바일 스크린샷 섹션
|
||||||
|
|
||||||
|
**산출물:** iOS App Store + Google Play 출시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 일정 요약
|
||||||
|
|
||||||
|
| Phase | 이름 | 기간 | 누적 |
|
||||||
|
|-------|------|------|------|
|
||||||
|
| M-1 | 프로젝트 기반 정비 | 1~2일 | 2일 |
|
||||||
|
| M-2 | D3RO 디자인 시스템 | 2~3일 | 5일 |
|
||||||
|
| M-3 | 녹음→전사→요약 핵심 | 3~4일 | 9일 |
|
||||||
|
| M-4 | 회의 모드 | 3~4일 | 13일 |
|
||||||
|
| M-5 | AI 음성 대화 | 2~3일 | 16일 |
|
||||||
|
| M-6 | 파일 전사 + 오프라인 | 2~3일 | 19일 |
|
||||||
|
| M-7 | 팀 기능 | 2~3일 | 22일 |
|
||||||
|
| M-8 | 크로스 플랫폼 연동 | 2~3일 | 25일 |
|
||||||
|
| M-9 | 인앱결제 + 구독 | 2~3일 | 28일 |
|
||||||
|
| M-10 | 모바일 특화 기능 | 2~3일 | 31일 |
|
||||||
|
| M-11 | 품질 + 스토어 출시 | 3~4일 | 35일 |
|
||||||
|
|
||||||
|
**총 예상: 약 5주 (35일)**
|
||||||
|
|
||||||
|
> 바이브코딩(Claude Code) 기준. 하루 평균 4~6시간 작업 가정.
|
||||||
|
> 병렬 작업 불가능한 순차 의존 관계 반영.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. UI 디자인 스펙 (확정)
|
||||||
|
|
||||||
|
> 4개 주요 화면 + 녹음 FAB 디자인이 확정됨
|
||||||
|
> HTML 원본: /tmp/d3ro_mobile_*.html 참조
|
||||||
|
|
||||||
|
### 디자인 레퍼런스 파일
|
||||||
|
|
||||||
|
> 모든 화면의 정확한 HTML/Tailwind 원본이 아래에 저장되어 있음.
|
||||||
|
> 새 세션에서 구현 시 반드시 이 파일들을 참조할 것.
|
||||||
|
|
||||||
|
| 화면 | 파일 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| Dashboard | `docs/v3/designs/dashboard.html` | DASH 탭 — 세션 통계, 사용량, 시스템 상태 |
|
||||||
|
| History | `docs/v3/designs/history.html` | HIST 탭 — 전사 기록 목록, 필터(전체/즐겨찾기/처리중) |
|
||||||
|
| Recording | `docs/v3/designs/recording.html` | 녹음 중 화면 — WaveBars, 실시간 전사, 정지 버튼 |
|
||||||
|
| Talk | `docs/v3/designs/talk.html` | TALK 탭 — AI 채팅, 메시지 입력, 리스닝 인디케이터 |
|
||||||
|
| Settings | `docs/v3/designs/settings.html` | SET 탭 — 계정, 백엔드, 환경설정, 로그아웃 |
|
||||||
|
| Upgrade | `docs/v3/designs/upgrade.html` | 구독 업그레이드 — FREE/PREMIUM 비교, 쿼터 프로그레스바 |
|
||||||
|
| Login | `docs/v3/designs/login.html` | 로그인 — LED 로고, OAuth 3종, 이메일/비번, 회원가입 링크 |
|
||||||
|
|
||||||
|
### 6.0 디자인 토큰 (모바일)
|
||||||
|
|
||||||
|
```
|
||||||
|
colors:
|
||||||
|
appBg: '#19191b' — 전체 배경
|
||||||
|
panelBg: '#242427' — 카드/패널 배경
|
||||||
|
insetBg: '#0f0f11' — 인셋 패널 (더 깊은 배경)
|
||||||
|
brandOrange:'#ff5c35' — 주 액센트 (활성 탭, CTA, 숫자)
|
||||||
|
brandGreen: '#4ade80' — 상태 LED (활성, 연결됨)
|
||||||
|
textMuted: '#71717a' — 보조 텍스트
|
||||||
|
textMain: '#d4d4d8' — 기본 텍스트
|
||||||
|
borderColor:'#2e2e32' — 카드/패널 테두리
|
||||||
|
|
||||||
|
fontFamily:
|
||||||
|
sans: system (SF Pro / Roboto)
|
||||||
|
mono: ui-monospace (SFMono / Menlo / Monaco)
|
||||||
|
|
||||||
|
shadows:
|
||||||
|
LED glow: '0 0 8px #ff5c35' (orange), '0 0 5px #4ade80' (green)
|
||||||
|
Record FAB: '0 0 20px rgba(255,92,53,0.4)'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.1 탭 구성 (5개)
|
||||||
|
|
||||||
|
| 탭 | 라벨 | 아이콘 | 설명 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| DASH | 대시보드 | 4-grid squares | 세션 통계, 사용량, 시스템 상태 |
|
||||||
|
| HIST | 히스토리 | clock circle | 전사 기록 목록 (필터: 전체/즐겨찾기/처리중) |
|
||||||
|
| REC | 녹음 | microphone (FAB) | 중앙 플로팅 버튼, 탭=녹음, 롱프레스=회의모드 |
|
||||||
|
| TALK | AI 대화 | chat bubble | 텍스트+음성 AI 채팅 |
|
||||||
|
| SET | 설정 | gear | 계정, 백엔드, 환경설정 |
|
||||||
|
|
||||||
|
### 6.2 DASH (대시보드) 화면
|
||||||
|
|
||||||
|
```
|
||||||
|
구조:
|
||||||
|
├── Header: LED(orange) + "D3RO-VOICE" + version + LEDs
|
||||||
|
├── InsetPanel (세션 개요):
|
||||||
|
│ ├── dot grid 배경 (radial-gradient, opacity 3%)
|
||||||
|
│ ├── 오늘 세션 카운터 (대형 font-mono 숫자)
|
||||||
|
│ ├── 가이드 텍스트 (⌘1을 눌러 녹음 시작)
|
||||||
|
│ └── 하단: 단어 수 / 연속 일수
|
||||||
|
├── 2-Button Row: [통계] [시스템]
|
||||||
|
├── 2x2 Grid Cards: 녹음시간 / 단어 / 오늘세션 / 연속일
|
||||||
|
├── Info Cards: 현재 백엔드 (CLAUDE) / 현재 등급 (FREE)
|
||||||
|
├── Usage Panel: 받아쓰기(무제한) / LLM처리(무제한) / 프리미엄쿼터(250/주간)
|
||||||
|
└── Status Bar: "OLLAMA GEMMA4:E4B" + "PRECISION DATA LINK"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 HIST (히스토리) 화면
|
||||||
|
|
||||||
|
```
|
||||||
|
구조:
|
||||||
|
├── Header: LED(orange) + "HISTORY" + LED(green)
|
||||||
|
├── Filter Chips: [전체(active)] [즐겨찾기] [처리중]
|
||||||
|
├── Date Group "TODAY":
|
||||||
|
│ ├── HistoryCard:
|
||||||
|
│ │ ├── LED(green) + 시각(14:32) + 단어수(42 W)
|
||||||
|
│ │ ├── 전사 텍스트 (2줄 클램프)
|
||||||
|
│ │ └── 메타: 녹음시간(0:45) + 백엔드(CLAUDE)
|
||||||
|
│ └── HistoryCard: ...
|
||||||
|
├── Date Group "YESTERDAY": (opacity 70%)
|
||||||
|
│ └── HistoryCard: LED(gray, 완료) + ...
|
||||||
|
└── Status Bar
|
||||||
|
|
||||||
|
히스토리 카드 특징:
|
||||||
|
- 오늘 항목: LED green, 전체 opacity
|
||||||
|
- 이전 항목: LED gray, opacity 70%
|
||||||
|
- 단어수 배지: brandOrange/10 배경
|
||||||
|
- 백엔드 표시: CLAUDE / LOCAL
|
||||||
|
- 즐겨찾기 아이콘 (bookmark)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 TALK (AI 대화) 화면
|
||||||
|
|
||||||
|
```
|
||||||
|
구조:
|
||||||
|
├── Header: LED(orange) + "TALK" + "LIVE" + LED(green, pulse)
|
||||||
|
├── Chat Area:
|
||||||
|
│ ├── Date Badge: "TODAY, 9:30 AM" (pill)
|
||||||
|
│ ├── AI Bubble (왼쪽): panelBg + border, rounded-tl-sm
|
||||||
|
│ │ └── 하단 라벨: "CLAUDE"
|
||||||
|
│ ├── User Bubble (오른쪽): brandOrange/10 + border, rounded-tr-sm
|
||||||
|
│ │ └── 하단 라벨: "YOU"
|
||||||
|
│ └── Listening Indicator: 3개 bounce dots
|
||||||
|
├── Input Area:
|
||||||
|
│ ├── dot grid 배경 (radial-gradient, orange, 10%)
|
||||||
|
│ ├── [첨부] + text input + [전송]
|
||||||
|
│ └── Status Bar: "OLLAMA GEMMA4:E4B"
|
||||||
|
└── Tab Bar
|
||||||
|
|
||||||
|
채팅 버블 특징:
|
||||||
|
- AI: bg-panelBg, rounded-2xl rounded-tl-sm
|
||||||
|
- User: bg-brandOrange/10, border-brandOrange/30, rounded-tr-sm
|
||||||
|
- AI 텍스트: textMain, User 텍스트: brandOrange
|
||||||
|
- 리스닝 상태: 3개 dot bounce 애니메이션
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 SET (설정) 화면
|
||||||
|
|
||||||
|
```
|
||||||
|
구조:
|
||||||
|
├── Header: "설정" (큰 제목, 다른 탭과 다른 스타일)
|
||||||
|
├── Section "ACCOUNT & PLAN":
|
||||||
|
│ ├── 프로필: 아바타(이니셜) + 이름 + 이메일 + [EDIT]
|
||||||
|
│ └── 현재 등급: LED(orange) + "FREE"
|
||||||
|
├── Section "BACKEND CONFIG":
|
||||||
|
│ ├── LLM 모델 선택: "CLAUDE" + chevron
|
||||||
|
│ └── 로컬 STT 엔진: toggle switch (checked)
|
||||||
|
├── Section "PREFERENCES":
|
||||||
|
│ ├── 주 언어: "한국어" + chevron
|
||||||
|
│ ├── 자동 교정: toggle switch (checked)
|
||||||
|
│ └── 햅틱 피드백: toggle switch (checked)
|
||||||
|
├── [로그아웃] 버튼
|
||||||
|
└── Status: "OLLAMA GEMMA4:E4B"
|
||||||
|
|
||||||
|
토글 스위치 스타일:
|
||||||
|
- off: bg-insetBg
|
||||||
|
- on: bg-brandOrange
|
||||||
|
- 크기: w-9 h-5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.6 녹음 버튼 (FAB) 상태
|
||||||
|
|
||||||
|
```
|
||||||
|
비활성 (다른 탭 활성):
|
||||||
|
bg-panelBg, border-appBg, text-textMuted
|
||||||
|
shadow 없음
|
||||||
|
|
||||||
|
활성 (DASH 탭 — 녹음 준비):
|
||||||
|
bg-brandOrange, border-appBg, text-appBg
|
||||||
|
shadow: 0 0 20px rgba(255,92,53,0.4)
|
||||||
|
|
||||||
|
크기: w-14 h-14, rounded-full
|
||||||
|
위치: 탭바 중앙, -top-5 (탭바 위로 돌출)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.7 REC (녹음 중) 화면
|
||||||
|
|
||||||
|
```
|
||||||
|
구조:
|
||||||
|
├── Header: LED(orange, pulse) + "REC_SESSION" + 타이머(03:24) + LED(green)
|
||||||
|
├── WaveBars Panel (h-32):
|
||||||
|
│ ├── bg-insetBg + dot grid 배경
|
||||||
|
│ └── 11개 오렌지 wave bars (5종 애니메이션: wave-1~5)
|
||||||
|
├── Transcript Area (scrollable):
|
||||||
|
│ ├── 초기화 로그 (font-mono, textMuted)
|
||||||
|
│ │ "> initializing voice recording protocol..."
|
||||||
|
│ ├── 전사된 텍스트 (text-lg, textMain)
|
||||||
|
│ └── 현재 입력 중: brandOrange 텍스트 + 깜빡이는 커서
|
||||||
|
├── Status: LED(orange, pulse) + "LISTENING_" + "REAL-TIME TRANSCRIPT"
|
||||||
|
└── Tab Bar:
|
||||||
|
├── 다른 탭 모두 opacity-50 + cursor-not-allowed (비활성)
|
||||||
|
└── 중앙 버튼: bg-panelBg + border-brandOrange + STOP 아이콘(사각형)
|
||||||
|
+ ping 애니메이션 (border-brandOrange, opacity 20%)
|
||||||
|
|
||||||
|
녹음 중 특징:
|
||||||
|
- 다른 탭 접근 불가 (opacity 50%, cursor-not-allowed)
|
||||||
|
- FAB 버튼이 STOP 모양(사각형)으로 변경
|
||||||
|
- STOP 버튼에 ping 애니메이션
|
||||||
|
- Wave animations: 5종 (0.8s ~ 1.5s ease-in-out infinite)
|
||||||
|
keyframes: '0%,100%': h=8px, '50%': h=32px
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.8 Upgrade (구독) 화면
|
||||||
|
|
||||||
|
```
|
||||||
|
구조:
|
||||||
|
├── Header: LED(orange) + "SUBSCRIPTION" + [X 닫기 버튼]
|
||||||
|
├── Title: "Unlock Full Potential" + 설명
|
||||||
|
├── Current Plan Card (panelBg):
|
||||||
|
│ ├── "CURRENT PLAN" + "FREE" + "$0 / mo"
|
||||||
|
│ ├── 기본 음성 인식: 무제한
|
||||||
|
│ ├── 로컬 AI (Gemma): 무제한
|
||||||
|
│ └── 프리미엄 쿼터: 250/500 + progress bar (brandGreen, 50%)
|
||||||
|
├── Premium Plan Card (insetBg + brandOrange border):
|
||||||
|
│ ├── 우상단 orange glow blur 효과
|
||||||
|
│ ├── "UPGRADE PLAN" + "PREMIUM" + "$15 / mo"
|
||||||
|
│ ├── 체크리스트 (brandOrange 체크 아이콘):
|
||||||
|
│ │ - Claude 3.5 Sonnet / Opus 무제한
|
||||||
|
│ │ - GPT-4o 외부 API 연동
|
||||||
|
│ │ - 클라우드 동기화 + 무제한 히스토리
|
||||||
|
│ │ - 초정밀 오디오 향상 필터
|
||||||
|
│ └── [UPGRADE NOW] 버튼 (bg-brandOrange, full-width)
|
||||||
|
└── Footer: "이용 약관 및 환불 정책 보기"
|
||||||
|
|
||||||
|
특징:
|
||||||
|
- 탭 바 없음 (모달 스타일, X 버튼으로 닫기)
|
||||||
|
- Premium 카드에 glow 효과: inset shadow + blur blob
|
||||||
|
- Progress bar: h-1.5, bg-brandGreen, rounded-full
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.9 공통 패턴
|
||||||
|
|
||||||
|
```
|
||||||
|
Status Bar (모든 탭 하단):
|
||||||
|
LED(green) + "OLLAMA GEMMA4:E4B" + "PRECISION DATA LINK"
|
||||||
|
text-[8px] font-mono tracking-wider
|
||||||
|
|
||||||
|
Home Indicator:
|
||||||
|
w-[120px] h-1 bg-white/30 rounded-full
|
||||||
|
absolute bottom-2 center
|
||||||
|
|
||||||
|
Header LED Pattern:
|
||||||
|
활성 탭: w-2 h-2 bg-brandOrange + shadow glow
|
||||||
|
연결 상태: w-1.5 h-1.5 bg-brandGreen + shadow glow
|
||||||
|
|
||||||
|
Card Pattern:
|
||||||
|
bg-panelBg border border-borderColor rounded-xl p-4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 기술 상세
|
||||||
|
|
||||||
|
### 7.1 Expo 설정
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "D3RO Voice",
|
||||||
|
"slug": "d3ro-voice",
|
||||||
|
"scheme": "d3ro-voice",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"platforms": ["ios", "android"],
|
||||||
|
"plugins": [
|
||||||
|
"expo-av",
|
||||||
|
"expo-notifications",
|
||||||
|
"expo-secure-store",
|
||||||
|
"expo-file-system",
|
||||||
|
"expo-document-picker",
|
||||||
|
"expo-background-fetch",
|
||||||
|
"expo-task-manager",
|
||||||
|
["expo-in-app-purchases", {}]
|
||||||
|
],
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true,
|
||||||
|
"infoPlist": {
|
||||||
|
"NSMicrophoneUsageDescription": "녹음 및 음성 전사를 위해 마이크 접근이 필요합니다",
|
||||||
|
"UIBackgroundModes": ["audio", "fetch", "remote-notification"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"permissions": [
|
||||||
|
"RECORD_AUDIO",
|
||||||
|
"FOREGROUND_SERVICE",
|
||||||
|
"FOREGROUND_SERVICE_MICROPHONE",
|
||||||
|
"POST_NOTIFICATIONS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 주요 의존성
|
||||||
|
|
||||||
|
```
|
||||||
|
# 코어
|
||||||
|
expo ~51.x
|
||||||
|
react-native ~0.74.x
|
||||||
|
@d3ro/core, @d3ro/api-client, @d3ro/i18n, @d3ro/ui-native
|
||||||
|
|
||||||
|
# 오디오
|
||||||
|
expo-av — 녹음/재생
|
||||||
|
expo-file-system — 파일 관리
|
||||||
|
expo-document-picker — 파일 선택
|
||||||
|
|
||||||
|
# 네트워크
|
||||||
|
@supabase/supabase-js — DB + Auth + Storage + Realtime
|
||||||
|
|
||||||
|
# 모바일 특화
|
||||||
|
expo-notifications — 푸시 알림
|
||||||
|
expo-task-manager — 백그라운드 작업
|
||||||
|
expo-background-fetch — 백그라운드 페치
|
||||||
|
expo-in-app-purchases — IAP
|
||||||
|
expo-secure-store — 토큰 저장
|
||||||
|
@react-native-async-storage/async-storage — 캐시
|
||||||
|
|
||||||
|
# UI
|
||||||
|
react-native-reanimated — 애니메이션
|
||||||
|
react-native-gesture-handler — 제스처
|
||||||
|
react-native-safe-area-context
|
||||||
|
expo-linear-gradient — 그라데이션
|
||||||
|
react-native-markdown-display — 마크다운 렌더링
|
||||||
|
|
||||||
|
# 품질
|
||||||
|
sentry-expo — 크래시 리포팅
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 네비게이션 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── _layout.tsx (Root: AuthProvider + Theme)
|
||||||
|
├── index.tsx (Entry: 세션 체크 → 리다이렉트)
|
||||||
|
├── login.tsx (OAuth 로그인)
|
||||||
|
├── (tabs)/
|
||||||
|
│ ├── _layout.tsx (Tab Bar: D3RO 스타일)
|
||||||
|
│ ├── record.tsx (녹음 탭 — 메인 CTA)
|
||||||
|
│ ├── meetings.tsx (회의 목록)
|
||||||
|
│ ├── chat.tsx (AI 음성 대화)
|
||||||
|
│ └── profile.tsx (프로필 + 설정 + 구독)
|
||||||
|
├── meeting/
|
||||||
|
│ └── [id].tsx (회의 상세: 전사/회의록/문서 탭)
|
||||||
|
├── history/
|
||||||
|
│ └── [id].tsx (전사 상세)
|
||||||
|
├── team/
|
||||||
|
│ ├── index.tsx (팀 목록)
|
||||||
|
│ ├── [id].tsx (팀 상세)
|
||||||
|
│ └── invite.tsx (팀 초대 수락)
|
||||||
|
├── settings/
|
||||||
|
│ ├── index.tsx (설정 메인)
|
||||||
|
│ ├── subscription.tsx (구독 관리)
|
||||||
|
│ ├── devices.tsx (기기 관리)
|
||||||
|
│ └── dictionary.tsx (커스텀 사전)
|
||||||
|
└── file-transcribe.tsx (파일 업로드 전사)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4 Edge Function 추가/확장 목록
|
||||||
|
|
||||||
|
| 함수 | 용도 | Phase |
|
||||||
|
|------|------|-------|
|
||||||
|
| stt-proxy (확장) | 긴 오디오 분할 + 진행 상태 업데이트 | M-3 |
|
||||||
|
| ai-process (신규) | 전사→요약/회의록/액션아이템 | M-3 |
|
||||||
|
| tts-proxy (신규) | 텍스트→음성 합성 | M-5 |
|
||||||
|
| iap-webhook (신규) | Apple/Google IAP 서버 검증 | M-9 |
|
||||||
|
| send-push (확장) | 전사 완료/팀 코멘트 알림 | M-3 |
|
||||||
|
|
||||||
|
### 7.5 Supabase 테이블 추가/확장
|
||||||
|
|
||||||
|
| 테이블 | 변경 | Phase |
|
||||||
|
|--------|------|-------|
|
||||||
|
| user_settings | 신규 — 크로스 플랫폼 설정 동기화 | M-8 |
|
||||||
|
| devices | 신규 — 기기 목록 관리 | M-8 |
|
||||||
|
| push_tokens | 확장 — device_type 컬럼 추가 | M-3 |
|
||||||
|
| audio_files | 신규 — 업로드 오디오 메타데이터 | M-3 |
|
||||||
|
| processing_jobs | 신규 — 비동기 처리 상태 추적 | M-3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 경쟁사 대비 차별화
|
||||||
|
|
||||||
|
| 기능 | Clova Note | Otter.ai | D3RO Voice |
|
||||||
|
|------|-----------|----------|------------|
|
||||||
|
| 전사 | ✅ | ✅ | ✅ |
|
||||||
|
| AI 요약 | ⚠️ 기본 | ✅ | ✅ Claude 기반 고품질 |
|
||||||
|
| 회의록 자동 생성 | ❌ | ⚠️ | ✅ 다중 템플릿 |
|
||||||
|
| 데스크톱 연동 | ❌ | ⚠️ 웹만 | ✅ 완벽한 크로스 플랫폼 |
|
||||||
|
| 로컬 처리 (데스크톱) | ❌ | ❌ | ✅ 완전 무료 |
|
||||||
|
| AI 음성 대화 | ❌ | ❌ | ✅ |
|
||||||
|
| 디자인 차별화 | 기본 | 기본 | ✅ 인스트루먼트 미학 |
|
||||||
|
| 다국어 | 한/영/일 | 영 중심 | ✅ 12개 언어 |
|
||||||
|
| 팀 협업 | ❌ | ✅ | ✅ |
|
||||||
|
| 가격 (월) | ₩16,900 | $16.99 | ₩9,900 (Pro) |
|
||||||
|
|
||||||
|
**핵심 차별화:**
|
||||||
|
1. **크로스 플랫폼 시너지** — 데스크톱(무료 로컬) + 모바일(클라우드) 연동
|
||||||
|
2. **AI 후처리 품질** — Claude API 기반 회의록/요약/액션아이템
|
||||||
|
3. **디자인** — D3RO 인스트루먼트 미학 (경쟁사 대비 압도적 브랜딩)
|
||||||
|
4. **가격** — 데스크톱은 완전 무료(로컬), 모바일 Pro ₩9,900/월
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 리스크 및 완화
|
||||||
|
|
||||||
|
| 리스크 | 영향 | 완화 |
|
||||||
|
|--------|------|------|
|
||||||
|
| Google STT 비용 ($0.006/15초) | 서버 비용 증가 | Free 티어 월 5시간 제한, Pro 이상 무제한 |
|
||||||
|
| Claude API 비용 ($3/1M input) | 요약 비용 | Haiku 사용 + 프롬프트 최적화 |
|
||||||
|
| iOS 백그라운드 녹음 제한 | 장시간 녹음 중단 | UIBackgroundModes audio + Silent Push |
|
||||||
|
| 앱스토어 IAP 수수료 30% | 수익 감소 | 웹 결제(Payple) 유도 + 가격 조정 |
|
||||||
|
| Expo 네이티브 모듈 제한 | 기능 구현 한계 | EAS Build (Custom Dev Client) |
|
||||||
|
| 대용량 오디오 업로드 | 네트워크 비용 | 압축 (AAC) + 청크 업로드 + 캐시 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 성공 지표 (KPI)
|
||||||
|
|
||||||
|
| 지표 | 목표 (출시 3개월) |
|
||||||
|
|------|------------------|
|
||||||
|
| 다운로드 | 10,000+ |
|
||||||
|
| DAU | 1,000+ |
|
||||||
|
| 전환율 (Free→Pro) | 5%+ |
|
||||||
|
| 평균 세션 길이 | 15분+ |
|
||||||
|
| 앱스토어 평점 | 4.5+ |
|
||||||
|
| 크래시율 | < 0.5% |
|
||||||
|
| 전사 정확도 만족도 | 4.0/5.0+ |
|
||||||
274
docs/v3/designs/dashboard.html
Normal file
274
docs/v3/designs/dashboard.html
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
<html lang="ko" vid="0"><head vid="1">
|
||||||
|
<meta charset="UTF-8" vid="2">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" vid="3">
|
||||||
|
<title vid="4">D3RO-VOICE Mobile</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com/3.4.17" vid="5"></script>
|
||||||
|
<script vid="6">
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
appBg: '#19191b',
|
||||||
|
panelBg: '#242427',
|
||||||
|
insetBg: '#0f0f11',
|
||||||
|
brandOrange: '#ff5c35',
|
||||||
|
brandGreen: '#4ade80',
|
||||||
|
textMuted: '#71717a',
|
||||||
|
textMain: '#d4d4d8',
|
||||||
|
borderColor: '#2e2e32'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'],
|
||||||
|
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', "Liberation Mono", "Courier New", 'monospace'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style vid="7">
|
||||||
|
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-black min-h-screen flex items-center justify-center p-4" vid="8">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="relative w-[375px] h-[812px] bg-appBg rounded-[40px] shadow-[0_0_50px_rgba(0,0,0,0.5)] overflow-hidden border-[8px] border-[#222] flex flex-col font-sans text-textMain" vid="9">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="h-12 w-full flex justify-between items-end px-6 pb-2 text-[11px] font-medium text-white/90 z-20" vid="10">
|
||||||
|
<span vid="11">9:41</span>
|
||||||
|
<div class="flex items-center gap-1.5" vid="12">
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="13"><path d="M12 20V10" vid="14"></path><path d="M18 20V4" vid="15"></path><path d="M6 20v-4" vid="16"></path></svg>
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="17"><path d="M5 12.55a11 11 0 0 1 14.08 0" vid="18"></path><path d="M1.42 9a16 16 0 0 1 21.16 0" vid="19"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0" vid="20"></path><line x1="12" y1="20" x2="12.01" y2="20" vid="21"></line></svg>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" vid="22"><path d="M2 12C2 7.02944 6.02944 3 11 3H13C17.9706 3 22 7.02944 22 12C22 16.9706 17.9706 21 13 21H11C6.02944 21 2 16.9706 2 12Z" vid="23"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<header class="px-5 py-3 flex justify-between items-center z-10" vid="24">
|
||||||
|
<div class="flex items-center gap-2" vid="25">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-brandOrange shadow-[0_0_8px_#ff5c35]" vid="26"></div>
|
||||||
|
<span class="text-[10px] tracking-[0.2em] font-mono text-textMuted" vid="27">D3RO-VOICE</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3" vid="28">
|
||||||
|
<span class="text-[10px] font-mono text-textMuted" vid="29">v1.0.0</span>
|
||||||
|
<div class="flex gap-1.5" vid="30">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandGreen shadow-[0_0_5px_#4ade80]" vid="31"></div>
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandOrange shadow-[0_0_5px_#ff5c35]" vid="32"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
<main class="flex-1 overflow-y-auto no-scrollbar px-5 pb-24" vid="33">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="bg-insetBg rounded-2xl p-5 border border-[#1a1a1c] shadow-[inset_0_4px_20px_rgba(0,0,0,0.5)] relative overflow-hidden mt-2" vid="34">
|
||||||
|
|
||||||
|
<div class="absolute inset-0 opacity-[0.03]" style="background-image: radial-gradient(#fff 1px, transparent 1px); background-size: 16px 16px;" vid="35"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 flex justify-between items-start mb-6" vid="36">
|
||||||
|
<span class="text-xs text-textMuted font-medium" vid="37">세션 개요</span>
|
||||||
|
<span class="text-xs font-mono text-brandOrange opacity-80" vid="38">MON</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative z-10 flex flex-col mb-8" vid="39">
|
||||||
|
<div class="flex items-baseline gap-3" vid="40">
|
||||||
|
<span class="text-6xl font-light font-mono text-brandOrange tracking-tighter" vid="41">0</span>
|
||||||
|
<span class="text-sm text-textMuted" vid="42">오늘 세션</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5 mt-2 text-[10px] text-textMuted font-mono" vid="43">
|
||||||
|
<svg class="w-3 h-3 text-brandOrange" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="44"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7l4-4m0 0l4 4m-4-4v18" vid="45"></path></svg>
|
||||||
|
<span vid="46">⌘1을 눌러 녹음 시작</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative z-10 flex justify-between items-end border-t border-[#1a1a1c] pt-4" vid="47">
|
||||||
|
<div vid="48">
|
||||||
|
<div class="text-[10px] text-textMuted mb-1" vid="49">단어</div>
|
||||||
|
<div class="flex items-baseline gap-1.5" vid="50">
|
||||||
|
<span class="text-xl font-mono text-brandOrange" vid="51">168</span>
|
||||||
|
<span class="text-[10px] text-textMuted" vid="52">전체</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right" vid="53">
|
||||||
|
<div class="text-[10px] text-textMuted mb-1" vid="54">연속</div>
|
||||||
|
<div class="flex items-baseline gap-1.5 justify-end" vid="55">
|
||||||
|
<span class="text-xl font-mono text-brandOrange" vid="56">0</span>
|
||||||
|
<span class="text-[10px] text-textMuted" vid="57">일</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-2 left-0 right-0 text-center z-0" vid="58">
|
||||||
|
<span class="text-[8px] font-mono text-white/5 tracking-[0.3em]" vid="59">LOCAL AI VOICE ASSISTANT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex gap-3 mt-4" vid="60">
|
||||||
|
<button class="flex-1 bg-panelBg border border-borderColor rounded-xl py-3.5 flex items-center justify-center text-sm font-medium hover:bg-[#2a2a2e] transition-colors shadow-sm" vid="61">
|
||||||
|
<span class="text-brandOrange" vid="62">통계</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex-1 bg-panelBg border border-borderColor rounded-xl py-3.5 flex items-center justify-center text-sm font-medium hover:bg-[#2a2a2e] transition-colors shadow-sm" vid="63">
|
||||||
|
<span class="text-textMuted" vid="64">시스템</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-3 mt-4" vid="65">
|
||||||
|
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl p-4 flex flex-col items-center justify-center gap-2" vid="66">
|
||||||
|
<span class="text-[11px] text-textMuted font-medium" vid="67">녹음</span>
|
||||||
|
<div class="flex items-baseline gap-1" vid="68">
|
||||||
|
<span class="text-2xl font-mono text-brandOrange" vid="69">6</span>
|
||||||
|
<span class="text-[10px] text-textMuted font-mono" vid="70">MIN</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl p-4 flex flex-col items-center justify-center gap-2" vid="71">
|
||||||
|
<span class="text-[11px] text-textMuted font-medium" vid="72">단어</span>
|
||||||
|
<div class="flex items-baseline gap-1" vid="73">
|
||||||
|
<span class="text-2xl font-mono text-brandOrange" vid="74">168</span>
|
||||||
|
<span class="text-[10px] text-textMuted" vid="75">전체</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl p-4 flex flex-col items-center justify-center gap-2" vid="76">
|
||||||
|
<span class="text-[11px] text-textMuted font-medium" vid="77">오늘</span>
|
||||||
|
<div class="flex items-baseline gap-1" vid="78">
|
||||||
|
<span class="text-2xl font-mono text-brandOrange" vid="79">0</span>
|
||||||
|
<span class="text-[10px] text-textMuted" vid="80">세션</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl p-4 flex flex-col items-center justify-center gap-2" vid="81">
|
||||||
|
<span class="text-[11px] text-textMuted font-medium" vid="82">연속</span>
|
||||||
|
<div class="flex items-baseline gap-1" vid="83">
|
||||||
|
<span class="text-2xl font-mono text-brandOrange" vid="84">0</span>
|
||||||
|
<span class="text-[10px] text-textMuted" vid="85">일</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3 mt-4" vid="86">
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl p-4 flex justify-between items-center" vid="87">
|
||||||
|
<span class="text-xs text-textMuted font-medium" vid="88">현재 백엔드</span>
|
||||||
|
<div class="flex items-center gap-2" vid="89">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandGreen shadow-[0_0_5px_#4ade80]" vid="90"></div>
|
||||||
|
<span class="text-sm text-brandOrange font-mono tracking-tight" vid="91">프리미엄 (CLAUDE)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl p-4 flex justify-between items-center" vid="92">
|
||||||
|
<span class="text-xs text-textMuted font-medium" vid="93">현재 등급</span>
|
||||||
|
<div class="flex items-center gap-2" vid="94">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandOrange shadow-[0_0_5px_#ff5c35]" vid="95"></div>
|
||||||
|
<span class="text-sm text-brandOrange font-mono tracking-tight" vid="96">FREE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl mt-4 overflow-hidden" vid="97">
|
||||||
|
<div class="px-5 py-4 border-b border-borderColor/50" vid="98">
|
||||||
|
<span class="text-xs text-brandOrange font-medium" vid="99">오늘 사용량</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col" vid="100">
|
||||||
|
<div class="px-5 py-3.5 flex justify-between items-center border-b border-borderColor/50" vid="101">
|
||||||
|
<span class="text-sm text-textMain" vid="102">받아쓰기</span>
|
||||||
|
<span class="text-sm text-brandOrange font-mono" vid="103">무제한</span>
|
||||||
|
</div>
|
||||||
|
<div class="px-5 py-3.5 flex justify-between items-center border-b border-borderColor/50" vid="104">
|
||||||
|
<span class="text-sm text-textMain" vid="105">LLM 처리</span>
|
||||||
|
<span class="text-sm text-brandOrange font-mono" vid="106">무제한</span>
|
||||||
|
</div>
|
||||||
|
<div class="px-5 py-3.5 flex flex-col gap-1.5" vid="107">
|
||||||
|
<span class="text-[11px] text-brandGreen font-medium" vid="108">프리미엄 쿼터</span>
|
||||||
|
<div class="flex justify-between items-center" vid="109">
|
||||||
|
<span class="text-sm text-textMain font-mono" vid="110">Haiku</span>
|
||||||
|
<span class="text-sm text-brandOrange font-mono" vid="111">250/주간</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="absolute bottom-[80px] left-0 w-full px-5 py-2 flex justify-between items-center bg-gradient-to-t from-appBg to-transparent z-20 pointer-events-none" vid="112">
|
||||||
|
<div class="flex items-center gap-1.5" vid="113">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandGreen shadow-[0_0_5px_#4ade80]" vid="114"></div>
|
||||||
|
<span class="text-[8px] font-mono text-textMuted tracking-wider" vid="115">OLLAMA GEMMA4:E4B</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[8px] font-mono text-textMuted tracking-widest opacity-50" vid="116">PRECISION DATA LINK</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="absolute bottom-0 left-0 w-full h-[80px] bg-appBg/95 backdrop-blur-md border-t border-borderColor flex justify-around items-center px-2 pb-5 z-30" vid="117">
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="118">
|
||||||
|
<div class="relative" vid="119">
|
||||||
|
<svg class="w-6 h-6 text-brandOrange" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="120">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1" vid="121"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1" vid="122"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1" vid="123"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1" vid="124"></rect>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="absolute inset-0 bg-brandOrange blur-[10px] opacity-20 rounded-full" vid="125"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[9px] font-mono text-brandOrange font-medium" vid="126">DASH</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="127">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="128">
|
||||||
|
<circle cx="12" cy="12" r="9" vid="129"></circle>
|
||||||
|
<path d="M12 7v5l3 3" vid="130"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="131">HIST</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="relative -top-5" vid="132">
|
||||||
|
<button class="w-14 h-14 rounded-full bg-brandOrange flex items-center justify-center shadow-[0_0_20px_rgba(255,92,53,0.4)] border-4 border-appBg group" vid="133">
|
||||||
|
<svg class="w-6 h-6 text-appBg" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" vid="134">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" vid="135"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="136">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="137">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" vid="138"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="139">TALK</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="140">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="141">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" vid="142"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" vid="143"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="144">SET</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 w-[120px] h-1 bg-white/30 rounded-full z-40" vid="145"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</body></html>
|
||||||
208
docs/v3/designs/history.html
Normal file
208
docs/v3/designs/history.html
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
<html lang="ko" vid="0"><head vid="1">
|
||||||
|
<meta charset="UTF-8" vid="2">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" vid="3">
|
||||||
|
<title vid="4">D3RO-VOICE Mobile - History</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com/3.4.17" vid="5"></script>
|
||||||
|
<script vid="6">
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
appBg: '#19191b',
|
||||||
|
panelBg: '#242427',
|
||||||
|
insetBg: '#0f0f11',
|
||||||
|
brandOrange: '#ff5c35',
|
||||||
|
brandGreen: '#4ade80',
|
||||||
|
textMuted: '#71717a',
|
||||||
|
textMain: '#d4d4d8',
|
||||||
|
borderColor: '#2e2e32'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'],
|
||||||
|
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', "Liberation Mono", "Courier New", 'monospace'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style vid="7">
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-black min-h-screen flex items-center justify-center p-4" vid="8">
|
||||||
|
|
||||||
|
<div class="relative w-[375px] h-[812px] bg-appBg rounded-[40px] shadow-[0_0_50px_rgba(0,0,0,0.5)] overflow-hidden border-[8px] border-[#222] flex flex-col font-sans text-textMain" vid="9">
|
||||||
|
|
||||||
|
<div class="h-12 w-full flex justify-between items-end px-6 pb-2 text-[11px] font-medium text-white/90 z-20" vid="10">
|
||||||
|
<span vid="11">9:41</span>
|
||||||
|
<div class="flex items-center gap-1.5" vid="12">
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="13"><path d="M12 20V10" vid="14"></path><path d="M18 20V4" vid="15"></path><path d="M6 20v-4" vid="16"></path></svg>
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="17"><path d="M5 12.55a11 11 0 0 1 14.08 0" vid="18"></path><path d="M1.42 9a16 16 0 0 1 21.16 0" vid="19"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0" vid="20"></path><line x1="12" y1="20" x2="12.01" y2="20" vid="21"></line></svg>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" vid="22"><path d="M2 12C2 7.02944 6.02944 3 11 3H13C17.9706 3 22 7.02944 22 12C22 16.9706 17.9706 21 13 21H11C6.02944 21 2 16.9706 2 12Z" vid="23"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header class="px-5 py-3 flex justify-between items-center z-10" vid="24">
|
||||||
|
<div class="flex items-center gap-2" vid="25">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-brandOrange shadow-[0_0_8px_#ff5c35]" vid="26"></div>
|
||||||
|
<span class="text-[10px] tracking-[0.2em] font-mono text-textMuted" vid="27">HISTORY</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3" vid="28">
|
||||||
|
<div class="flex gap-1.5" vid="29">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandGreen shadow-[0_0_5px_#4ade80]" vid="30"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="px-5 pb-3 flex gap-3 z-10 border-b border-borderColor/50" vid="31">
|
||||||
|
<button class="bg-brandOrange text-appBg rounded-full px-4 py-1.5 text-xs font-medium" vid="32">전체</button>
|
||||||
|
<button class="bg-panelBg border border-borderColor rounded-full px-4 py-1.5 text-xs font-medium text-textMuted hover:text-textMain" vid="33">즐겨찾기</button>
|
||||||
|
<button class="bg-panelBg border border-borderColor rounded-full px-4 py-1.5 text-xs font-medium text-textMuted hover:text-textMain" vid="34">처리중</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="flex-1 overflow-y-auto no-scrollbar px-5 py-4 pb-24 flex flex-col gap-3" vid="35">
|
||||||
|
|
||||||
|
<div class="text-xs font-mono text-textMuted mb-1 mt-2" vid="36">TODAY</div>
|
||||||
|
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl p-4 relative group" vid="37">
|
||||||
|
<div class="flex justify-between items-start mb-2" vid="38">
|
||||||
|
<div class="flex items-center gap-2" vid="39">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandGreen shadow-[0_0_5px_#4ade80]" vid="40"></div>
|
||||||
|
<span class="text-xs font-mono text-textMain" vid="41">14:32</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-mono text-brandOrange bg-brandOrange/10 px-2 py-0.5 rounded" vid="42">42 W</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-textMain leading-relaxed mb-3 line-clamp-2" vid="43">
|
||||||
|
네, 지금 보고 있는 색상은 바다색입니다. 약간 푸른 빛이 도는 에메랄드 그린에 가깝네요. 이걸 디자인 시스템에 추가해 주세요.
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-3 text-textMuted" vid="44">
|
||||||
|
<div class="flex items-center gap-1" vid="45">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="46"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" vid="47"></path></svg>
|
||||||
|
<span class="text-[10px] font-mono" vid="48">0:45</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1" vid="49">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="50"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" vid="51"></path></svg>
|
||||||
|
<span class="text-[10px] font-mono" vid="52">CLAUDE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl p-4 relative group" vid="53">
|
||||||
|
<div class="flex justify-between items-start mb-2" vid="54">
|
||||||
|
<div class="flex items-center gap-2" vid="55">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandGreen shadow-[0_0_5px_#4ade80]" vid="56"></div>
|
||||||
|
<span class="text-xs font-mono text-textMain" vid="57">09:15</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-mono text-brandOrange bg-brandOrange/10 px-2 py-0.5 rounded" vid="58">18 W</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-textMain leading-relaxed mb-3 line-clamp-2" vid="59">
|
||||||
|
테스트중입니다. 마이크 입력 레벨 확인하고 있어요. 하나 둘 셋.
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-3 text-textMuted" vid="60">
|
||||||
|
<div class="flex items-center gap-1" vid="61">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="62"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" vid="63"></path></svg>
|
||||||
|
<span class="text-[10px] font-mono" vid="64">0:12</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1" vid="65">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="66"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" vid="67"></path></svg>
|
||||||
|
<span class="text-[10px] font-mono" vid="68">LOCAL</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs font-mono text-textMuted mb-1 mt-4" vid="69">YESTERDAY</div>
|
||||||
|
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl p-4 relative group opacity-70" vid="70">
|
||||||
|
<div class="flex justify-between items-start mb-2" vid="71">
|
||||||
|
<div class="flex items-center gap-2" vid="72">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-textMuted" vid="73"></div>
|
||||||
|
<span class="text-xs font-mono text-textMain" vid="74">18:45</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-mono text-textMuted bg-[#2a2a2e] px-2 py-0.5 rounded" vid="75">128 W</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-textMuted leading-relaxed mb-3 line-clamp-2" vid="76">
|
||||||
|
내일 회의 안건 정리해줘. 첫째로 새로운 UI 디자인 리뷰, 둘째로 프론트엔드 아키텍처 개선 방안, 셋째로 다음 스프린트 일정 산정이야.
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-3 text-textMuted" vid="77">
|
||||||
|
<div class="flex items-center gap-1" vid="78">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="79"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" vid="80"></path></svg>
|
||||||
|
<span class="text-[10px] font-mono" vid="81">2:30</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1" vid="82">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="83"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" vid="84"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="absolute bottom-[80px] left-0 w-full px-5 py-2 flex justify-between items-center bg-gradient-to-t from-appBg to-transparent z-20 pointer-events-none" vid="85">
|
||||||
|
<div class="flex items-center gap-1.5" vid="86">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandGreen shadow-[0_0_5px_#4ade80]" vid="87"></div>
|
||||||
|
<span class="text-[8px] font-mono text-textMuted tracking-wider" vid="88">OLLAMA GEMMA4:E4B</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[8px] font-mono text-textMuted tracking-widest opacity-50" vid="89">PRECISION DATA LINK</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="absolute bottom-0 left-0 w-full h-[80px] bg-appBg/95 backdrop-blur-md border-t border-borderColor flex justify-around items-center px-2 pb-5 z-30" vid="90">
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="91">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="92">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1" vid="93"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1" vid="94"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1" vid="95"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1" vid="96"></rect>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="97">DASH</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="98">
|
||||||
|
<div class="relative" vid="99">
|
||||||
|
<svg class="w-6 h-6 text-brandOrange" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="100">
|
||||||
|
<circle cx="12" cy="12" r="9" vid="101"></circle>
|
||||||
|
<path d="M12 7v5l3 3" vid="102"></path>
|
||||||
|
</svg>
|
||||||
|
<div class="absolute inset-0 bg-brandOrange blur-[10px] opacity-20 rounded-full" vid="103"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[9px] font-mono text-brandOrange font-medium" vid="104">HIST</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="relative -top-5" vid="105">
|
||||||
|
<button class="w-14 h-14 rounded-full bg-panelBg flex items-center justify-center border-4 border-appBg group hover:bg-[#2a2a2e] transition-colors" vid="106">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" vid="107">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" vid="108"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="109">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="110">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" vid="111"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="112">TALK</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="113">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="114">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" vid="115"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" vid="116"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="117">SET</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 w-[120px] h-1 bg-white/30 rounded-full z-40" vid="118"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body></html>
|
||||||
93
docs/v3/designs/login.html
Normal file
93
docs/v3/designs/login.html
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<html lang="ko" vid="0"><head vid="1">
|
||||||
|
<meta charset="UTF-8" vid="2">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" vid="3">
|
||||||
|
<title vid="4">D3RO-VOICE Mobile - Login</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com/3.4.17" vid="5"></script>
|
||||||
|
<script vid="6">
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
appBg: '#19191b',
|
||||||
|
panelBg: '#242427',
|
||||||
|
insetBg: '#0f0f11',
|
||||||
|
brandOrange: '#ff5c35',
|
||||||
|
brandGreen: '#4ade80',
|
||||||
|
textMuted: '#71717a',
|
||||||
|
textMain: '#d4d4d8',
|
||||||
|
borderColor: '#2e2e32'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'],
|
||||||
|
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', "Liberation Mono", "Courier New", 'monospace'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style vid="7">
|
||||||
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||||
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-black min-h-screen flex items-center justify-center p-4" vid="8">
|
||||||
|
<div class="relative w-[375px] h-[812px] bg-appBg rounded-[40px] shadow-[0_0_50px_rgba(0,0,0,0.5)] overflow-hidden border-[8px] border-[#222] flex flex-col font-sans text-textMain" vid="9">
|
||||||
|
<div class="h-12 w-full flex justify-between items-end px-6 pb-2 text-[11px] font-medium text-white/90 z-20 shrink-0" vid="10">
|
||||||
|
<span vid="11">9:41</span>
|
||||||
|
<div class="flex items-center gap-1.5" vid="12">
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="13"><path d="M12 20V10" vid="14"></path><path d="M18 20V4" vid="15"></path><path d="M6 20v-4" vid="16"></path></svg>
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="17"><path d="M5 12.55a11 11 0 0 1 14.08 0" vid="18"></path><path d="M1.42 9a16 16 0 0 1 21.16 0" vid="19"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0" vid="20"></path><line x1="12" y1="20" x2="12.01" y2="20" vid="21"></line></svg>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" vid="22"><path d="M2 12C2 7.02944 6.02944 3 11 3H13C17.9706 3 22 7.02944 22 12C22 16.9706 17.9706 21 13 21H11C6.02944 21 2 16.9706 2 12Z" vid="23"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<main class="flex-1 overflow-y-auto no-scrollbar px-6 pb-12 pt-6 flex flex-col" vid="24">
|
||||||
|
<div class="flex-1 flex flex-col justify-center" vid="25">
|
||||||
|
<div class="flex flex-col items-center mb-10" vid="26">
|
||||||
|
<div class="flex items-center gap-2.5 mb-5" vid="27">
|
||||||
|
<div class="w-2.5 h-2.5 rounded-full bg-brandOrange shadow-[0_0_10px_#ff5c35] animate-pulse" vid="28"></div>
|
||||||
|
<div class="w-2.5 h-2.5 rounded-full bg-brandGreen shadow-[0_0_10px_#4ade80]" vid="29"></div>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-[22px] tracking-[0.25em] font-mono text-textMain ml-1" vid="30">D3RO-VOICE</h1>
|
||||||
|
<p class="text-[9px] font-mono text-textMuted tracking-[0.15em] mt-2 uppercase" vid="31">Precision Voice Intelligence</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3 mb-8" vid="32">
|
||||||
|
<button class="w-full bg-panelBg border border-borderColor rounded-xl py-3.5 flex items-center justify-center gap-3 text-sm font-medium text-textMain hover:bg-[#2a2a2e] hover:border-textMuted/40 transition-all" vid="33">
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" vid="34"><path d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z" vid="35"></path></svg>
|
||||||
|
Google로 로그인
|
||||||
|
</button>
|
||||||
|
<button class="w-full bg-panelBg border border-borderColor rounded-xl py-3.5 flex items-center justify-center gap-3 text-sm font-medium text-textMain hover:bg-[#2a2a2e] hover:border-textMuted/40 transition-all" vid="36">
|
||||||
|
<svg class="w-5 h-5 mb-0.5" viewBox="0 0 24 24" fill="currentColor" vid="37"><path d="M15.4,8.3c-0.4-2.2,1.3-4.1,3.2-4.5c-0.5-2.2-2.3-3.6-4.5-3.6c-1.8-0.2-3.8,1.2-4.8,1.2c-1,0-2.6-1.1-4.1-1.1c-2,0-4,1.1-5,2.9C-1.8,7.2,0.6,13,2.6,15.9c1,1.4,2.2,3.1,3.8,3c1.5-0.1,2.1-1,3.9-1c1.8,0,2.3,1,3.9,1c1.6,0,2.6-1.5,3.6-2.9C18.9,14.4,19.3,13,19.3,12.9C19.2,12.8,15.9,11.5,15.4,8.3z M12.8,3.2c0.8-1,1.4-2.5,1.2-3.9C12.8,0.1,11.3,0.8,10.5,1.8C9.8,2.7,9.2,4.2,9.4,5.6C10.8,5.7,12,4.5,12.8,3.2z" vid="38"></path></svg>
|
||||||
|
Apple로 로그인
|
||||||
|
</button>
|
||||||
|
<button class="w-full bg-panelBg border border-borderColor rounded-xl py-3.5 flex items-center justify-center gap-3 text-sm font-medium text-textMain hover:bg-[#2a2a2e] hover:border-textMuted/40 transition-all" vid="39">
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" vid="40"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" vid="41"></path></svg>
|
||||||
|
GitHub로 로그인
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4 mb-8" vid="42">
|
||||||
|
<div class="flex-1 h-px bg-borderColor" vid="43"></div>
|
||||||
|
<span class="text-[10px] font-mono text-textMuted tracking-wider" vid="44">또는</span>
|
||||||
|
<div class="flex-1 h-px bg-borderColor" vid="45"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-4 mb-6" vid="46">
|
||||||
|
<div vid="47">
|
||||||
|
<label class="block text-[10px] font-mono text-textMuted tracking-widest mb-1.5 pl-1" vid="48">EMAIL</label>
|
||||||
|
<input type="email" placeholder="user@studio.com" class="w-full bg-insetBg border border-borderColor rounded-xl py-3.5 px-4 text-sm text-textMain outline-none focus:border-brandOrange/50 focus:ring-1 focus:ring-brandOrange/50 transition-all placeholder:text-textMuted/30 font-sans" vid="49">
|
||||||
|
</div>
|
||||||
|
<div vid="50">
|
||||||
|
<label class="block text-[10px] font-mono text-textMuted tracking-widest mb-1.5 pl-1" vid="51">PASSWORD</label>
|
||||||
|
<input type="password" placeholder="••••••••" class="w-full bg-insetBg border border-borderColor rounded-xl py-3.5 px-4 text-sm text-textMain outline-none focus:border-brandOrange/50 focus:ring-1 focus:ring-brandOrange/50 transition-all placeholder:text-textMuted/30 font-sans" vid="52">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="w-full bg-brandOrange text-appBg rounded-xl py-3.5 text-sm font-semibold tracking-wide hover:bg-opacity-90 transition-all shadow-[0_0_20px_rgba(255,92,53,0.3)]" vid="53">
|
||||||
|
로그인
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center gap-1.5 mt-8 pb-4" vid="54">
|
||||||
|
<span class="text-[11px] text-textMuted font-sans" vid="55">계정이 없으신가요?</span>
|
||||||
|
<a href="#" class="text-[11px] text-brandOrange font-medium hover:underline" vid="56">회원가입</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 w-[120px] h-1 bg-white/30 rounded-full z-40" vid="57"></div>
|
||||||
|
</div>
|
||||||
|
</body></html>
|
||||||
178
docs/v3/designs/recording.html
Normal file
178
docs/v3/designs/recording.html
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
<html lang="ko" vid="0"><head vid="1">
|
||||||
|
<meta charset="UTF-8" vid="2">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" vid="3">
|
||||||
|
<title vid="4">D3RO-VOICE Mobile - Recording</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com/3.4.17" vid="5"></script>
|
||||||
|
<script vid="6">
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
appBg: '#19191b',
|
||||||
|
panelBg: '#242427',
|
||||||
|
insetBg: '#0f0f11',
|
||||||
|
brandOrange: '#ff5c35',
|
||||||
|
brandGreen: '#4ade80',
|
||||||
|
textMuted: '#71717a',
|
||||||
|
textMain: '#d4d4d8',
|
||||||
|
borderColor: '#2e2e32'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'],
|
||||||
|
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', "Liberation Mono", "Courier New", 'monospace'],
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'pulse-fast': 'pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
'wave-1': 'wave 1.2s ease-in-out infinite',
|
||||||
|
'wave-2': 'wave 1s ease-in-out infinite',
|
||||||
|
'wave-3': 'wave 1.5s ease-in-out infinite',
|
||||||
|
'wave-4': 'wave 0.8s ease-in-out infinite',
|
||||||
|
'wave-5': 'wave 1.3s ease-in-out infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
wave: {
|
||||||
|
'0%, 100%': { height: '8px' },
|
||||||
|
'50%': { height: '32px' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style vid="7">
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-black min-h-screen flex items-center justify-center p-4" vid="8">
|
||||||
|
|
||||||
|
<div class="relative w-[375px] h-[812px] bg-appBg rounded-[40px] shadow-[0_0_50px_rgba(0,0,0,0.5)] overflow-hidden border-[8px] border-[#222] flex flex-col font-sans text-textMain" vid="9">
|
||||||
|
|
||||||
|
<div class="h-12 w-full flex justify-between items-end px-6 pb-2 text-[11px] font-medium text-white/90 z-20" vid="10">
|
||||||
|
<span vid="11">9:41</span>
|
||||||
|
<div class="flex items-center gap-1.5" vid="12">
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="13"><path d="M12 20V10" vid="14"></path><path d="M18 20V4" vid="15"></path><path d="M6 20v-4" vid="16"></path></svg>
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="17"><path d="M5 12.55a11 11 0 0 1 14.08 0" vid="18"></path><path d="M1.42 9a16 16 0 0 1 21.16 0" vid="19"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0" vid="20"></path><line x1="12" y1="20" x2="12.01" y2="20" vid="21"></line></svg>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" vid="22"><path d="M2 12C2 7.02944 6.02944 3 11 3H13C17.9706 3 22 7.02944 22 12C22 16.9706 17.9706 21 13 21H11C6.02944 21 2 16.9706 2 12Z" vid="23"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header class="px-5 py-3 flex justify-between items-center z-10 border-b border-borderColor/50" vid="24">
|
||||||
|
<div class="flex items-center gap-2" vid="25">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-brandOrange animate-pulse shadow-[0_0_8px_#ff5c35]" vid="26"></div>
|
||||||
|
<span class="text-[10px] tracking-[0.2em] font-mono text-brandOrange" vid="27">REC_SESSION</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3" vid="28">
|
||||||
|
<span class="text-[10px] font-mono text-brandOrange" vid="29">03:24</span>
|
||||||
|
<div class="flex gap-1.5" vid="30">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandGreen shadow-[0_0_5px_#4ade80]" vid="31"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-1 flex flex-col overflow-hidden relative" vid="32">
|
||||||
|
|
||||||
|
<div class="h-32 bg-insetBg flex items-center justify-center border-b border-[#1a1a1c] relative" vid="33">
|
||||||
|
<div class="absolute inset-0 opacity-[0.03]" style="background-image: radial-gradient(#fff 1px, transparent 1px); background-size: 16px 16px;" vid="34"></div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1.5 z-10 h-16" vid="35">
|
||||||
|
<div class="w-1.5 bg-brandOrange rounded-full animate-wave-1" vid="36"></div>
|
||||||
|
<div class="w-1.5 bg-brandOrange rounded-full animate-wave-2" vid="37"></div>
|
||||||
|
<div class="w-1.5 bg-brandOrange rounded-full animate-wave-3" vid="38"></div>
|
||||||
|
<div class="w-1.5 bg-brandOrange rounded-full animate-wave-4" vid="39"></div>
|
||||||
|
<div class="w-1.5 bg-brandOrange rounded-full animate-wave-5" vid="40"></div>
|
||||||
|
<div class="w-1.5 bg-brandOrange rounded-full animate-wave-2" vid="41"></div>
|
||||||
|
<div class="w-1.5 bg-brandOrange rounded-full animate-wave-1" vid="42"></div>
|
||||||
|
<div class="w-1.5 bg-brandOrange rounded-full animate-wave-3" vid="43"></div>
|
||||||
|
<div class="w-1.5 bg-brandOrange rounded-full animate-wave-5" vid="44"></div>
|
||||||
|
<div class="w-1.5 bg-brandOrange rounded-full animate-wave-4" vid="45"></div>
|
||||||
|
<div class="w-1.5 bg-brandOrange rounded-full animate-wave-2" vid="46"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto no-scrollbar p-5 flex flex-col gap-4 bg-appBg" vid="47">
|
||||||
|
<div class="text-sm leading-relaxed text-textMuted font-mono" vid="48">
|
||||||
|
> initializing voice recording protocol...<br vid="49">
|
||||||
|
> establishing local AI link...<br vid="50">
|
||||||
|
> connection secure.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-lg leading-relaxed text-textMain" vid="51">
|
||||||
|
오늘 회의에서는 다음 분기 마케팅 전략에 대해 논의했습니다. 주요 목표는 기존 고객 유지율을 15% 높이는 것입니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-lg leading-relaxed text-textMain" vid="52">
|
||||||
|
이를 위해 이메일 캠페인을 개편하고, 로열티 프로그램을 도입할 예정입니다. 각 팀은 다음 주 수요일까지 세부 실행 계획을 제출해 주시기 바랍니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-lg leading-relaxed text-textMain flex items-baseline gap-2" vid="53">
|
||||||
|
<span class="text-brandOrange" vid="54">그리고 예산 관련해서는</span>
|
||||||
|
<span class="w-2 h-4 bg-brandOrange animate-pulse-fast inline-block" vid="55"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="absolute bottom-[80px] left-0 w-full px-5 py-2 flex justify-between items-center bg-gradient-to-t from-appBg via-appBg to-transparent z-20" vid="56">
|
||||||
|
<div class="flex items-center gap-1.5" vid="57">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandOrange animate-pulse shadow-[0_0_5px_#ff5c35]" vid="58"></div>
|
||||||
|
<span class="text-[8px] font-mono text-brandOrange tracking-wider" vid="59">LISTENING_</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[8px] font-mono text-textMuted tracking-widest opacity-50" vid="60">REAL-TIME TRANSCRIPT</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="absolute bottom-0 left-0 w-full h-[80px] bg-appBg/95 backdrop-blur-md border-t border-borderColor flex justify-around items-center px-2 pb-5 z-30" vid="61">
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group opacity-50 cursor-not-allowed" vid="62">
|
||||||
|
<svg class="w-6 h-6 text-textMuted" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="63">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1" vid="64"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1" vid="65"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1" vid="66"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1" vid="67"></rect>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted" vid="68">DASH</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group opacity-50 cursor-not-allowed" vid="69">
|
||||||
|
<svg class="w-6 h-6 text-textMuted" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="70">
|
||||||
|
<circle cx="12" cy="12" r="9" vid="71"></circle>
|
||||||
|
<path d="M12 7v5l3 3" vid="72"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted" vid="73">HIST</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="relative -top-5" vid="74">
|
||||||
|
<button class="w-14 h-14 rounded-full bg-panelBg flex items-center justify-center border-4 border-appBg group border-brandOrange" vid="75">
|
||||||
|
<svg class="w-5 h-5 text-brandOrange" fill="currentColor" viewBox="0 0 24 24" vid="76">
|
||||||
|
<rect x="6" y="6" width="12" height="12" rx="2" vid="77"></rect>
|
||||||
|
</svg>
|
||||||
|
<div class="absolute inset-0 border-2 border-brandOrange rounded-full animate-ping opacity-20" vid="78"></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group opacity-50 cursor-not-allowed" vid="79">
|
||||||
|
<svg class="w-6 h-6 text-textMuted" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="80">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" vid="81"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted" vid="82">TALK</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group opacity-50 cursor-not-allowed" vid="83">
|
||||||
|
<svg class="w-6 h-6 text-textMuted" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="84">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" vid="85"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" vid="86"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted" vid="87">SET</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 w-[120px] h-1 bg-white/30 rounded-full z-40" vid="88"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body></html>
|
||||||
202
docs/v3/designs/settings.html
Normal file
202
docs/v3/designs/settings.html
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
<html lang="ko" vid="0"><head vid="1">
|
||||||
|
<meta charset="UTF-8" vid="2">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" vid="3">
|
||||||
|
<title vid="4">D3RO-VOICE Mobile - Settings</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com/3.4.17" vid="5"></script>
|
||||||
|
<script vid="6">
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
appBg: '#19191b',
|
||||||
|
panelBg: '#242427',
|
||||||
|
insetBg: '#0f0f11',
|
||||||
|
brandOrange: '#ff5c35',
|
||||||
|
brandGreen: '#4ade80',
|
||||||
|
textMuted: '#71717a',
|
||||||
|
textMain: '#d4d4d8',
|
||||||
|
borderColor: '#2e2e32'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'],
|
||||||
|
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', "Liberation Mono", "Courier New", 'monospace'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style vid="7">
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-black min-h-screen flex items-center justify-center p-4" vid="8">
|
||||||
|
|
||||||
|
<div class="relative w-[375px] h-[812px] bg-appBg rounded-[40px] shadow-[0_0_50px_rgba(0,0,0,0.5)] overflow-hidden border-[8px] border-[#222] flex flex-col font-sans text-textMain" vid="9">
|
||||||
|
|
||||||
|
<div class="h-12 w-full flex justify-between items-end px-6 pb-2 text-[11px] font-medium text-white/90 z-20" vid="10">
|
||||||
|
<span vid="11">9:41</span>
|
||||||
|
<div class="flex items-center gap-1.5" vid="12">
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="13"><path d="M12 20V10" vid="14"></path><path d="M18 20V4" vid="15"></path><path d="M6 20v-4" vid="16"></path></svg>
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="17"><path d="M5 12.55a11 11 0 0 1 14.08 0" vid="18"></path><path d="M1.42 9a16 16 0 0 1 21.16 0" vid="19"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0" vid="20"></path><line x1="12" y1="20" x2="12.01" y2="20" vid="21"></line></svg>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" vid="22"><path d="M2 12C2 7.02944 6.02944 3 11 3H13C17.9706 3 22 7.02944 22 12C22 16.9706 17.9706 21 13 21H11C6.02944 21 2 16.9706 2 12Z" vid="23"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header class="px-5 py-3 flex justify-between items-center z-10 border-b border-borderColor/50 mb-2" vid="24">
|
||||||
|
<h1 class="text-xl font-medium tracking-tight text-white" vid="25">설정</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-1 overflow-y-auto no-scrollbar px-5 pb-32" vid="26">
|
||||||
|
|
||||||
|
<div class="mb-6" vid="27">
|
||||||
|
<h2 class="text-[11px] font-mono text-textMuted tracking-widest mb-3 pl-1" vid="28">ACCOUNT & PLAN</h2>
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl overflow-hidden flex flex-col" vid="29">
|
||||||
|
<div class="px-4 py-4 flex justify-between items-center border-b border-borderColor/50" vid="30">
|
||||||
|
<div class="flex items-center gap-3" vid="31">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-insetBg border border-borderColor flex items-center justify-center" vid="32">
|
||||||
|
<span class="font-mono text-brandOrange" vid="33">US</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col" vid="34">
|
||||||
|
<span class="text-sm font-medium text-white" vid="35">User Studio</span>
|
||||||
|
<span class="text-[10px] text-textMuted font-mono" vid="36">user@studio.com</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="text-[10px] font-mono text-textMuted hover:text-brandOrange transition-colors" vid="37">EDIT</button>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-4 flex justify-between items-center bg-insetBg" vid="38">
|
||||||
|
<span class="text-xs text-textMuted font-medium" vid="39">현재 등급</span>
|
||||||
|
<div class="flex items-center gap-2" vid="40">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandOrange shadow-[0_0_5px_#ff5c35]" vid="41"></div>
|
||||||
|
<span class="text-sm text-brandOrange font-mono tracking-tight" vid="42">FREE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6" vid="43">
|
||||||
|
<h2 class="text-[11px] font-mono text-textMuted tracking-widest mb-3 pl-1" vid="44">BACKEND CONFIG</h2>
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl overflow-hidden flex flex-col" vid="45">
|
||||||
|
<div class="px-4 py-4 flex justify-between items-center border-b border-borderColor/50" vid="46">
|
||||||
|
<div class="flex flex-col gap-1" vid="47">
|
||||||
|
<span class="text-sm font-medium text-white" vid="48">LLM 모델 선택</span>
|
||||||
|
<span class="text-[10px] text-textMuted" vid="49">음성 인식 후 처리할 백엔드 모델</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2" vid="50">
|
||||||
|
<span class="text-sm text-brandOrange font-mono tracking-tight" vid="51">CLAUDE</span>
|
||||||
|
<svg class="w-4 h-4 text-textMuted" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="52"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" vid="53"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-4 flex justify-between items-center" vid="54">
|
||||||
|
<div class="flex flex-col gap-1" vid="55">
|
||||||
|
<span class="text-sm font-medium text-white" vid="56">로컬 STT 엔진</span>
|
||||||
|
<span class="text-[10px] text-textMuted" vid="57">오프라인 음성 인식 (Ollama)</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative inline-flex items-center cursor-pointer" vid="58">
|
||||||
|
<input type="checkbox" value="" class="sr-only peer" checked="" vid="59">
|
||||||
|
<div class="w-9 h-5 bg-insetBg peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-brandOrange" vid="60"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6" vid="61">
|
||||||
|
<h2 class="text-[11px] font-mono text-textMuted tracking-widest mb-3 pl-1" vid="62">PREFERENCES</h2>
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-xl overflow-hidden flex flex-col" vid="63">
|
||||||
|
<div class="px-4 py-4 flex justify-between items-center border-b border-borderColor/50" vid="64">
|
||||||
|
<span class="text-sm font-medium text-white" vid="65">주 언어</span>
|
||||||
|
<div class="flex items-center gap-2" vid="66">
|
||||||
|
<span class="text-sm text-textMuted" vid="67">한국어</span>
|
||||||
|
<svg class="w-4 h-4 text-textMuted" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="68"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" vid="69"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-4 flex justify-between items-center border-b border-borderColor/50" vid="70">
|
||||||
|
<span class="text-sm font-medium text-white" vid="71">자동 교정</span>
|
||||||
|
<div class="relative inline-flex items-center cursor-pointer" vid="72">
|
||||||
|
<input type="checkbox" value="" class="sr-only peer" checked="" vid="73">
|
||||||
|
<div class="w-9 h-5 bg-insetBg peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-brandOrange" vid="74"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-4 flex justify-between items-center" vid="75">
|
||||||
|
<span class="text-sm font-medium text-white" vid="76">햅틱 피드백</span>
|
||||||
|
<div class="relative inline-flex items-center cursor-pointer" vid="77">
|
||||||
|
<input type="checkbox" value="" class="sr-only peer" checked="" vid="78">
|
||||||
|
<div class="w-9 h-5 bg-insetBg peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-brandOrange" vid="79"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4" vid="80">
|
||||||
|
<button class="w-full bg-panelBg border border-borderColor rounded-xl py-3.5 flex items-center justify-center text-sm font-medium text-brandOrange hover:bg-[#2a2a2e] transition-colors shadow-sm" vid="81">
|
||||||
|
로그아웃
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center mt-6 opacity-50" vid="82">
|
||||||
|
<div class="flex items-center gap-1.5" vid="83">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandGreen shadow-[0_0_5px_#4ade80]" vid="84"></div>
|
||||||
|
<span class="text-[8px] font-mono text-textMuted tracking-wider" vid="85">OLLAMA GEMMA4:E4B</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="absolute bottom-0 left-0 w-full h-[80px] bg-appBg/95 backdrop-blur-md border-t border-borderColor flex justify-around items-center px-2 pb-5 z-30" vid="86">
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="87">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="88">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1" vid="89"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1" vid="90"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1" vid="91"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1" vid="92"></rect>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="93">DASH</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="94">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="95">
|
||||||
|
<circle cx="12" cy="12" r="9" vid="96"></circle>
|
||||||
|
<path d="M12 7v5l3 3" vid="97"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="98">HIST</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="relative -top-5" vid="99">
|
||||||
|
<button class="w-14 h-14 rounded-full bg-panelBg flex items-center justify-center shadow-[0_0_20px_rgba(0,0,0,0.4)] border-4 border-appBg group border-borderColor" vid="100">
|
||||||
|
<svg class="w-6 h-6 text-brandOrange" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" vid="101">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" vid="102"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="103">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="104">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" vid="105"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="106">TALK</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="107">
|
||||||
|
<div class="relative" vid="108">
|
||||||
|
<svg class="w-6 h-6 text-brandOrange" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="109">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" vid="110"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" vid="111"></path>
|
||||||
|
</svg>
|
||||||
|
<div class="absolute inset-0 bg-brandOrange blur-[10px] opacity-20 rounded-full" vid="112"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[9px] font-mono text-brandOrange font-medium" vid="113">SET</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 w-[120px] h-1 bg-white/30 rounded-full z-40" vid="114"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</body></html>
|
||||||
184
docs/v3/designs/talk.html
Normal file
184
docs/v3/designs/talk.html
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
<html lang="ko" vid="0"><head vid="1">
|
||||||
|
<meta charset="UTF-8" vid="2">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" vid="3">
|
||||||
|
<title vid="4">D3RO-VOICE Mobile - Talk</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com/3.4.17" vid="5"></script>
|
||||||
|
<script vid="6">
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
appBg: '#19191b',
|
||||||
|
panelBg: '#242427',
|
||||||
|
insetBg: '#0f0f11',
|
||||||
|
brandOrange: '#ff5c35',
|
||||||
|
brandGreen: '#4ade80',
|
||||||
|
textMuted: '#71717a',
|
||||||
|
textMain: '#d4d4d8',
|
||||||
|
borderColor: '#2e2e32'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'],
|
||||||
|
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', "Liberation Mono", "Courier New", 'monospace'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style vid="7">
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-black min-h-screen flex items-center justify-center p-4" vid="8">
|
||||||
|
|
||||||
|
<div class="relative w-[375px] h-[812px] bg-appBg rounded-[40px] shadow-[0_0_50px_rgba(0,0,0,0.5)] overflow-hidden border-[8px] border-[#222] flex flex-col font-sans text-textMain" vid="9">
|
||||||
|
|
||||||
|
<div class="h-12 w-full flex justify-between items-end px-6 pb-2 text-[11px] font-medium text-white/90 z-20" vid="10">
|
||||||
|
<span vid="11">9:41</span>
|
||||||
|
<div class="flex items-center gap-1.5" vid="12">
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="13"><path d="M12 20V10" vid="14"></path><path d="M18 20V4" vid="15"></path><path d="M6 20v-4" vid="16"></path></svg>
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="17"><path d="M5 12.55a11 11 0 0 1 14.08 0" vid="18"></path><path d="M1.42 9a16 16 0 0 1 21.16 0" vid="19"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0" vid="20"></path><line x1="12" y1="20" x2="12.01" y2="20" vid="21"></line></svg>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" vid="22"><path d="M2 12C2 7.02944 6.02944 3 11 3H13C17.9706 3 22 7.02944 22 12C22 16.9706 17.9706 21 13 21H11C6.02944 21 2 16.9706 2 12Z" vid="23"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header class="px-5 py-3 flex justify-between items-center z-10 border-b border-borderColor" vid="24">
|
||||||
|
<div class="flex items-center gap-2" vid="25">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-brandOrange shadow-[0_0_8px_#ff5c35]" vid="26"></div>
|
||||||
|
<span class="text-[10px] tracking-[0.2em] font-mono text-textMuted" vid="27">TALK</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3" vid="28">
|
||||||
|
<span class="text-[10px] font-mono text-brandOrange" vid="29">LIVE</span>
|
||||||
|
<div class="flex gap-1.5" vid="30">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandGreen shadow-[0_0_5px_#4ade80] animate-pulse" vid="31"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-1 overflow-y-auto no-scrollbar px-5 py-4 pb-32 flex flex-col gap-4" vid="32">
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1 items-center mb-4" vid="33">
|
||||||
|
<span class="text-[10px] font-mono text-textMuted bg-panelBg px-3 py-1 rounded-full border border-borderColor" vid="34">TODAY, 9:30 AM</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-start" vid="35">
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-2xl rounded-tl-sm p-4 max-w-[85%] shadow-sm relative" vid="36">
|
||||||
|
<p class="text-sm text-textMain leading-relaxed" vid="37">안녕하세요! 오늘 하루는 어떠신가요? 도움이 필요하신 작업이 있다면 말씀해주세요.</p>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted absolute -bottom-5 left-1" vid="38">CLAUDE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-2" vid="39">
|
||||||
|
<div class="bg-brandOrange/10 border border-brandOrange/30 rounded-2xl rounded-tr-sm p-4 max-w-[85%] shadow-sm relative" vid="40">
|
||||||
|
<p class="text-sm text-brandOrange leading-relaxed" vid="41">이번 주에 진행해야 할 프로젝트 기획안 초안을 작성해야 해. 어떤 구조로 짜는 게 좋을까?</p>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted absolute -bottom-5 right-1" vid="42">YOU</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-start mt-2" vid="43">
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-2xl rounded-tl-sm p-4 max-w-[85%] shadow-sm relative" vid="44">
|
||||||
|
<p class="text-sm text-textMain leading-relaxed mb-3" vid="45">프로젝트 기획안 초안 구조를 제안해 드릴게요.</p>
|
||||||
|
<ul class="text-sm text-textMain/80 list-disc pl-4 space-y-1.5 font-light" vid="46">
|
||||||
|
<li vid="47">프로젝트 개요 (배경 및 목적)</li>
|
||||||
|
<li vid="48">목표 설정 및 핵심 지표 (KPI)</li>
|
||||||
|
<li vid="49">타겟 오디언스 분석</li>
|
||||||
|
<li vid="50">주요 기능 및 요구사항</li>
|
||||||
|
<li vid="51">일정 및 마일스톤</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-sm text-textMain leading-relaxed mt-3" vid="52">이 중에서 어떤 부분부터 구체화해 볼까요?</p>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted absolute -bottom-5 left-1" vid="53">CLAUDE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-2" vid="54">
|
||||||
|
<div class="bg-brandOrange/5 border border-brandOrange/20 rounded-2xl rounded-tr-sm p-4 max-w-[85%] shadow-sm relative flex items-center gap-2" vid="55">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandOrange animate-bounce" style="animation-delay: 0ms;" vid="56"></div>
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandOrange animate-bounce" style="animation-delay: 150ms;" vid="57"></div>
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandOrange animate-bounce" style="animation-delay: 300ms;" vid="58"></div>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted absolute -bottom-5 right-1" vid="59">LISTENING...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="absolute bottom-[80px] left-0 w-full bg-appBg/95 backdrop-blur-md border-t border-borderColor z-20" vid="60">
|
||||||
|
<div class="px-4 py-3" vid="61">
|
||||||
|
<div class="bg-insetBg rounded-xl border border-borderColor flex items-center p-2 relative overflow-hidden" vid="62">
|
||||||
|
<div class="absolute inset-0 opacity-10" style="background-image: radial-gradient(#ff5c35 1px, transparent 1px); background-size: 8px 8px;" vid="63"></div>
|
||||||
|
<button class="w-8 h-8 rounded-full bg-panelBg border border-borderColor flex items-center justify-center text-textMuted hover:text-brandOrange transition-colors z-10" vid="64">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="65"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" vid="66"></path></svg>
|
||||||
|
</button>
|
||||||
|
<input type="text" placeholder="메시지를 입력하거나 말하세요..." class="flex-1 bg-transparent border-none text-sm text-textMain px-3 outline-none placeholder:text-textMuted/50 z-10 font-sans" vid="67">
|
||||||
|
<button class="w-8 h-8 rounded-full bg-brandOrange/20 border border-brandOrange/30 flex items-center justify-center text-brandOrange z-10" vid="68">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="69"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" vid="70"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-5 pb-2 flex justify-between items-center" vid="71">
|
||||||
|
<div class="flex items-center gap-1.5" vid="72">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-brandGreen shadow-[0_0_5px_#4ade80]" vid="73"></div>
|
||||||
|
<span class="text-[8px] font-mono text-textMuted tracking-wider" vid="74">OLLAMA GEMMA4:E4B</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[8px] font-mono text-textMuted tracking-widest opacity-50" vid="75">PRECISION DATA LINK</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="absolute bottom-0 left-0 w-full h-[80px] bg-appBg/95 backdrop-blur-md border-t border-borderColor flex justify-around items-center px-2 pb-5 z-30" vid="76">
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="77">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="78">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1" vid="79"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1" vid="80"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1" vid="81"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1" vid="82"></rect>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="83">DASH</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="84">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="85">
|
||||||
|
<circle cx="12" cy="12" r="9" vid="86"></circle>
|
||||||
|
<path d="M12 7v5l3 3" vid="87"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="88">HIST</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="relative -top-5" vid="89">
|
||||||
|
<button class="w-14 h-14 rounded-full bg-brandOrange flex items-center justify-center shadow-[0_0_20px_rgba(255,92,53,0.4)] border-4 border-appBg group" vid="90">
|
||||||
|
<svg class="w-6 h-6 text-appBg" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" vid="91">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" vid="92"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="93">
|
||||||
|
<div class="relative" vid="94">
|
||||||
|
<svg class="w-6 h-6 text-brandOrange" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="95">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" vid="96"></path>
|
||||||
|
</svg>
|
||||||
|
<div class="absolute inset-0 bg-brandOrange blur-[10px] opacity-20 rounded-full" vid="97"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[9px] font-mono text-brandOrange font-medium" vid="98">TALK</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="flex flex-col items-center justify-center w-14 gap-1.5 group" vid="99">
|
||||||
|
<svg class="w-6 h-6 text-textMuted group-hover:text-textMain transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" vid="100">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" vid="101"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" vid="102"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[9px] font-mono text-textMuted group-hover:text-textMain transition-colors" vid="103">SET</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 w-[120px] h-1 bg-white/30 rounded-full z-40" vid="104"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</body></html>
|
||||||
150
docs/v3/designs/upgrade.html
Normal file
150
docs/v3/designs/upgrade.html
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
<html lang="ko" vid="0"><head vid="1">
|
||||||
|
<meta charset="UTF-8" vid="2">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" vid="3">
|
||||||
|
<title vid="4">D3RO-VOICE Mobile - Upgrade</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com/3.4.17" vid="5"></script>
|
||||||
|
<script vid="6">
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
appBg: '#19191b',
|
||||||
|
panelBg: '#242427',
|
||||||
|
insetBg: '#0f0f11',
|
||||||
|
brandOrange: '#ff5c35',
|
||||||
|
brandGreen: '#4ade80',
|
||||||
|
textMuted: '#71717a',
|
||||||
|
textMain: '#d4d4d8',
|
||||||
|
borderColor: '#2e2e32'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'],
|
||||||
|
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', "Liberation Mono", "Courier New", 'monospace'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style vid="7">
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-black min-h-screen flex items-center justify-center p-4" vid="8">
|
||||||
|
|
||||||
|
<div class="relative w-[375px] h-[812px] bg-appBg rounded-[40px] shadow-[0_0_50px_rgba(0,0,0,0.5)] overflow-hidden border-[8px] border-[#222] flex flex-col font-sans text-textMain" vid="9">
|
||||||
|
|
||||||
|
<div class="h-12 w-full flex justify-between items-end px-6 pb-2 text-[11px] font-medium text-white/90 z-20" vid="10">
|
||||||
|
<span vid="11">9:41</span>
|
||||||
|
<div class="flex items-center gap-1.5" vid="12">
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="13"><path d="M12 20V10" vid="14"></path><path d="M18 20V4" vid="15"></path><path d="M6 20v-4" vid="16"></path></svg>
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" vid="17"><path d="M5 12.55a11 11 0 0 1 14.08 0" vid="18"></path><path d="M1.42 9a16 16 0 0 1 21.16 0" vid="19"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0" vid="20"></path><line x1="12" y1="20" x2="12.01" y2="20" vid="21"></line></svg>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" vid="22"><path d="M2 12C2 7.02944 6.02944 3 11 3H13C17.9706 3 22 7.02944 22 12C22 16.9706 17.9706 21 13 21H11C6.02944 21 2 16.9706 2 12Z" vid="23"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header class="px-5 py-3 flex justify-between items-center z-10" vid="24">
|
||||||
|
<div class="flex items-center gap-2" vid="25">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-brandOrange shadow-[0_0_8px_#ff5c35]" vid="26"></div>
|
||||||
|
<span class="text-[10px] tracking-[0.2em] font-mono text-textMuted" vid="27">SUBSCRIPTION</span>
|
||||||
|
</div>
|
||||||
|
<button class="w-8 h-8 flex items-center justify-center bg-panelBg border border-borderColor rounded-full text-textMuted hover:text-textMain transition-colors" vid="28">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="29"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" vid="30"></path></svg>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-1 overflow-y-auto no-scrollbar px-5 pb-24" vid="31">
|
||||||
|
|
||||||
|
<div class="text-center mt-4 mb-8" vid="32">
|
||||||
|
<h1 class="text-2xl font-semibold text-white mb-2 tracking-tight" vid="33">Unlock Full Potential</h1>
|
||||||
|
<p class="text-xs text-textMuted" vid="34">업그레이드하고 프리미엄 AI 모델을 무제한으로 사용하세요.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-panelBg border border-borderColor rounded-2xl p-5 mb-4 relative overflow-hidden" vid="35">
|
||||||
|
<div class="flex justify-between items-start mb-4" vid="36">
|
||||||
|
<div vid="37">
|
||||||
|
<span class="text-[10px] font-mono text-textMuted mb-1 block" vid="38">CURRENT PLAN</span>
|
||||||
|
<h2 class="text-xl font-bold text-white tracking-tight" vid="39">FREE</h2>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-mono text-textMuted" vid="40">$0 / mo</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4" vid="41">
|
||||||
|
<div class="flex flex-col gap-1.5" vid="42">
|
||||||
|
<div class="flex justify-between items-center text-xs" vid="43">
|
||||||
|
<span class="text-textMuted" vid="44">기본 음성 인식</span>
|
||||||
|
<span class="text-brandOrange font-mono" vid="45">무제한</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1.5" vid="46">
|
||||||
|
<div class="flex justify-between items-center text-xs" vid="47">
|
||||||
|
<span class="text-textMuted" vid="48">로컬 AI (Gemma)</span>
|
||||||
|
<span class="text-brandOrange font-mono" vid="49">무제한</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1.5 pt-2 border-t border-borderColor" vid="50">
|
||||||
|
<div class="flex justify-between items-center text-[11px] mb-1" vid="51">
|
||||||
|
<span class="text-brandGreen font-medium" vid="52">프리미엄 쿼터 (Haiku)</span>
|
||||||
|
<span class="text-white font-mono" vid="53">250 / 500</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1.5 w-full bg-insetBg rounded-full overflow-hidden" vid="54">
|
||||||
|
<div class="h-full bg-brandGreen rounded-full" style="width: 50%" vid="55"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-insetBg border border-brandOrange/30 rounded-2xl p-5 relative overflow-hidden shadow-[inset_0_0_30px_rgba(255,92,53,0.05)]" vid="56">
|
||||||
|
<div class="absolute top-0 right-0 w-32 h-32 bg-brandOrange/10 blur-[40px] rounded-full -translate-y-1/2 translate-x-1/4" vid="57"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 flex justify-between items-start mb-4" vid="58">
|
||||||
|
<div vid="59">
|
||||||
|
<span class="text-[10px] font-mono text-brandOrange mb-1 block" vid="60">UPGRADE PLAN</span>
|
||||||
|
<h2 class="text-xl font-bold text-white tracking-tight" vid="61">PREMIUM</h2>
|
||||||
|
</div>
|
||||||
|
<div class="text-right" vid="62">
|
||||||
|
<span class="text-lg font-mono text-white" vid="63">$15</span>
|
||||||
|
<span class="text-[10px] font-mono text-textMuted" vid="64"> / mo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="space-y-3 relative z-10 mb-6" vid="65">
|
||||||
|
<li class="flex items-start gap-2.5 text-xs text-textMain" vid="66">
|
||||||
|
<svg class="w-4 h-4 text-brandOrange shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="67"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" vid="68"></path></svg>
|
||||||
|
<span vid="69">Claude 3.5 Sonnet / Opus 무제한 사용</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2.5 text-xs text-textMain" vid="70">
|
||||||
|
<svg class="w-4 h-4 text-brandOrange shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="71"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" vid="72"></path></svg>
|
||||||
|
<span vid="73">GPT-4o 및 외부 API 연동 완벽 지원</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2.5 text-xs text-textMain" vid="74">
|
||||||
|
<svg class="w-4 h-4 text-brandOrange shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="75"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" vid="76"></path></svg>
|
||||||
|
<span vid="77">클라우드 세션 동기화 및 무제한 히스토리</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2.5 text-xs text-textMain" vid="78">
|
||||||
|
<svg class="w-4 h-4 text-brandOrange shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" vid="79"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" vid="80"></path></svg>
|
||||||
|
<span vid="81">초정밀 오디오 향상 필터 적용</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button class="relative z-10 w-full bg-brandOrange hover:bg-[#ff7554] text-appBg font-bold py-3.5 rounded-xl transition-colors shadow-[0_0_15px_rgba(255,92,53,0.3)]" vid="82">
|
||||||
|
UPGRADE NOW
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 text-center" vid="83">
|
||||||
|
<a href="#" class="text-[10px] text-textMuted underline underline-offset-2 hover:text-textMain transition-colors" vid="84">이용 약관 및 환불 정책 보기</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 w-[120px] h-1 bg-white/30 rounded-full z-40" vid="85"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body></html>
|
||||||
11551
package-lock.json
generated
11551
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
||||||
// packages/ui-native/src/components/PhosphorText.tsx
|
// packages/ui-native/src/components/PhosphorText.tsx
|
||||||
// RN용 PhosphorText — Text + 앰버 glow (textShadowColor)
|
// RN용 PhosphorText — Text + 앰버 glow (textShadowColor)
|
||||||
|
|
||||||
import { Text, type TextProps, type TextStyle, type StyleProp } from 'react-native'
|
import { Text, Platform, type TextProps, type TextStyle, type StyleProp } from 'react-native'
|
||||||
import { d3roNativePalette, d3roNativeTypo, type D3roNativeTypoKey } from '../theme'
|
import { d3roNativePalette, d3roNativeTypo, type D3roNativeTypoKey } from '../theme'
|
||||||
|
|
||||||
export type PhosphorVariant = D3roNativeTypoKey
|
export type PhosphorVariant = D3roNativeTypoKey
|
||||||
|
|
@ -34,7 +34,7 @@ export function PhosphorText({
|
||||||
const baseStyle: TextStyle = {
|
const baseStyle: TextStyle = {
|
||||||
...typo,
|
...typo,
|
||||||
color: resolvedColor,
|
color: resolvedColor,
|
||||||
fontFamily: 'monospace',
|
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
||||||
...(isAmberVariant
|
...(isAmberVariant
|
||||||
? {
|
? {
|
||||||
textShadowColor: d3roNativePalette.accent.amberGlow,
|
textShadowColor: d3roNativePalette.accent.amberGlow,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
// packages/ui-native — RN용 DS 컴포넌트 barrel
|
// packages/ui-native — RN용 DS 컴포넌트 barrel
|
||||||
// apps/mobile이 file: 의존성으로 참조
|
// apps/mobile이 file: 의존성으로 참조
|
||||||
|
|
||||||
export * from './theme'
|
export {
|
||||||
|
d3roNativePalette,
|
||||||
|
d3roNativeTypo,
|
||||||
|
d3roNativeRadius,
|
||||||
|
d3roNativeFonts,
|
||||||
|
type D3roNativePaletteKey,
|
||||||
|
type D3roNativeTypoKey
|
||||||
|
} from './theme'
|
||||||
export { MetalCard, type MetalCardProps } from './components/MetalCard'
|
export { MetalCard, type MetalCardProps } from './components/MetalCard'
|
||||||
export {
|
export {
|
||||||
PhosphorText,
|
PhosphorText,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
// RN용 테마 토큰 — MUI 없이 순수 색상/타이포/그림자 값
|
// RN용 테마 토큰 — MUI 없이 순수 색상/타이포/그림자 값
|
||||||
// packages/ui의 d3roPalette와 의도적으로 동기화 (RN은 CSS var 불가, 정적 값)
|
// packages/ui의 d3roPalette와 의도적으로 동기화 (RN은 CSS var 불가, 정적 값)
|
||||||
|
|
||||||
|
import { Platform } from 'react-native'
|
||||||
|
|
||||||
export const d3roNativePalette = {
|
export const d3roNativePalette = {
|
||||||
bg: {
|
bg: {
|
||||||
app: '#19191b',
|
app: '#19191b',
|
||||||
|
|
@ -9,25 +11,28 @@ export const d3roNativePalette = {
|
||||||
cardHover: '#2a2a2d',
|
cardHover: '#2a2a2d',
|
||||||
elevated: '#2e2e32',
|
elevated: '#2e2e32',
|
||||||
sidebar: '#1e1f21',
|
sidebar: '#1e1f21',
|
||||||
inset: '#1b1c1e',
|
inset: '#0f0f11',
|
||||||
chassis: '#242528'
|
chassis: '#242528'
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: '#ffffff',
|
primary: '#d4d4d8',
|
||||||
|
white: '#ffffff',
|
||||||
secondary: '#8e8e93',
|
secondary: '#8e8e93',
|
||||||
label: '#7c7c82',
|
label: '#7c7c82',
|
||||||
disabled: '#4a4a4e',
|
disabled: '#4a4a4e',
|
||||||
inactive: '#77797c',
|
inactive: '#77797c',
|
||||||
muted: '#3a3b3f'
|
muted: '#71717a'
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
amber: '#f25b29',
|
amber: '#ff5c35',
|
||||||
amberDim: 'rgba(242, 91, 41, 0.15)',
|
amberDim: 'rgba(255, 92, 53, 0.15)',
|
||||||
amberGlow: 'rgba(242, 91, 41, 0.6)'
|
amberGlow: 'rgba(255, 92, 53, 0.6)',
|
||||||
|
green: '#4ade80',
|
||||||
|
greenGlow: 'rgba(74, 222, 128, 0.6)'
|
||||||
},
|
},
|
||||||
border: {
|
border: {
|
||||||
subtle: 'rgba(255,255,255,0.04)',
|
subtle: 'rgba(255,255,255,0.04)',
|
||||||
default: 'rgba(255,255,255,0.08)',
|
default: '#2e2e32',
|
||||||
strong: 'rgba(255,255,255,0.12)'
|
strong: 'rgba(255,255,255,0.12)'
|
||||||
},
|
},
|
||||||
tag: {
|
tag: {
|
||||||
|
|
@ -61,5 +66,10 @@ export const d3roNativeRadius = {
|
||||||
pill: 999
|
pill: 999
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export const d3roNativeFonts = {
|
||||||
|
mono: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
||||||
|
sans: Platform.OS === 'ios' ? 'System' : 'Roboto'
|
||||||
|
} as const
|
||||||
|
|
||||||
export type D3roNativePaletteKey = keyof typeof d3roNativePalette
|
export type D3roNativePaletteKey = keyof typeof d3roNativePalette
|
||||||
export type D3roNativeTypoKey = keyof typeof d3roNativeTypo
|
export type D3roNativeTypoKey = keyof typeof d3roNativeTypo
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue