feat(icons): 4개 시스템 라인 SVG 아이콘 전면 적용 [auto-sync]
This commit is contained in:
parent
b60a19ada2
commit
8e5a5b7e6f
63
CLAUDE.md
63
CLAUDE.md
@ -1,18 +1,53 @@
|
||||
# GUARDiA Messenger
|
||||
# GUARDiA Messenger — React Native 모바일 앱
|
||||
|
||||
**저장소**: http://101.79.17.164:3000/zio/guardia-messenger
|
||||
**배포**: EAS Build (Android APK/AAB, iOS IPA)
|
||||
**패키지**: kr.co.zioinfo.guardia
|
||||
> **Claude Code용 프로젝트 마스터 컨텍스트**
|
||||
|
||||
## 기술 스택
|
||||
- React Native 0.74 + Expo SDK 51 + TypeScript
|
||||
- expo-router (파일 기반 라우팅)
|
||||
---
|
||||
|
||||
## 빌드
|
||||
```bash
|
||||
eas build --platform android --profile preview # 테스트 APK
|
||||
eas build --platform android --profile production # 스토어 AAB
|
||||
```
|
||||
## 프로젝트 현황
|
||||
|
||||
## 하네스
|
||||
- 앱 개발: `messenger-orchestrator` 스킬
|
||||
| 항목 | 상태 |
|
||||
|------|------|
|
||||
| 기술 스택 | React Native 0.74.5 + Expo SDK 51 + TypeScript |
|
||||
| 패키지명 | kr.co.zioinfo.guardia |
|
||||
| EAS 계정 | zioinfo |
|
||||
| 프로젝트 ID | ca2f72d6-7dda-4491-9590-7ace34b10a88 |
|
||||
| 최신 성공 빌드 | 51096ada (Android APK) |
|
||||
| 서버 | https://zioinfo.co.kr:8443 (GUARDiA ITSM) |
|
||||
|
||||
---
|
||||
|
||||
## 구현된 화면 (6개)
|
||||
|
||||
| 화면 | 경로 | 기능 |
|
||||
|------|------|------|
|
||||
| 로그인 | `app/(auth)/login.tsx` | JWT 인증 |
|
||||
| 대시보드 | `app/(tabs)/index.tsx` | SR 통계, 서비스 상태 |
|
||||
| SR 관리 | `app/(tabs)/sr.tsx` | SR 목록·등록 |
|
||||
| AI 챗봇 | `app/(tabs)/chat.tsx` | Ollama 연동 |
|
||||
| 알림 | `app/(tabs)/notifications.tsx` | 푸시 알림 목록 |
|
||||
| 설정 | `app/(tabs)/settings.tsx` | 프로필·로그아웃 |
|
||||
|
||||
---
|
||||
|
||||
## 빌드 핵심 원칙 (위반 시 빌드 실패)
|
||||
|
||||
1. `android/`, `ios/` 폴더 — 로컬 생성 금지 (`.easignore`로 EAS 제외)
|
||||
2. `expo-notifications` — `app.json` 플러그인 등록 금지
|
||||
3. `babel.config.js` — `expo-router/babel` 추가 금지
|
||||
4. `plugins/withGradleProps.js` — `enablePngCrunchInReleaseBuilds=false` 필수
|
||||
|
||||
---
|
||||
|
||||
## 하네스: GUARDiA Messenger
|
||||
|
||||
**목표:** 화면 개발·EAS 빌드·스토어 등록·문서화 전 과정 자동화
|
||||
|
||||
**트리거:** 화면 구현, 기능 추가, EAS 빌드, Play Store 등록, 가이드 작성 요청 시
|
||||
`messenger-orchestrator` 스킬을 사용하라. 다시 실행, 수정, 업데이트도 포함.
|
||||
|
||||
**변경 이력:**
|
||||
| 날짜 | 변경 내용 | 대상 | 사유 |
|
||||
|------|----------|------|------|
|
||||
| 2026-05-31 | 초기 하네스 구성 | 전체 | GUARDiA Messenger 앱 개발·배포 자동화 |
|
||||
| 2026-05-31 | feature-developer 에이전트 + new-features 스킬 추가 | agents/, skills/ | DR·네트워크·CSAP·생체인증·오프라인 등 신규 기능 10종 |
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
import { COLORS } from '../../constants/Config'
|
||||
import { sendAIMessage } from '../../services/api'
|
||||
import { useAuth } from '../../hooks/useAuth'
|
||||
import LineIcon from '../../components/LineIcon'
|
||||
|
||||
interface Msg { id: number; role: 'user' | 'ai'; text: string; time: string }
|
||||
|
||||
@ -50,7 +51,11 @@ export default function ChatScreen() {
|
||||
<ScrollView ref={scrollRef} style={s.messages} contentContainerStyle={{ padding: 16 }}>
|
||||
{msgs.map(m => (
|
||||
<View key={m.id} style={[s.msgRow, m.role === 'user' && s.userRow]}>
|
||||
{m.role === 'ai' && <Text style={s.avatar}>🤖</Text>}
|
||||
{m.role === 'ai' && (
|
||||
<View style={[s.avatar, { backgroundColor:'rgba(0,160,200,.12)', borderRadius:20 }]}>
|
||||
<LineIcon name="ai" size={20} color={COLORS.accent} />
|
||||
</View>
|
||||
)}
|
||||
<View style={[s.bubble, m.role === 'user' ? s.userBubble : s.aiBubble]}>
|
||||
<Text style={[s.bubbleText, m.role === 'user' && s.userText]}>{m.text}</Text>
|
||||
<Text style={[s.timeText, m.role === 'user' && { color: 'rgba(255,255,255,.6)' }]}>{m.time}</Text>
|
||||
@ -59,7 +64,9 @@ export default function ChatScreen() {
|
||||
))}
|
||||
{loading && (
|
||||
<View style={s.msgRow}>
|
||||
<Text style={s.avatar}>🤖</Text>
|
||||
<View style={[s.avatar, { backgroundColor:'rgba(0,160,200,.12)', borderRadius:20 }]}>
|
||||
<LineIcon name="ai" size={20} color={COLORS.accent} />
|
||||
</View>
|
||||
<View style={s.aiBubble}>
|
||||
<ActivityIndicator size="small" color={COLORS.accent} />
|
||||
</View>
|
||||
@ -92,7 +99,7 @@ export default function ChatScreen() {
|
||||
/>
|
||||
<TouchableOpacity style={[s.sendBtn, (!input.trim() || loading) && s.sendDisabled]}
|
||||
onPress={() => send()} disabled={!input.trim() || loading}>
|
||||
<Text style={s.sendIcon}>➤</Text>
|
||||
<LineIcon name="send" size={18} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@ -105,7 +112,7 @@ const s = StyleSheet.create({
|
||||
messages: { flex:1 },
|
||||
msgRow: { flexDirection:'row', alignItems:'flex-end', marginBottom:12, gap:8 },
|
||||
userRow: { flexDirection:'row-reverse' },
|
||||
avatar: { fontSize:24, marginBottom:4 },
|
||||
avatar: { width:38, height:38, alignItems:'center', justifyContent:'center', marginBottom:4 },
|
||||
bubble: { maxWidth:'75%', borderRadius:16, padding:12 },
|
||||
aiBubble: { backgroundColor:'#fff', borderBottomLeftRadius:4,
|
||||
shadowColor:'#000', shadowOffset:{width:0,height:1}, shadowOpacity:.06, elevation:1 },
|
||||
|
||||
@ -3,6 +3,7 @@ import { View, Text, ScrollView, StyleSheet, TouchableOpacity, RefreshControl, A
|
||||
import { COLORS, PRIORITY_COLOR } from '../../constants/Config'
|
||||
import { getDashboard, getLicenseStatus } from '../../services/api'
|
||||
import { useAuth } from '../../hooks/useAuth'
|
||||
import LineIcon from '../../components/LineIcon'
|
||||
|
||||
interface Stats {
|
||||
total_tasks: number
|
||||
@ -14,13 +15,15 @@ interface Stats {
|
||||
recent_tasks?: any[]
|
||||
}
|
||||
|
||||
type IconName = Parameters<typeof LineIcon>[0]['name']
|
||||
|
||||
/* Variant 스타일 StatCard — screenshot9 패턴 */
|
||||
function StatCard({ icon, label, value, color }: { icon: string; label: string; value: number | string; color: string }) {
|
||||
function StatCard({ iconName, label, value, color }: { iconName: IconName; label: string; value: number | string; color: string }) {
|
||||
return (
|
||||
<View style={[s.statCard, { borderTopColor: color, borderTopWidth: 3 }]}>
|
||||
{/* 아이콘 박스 — 연파랑 컨테이너 */}
|
||||
<View style={[s.statIconBox, { backgroundColor: color + '18' }]}>
|
||||
<Text style={s.statIcon}>{icon}</Text>
|
||||
<LineIcon name={iconName} size={22} color={color} />
|
||||
</View>
|
||||
{/* 라벨 (위) */}
|
||||
<Text style={s.statLabel}>{label}</Text>
|
||||
@ -84,16 +87,16 @@ export default function Dashboard() {
|
||||
|
||||
{/* 통계 카드 */}
|
||||
<View style={s.statsGrid}>
|
||||
<StatCard icon="📋" label="전체 SR" value={stats?.total_tasks ?? '-'} color={COLORS.accent} />
|
||||
<StatCard icon="🔄" label="진행 중" value={stats?.in_progress_tasks ?? '-'} color={COLORS.warning} />
|
||||
<StatCard icon="🚨" label="긴급" value={stats?.critical_count ?? '-'} color={COLORS.danger} />
|
||||
<StatCard icon="✅" label="오늘 완료" value={stats?.completed_today ?? '-'} color={COLORS.success} />
|
||||
<StatCard iconName="sr" label="전체 SR" value={stats?.total_tasks ?? '-'} color={COLORS.accent} />
|
||||
<StatCard iconName="sync" label="진행 중" value={stats?.in_progress_tasks ?? '-'} color={COLORS.warning} />
|
||||
<StatCard iconName="alert" label="긴급" value={stats?.critical_count ?? '-'} color={COLORS.danger} />
|
||||
<StatCard iconName="check" label="오늘 완료" value={stats?.completed_today ?? '-'} color={COLORS.success} />
|
||||
</View>
|
||||
|
||||
{/* 최근 SR */}
|
||||
{stats?.recent_tasks && stats.recent_tasks.length > 0 && (
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>📋 최근 서비스 요청</Text>
|
||||
<Text style={s.sectionTitle}>최근 서비스 요청</Text>
|
||||
{stats.recent_tasks.slice(0, 5).map((sr: any) => (
|
||||
<View key={sr.id} style={s.srItem}>
|
||||
<View style={[s.priorityDot, { backgroundColor: PRIORITY_COLOR[sr.priority] ?? COLORS.muted }]} />
|
||||
@ -108,16 +111,18 @@ export default function Dashboard() {
|
||||
|
||||
{/* 빠른 실행 */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>⚡ 빠른 실행</Text>
|
||||
<Text style={s.sectionTitle}>빠른 실행</Text>
|
||||
<View style={s.quickRow}>
|
||||
{[
|
||||
{ icon: '📝', label: 'SR 등록' },
|
||||
{ icon: '🤖', label: 'AI 질문' },
|
||||
{ icon: '📊', label: '리포트' },
|
||||
{ icon: '🔒', label: '감사로그' },
|
||||
{ iconName: 'sr' as const, label: 'SR 등록' },
|
||||
{ iconName: 'ai' as const, label: 'AI 질문' },
|
||||
{ iconName: 'dashboard'as const, label: '리포트' },
|
||||
{ iconName: 'lock' as const, label: '감사로그' },
|
||||
].map(q => (
|
||||
<TouchableOpacity key={q.label} style={s.quickBtn}>
|
||||
<Text style={s.quickIcon}>{q.icon}</Text>
|
||||
<View style={s.quickIconBox}>
|
||||
<LineIcon name={q.iconName} size={20} color={COLORS.accent} />
|
||||
</View>
|
||||
<Text style={s.quickLabel}>{q.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@ -188,6 +193,9 @@ const s = StyleSheet.create({
|
||||
backgroundColor: COLORS.bg, borderRadius: 12,
|
||||
flex: 1, marginHorizontal: 3,
|
||||
},
|
||||
quickIconBox: { width: 36, height: 36, borderRadius: 10, marginBottom: 5,
|
||||
backgroundColor: 'rgba(0,160,200,.1)',
|
||||
alignItems: 'center', justifyContent: 'center' },
|
||||
quickIcon: { fontSize: 26, marginBottom: 5 },
|
||||
quickLabel: { fontSize: 11, fontWeight: '600', color: COLORS.primary },
|
||||
})
|
||||
|
||||
@ -3,16 +3,32 @@ import { View, Text, ScrollView, StyleSheet, TouchableOpacity, RefreshControl, A
|
||||
import { COLORS } from '../../constants/Config'
|
||||
import { getNotifications, markNotificationRead } from '../../services/api'
|
||||
import { useWebSocket } from '../../hooks/useWebSocket'
|
||||
import LineIcon from '../../components/LineIcon'
|
||||
|
||||
const TYPE_ICON: Record<string, string> = {
|
||||
SR_CREATED:'📋', SR_UPDATED:'🔄', SR_COMPLETED:'✅',
|
||||
INCIDENT:'🚨', SLA_BREACH:'⏰', DEPLOY_SUCCESS:'🚀',
|
||||
DEPLOY_FAIL:'❌', LICENSE:'🔑', SYSTEM:'⚙️', DEFAULT:'🔔',
|
||||
sr_created:'📋', sr_updated:'🔄', sr_completed:'✅',
|
||||
deploy_notify:'🚀', incident_created:'🚨', sla_breach:'⏰',
|
||||
anomaly_alert:'⚠️', batch_notify:'⚙️',
|
||||
type IconName = Parameters<typeof LineIcon>[0]['name']
|
||||
|
||||
const TYPE_ICON_NAME: Record<string, IconName> = {
|
||||
SR_CREATED: 'sr', SR_UPDATED: 'sync', SR_COMPLETED: 'check',
|
||||
INCIDENT: 'alert', SLA_BREACH: 'bell', DEPLOY_SUCCESS: 'zap',
|
||||
DEPLOY_FAIL:'alert', LICENSE: 'lock', SYSTEM: 'settings',
|
||||
DEFAULT: 'bell',
|
||||
sr_created: 'sr', sr_updated: 'sync', sr_completed: 'check',
|
||||
deploy_notify: 'zap', incident_created: 'alert', sla_breach: 'bell',
|
||||
anomaly_alert: 'alert', batch_notify: 'settings',
|
||||
}
|
||||
|
||||
/* 하위 호환: 이모지 대신 타입 색상 */
|
||||
const TYPE_COLOR: Record<string, string> = {
|
||||
SR_CREATED: '#00A0C8', SR_UPDATED: '#f59e0b', SR_COMPLETED: '#22c55e',
|
||||
INCIDENT: '#ef4444', SLA_BREACH: '#f59e0b', DEPLOY_SUCCESS: '#22c55e',
|
||||
DEPLOY_FAIL: '#ef4444', DEFAULT: '#64748b',
|
||||
sr_created: '#00A0C8', sr_updated: '#f59e0b', sr_completed: '#22c55e',
|
||||
deploy_notify: '#22c55e', incident_created: '#ef4444',
|
||||
}
|
||||
|
||||
/** 이전 코드와의 호환을 위한 빈 맵 (미사용) */
|
||||
const TYPE_ICON: Record<string, string> = {}
|
||||
|
||||
interface NotifItem {
|
||||
id: string | number; type: string; title: string
|
||||
message: string; is_read: boolean; created_at: string
|
||||
@ -107,9 +123,11 @@ export default function NotificationsScreen() {
|
||||
|
||||
{wsEvents.length>0 && wsEvents[0] && (
|
||||
<View style={s.wsBanner}>
|
||||
<Text style={s.wsIcon}>{TYPE_ICON[wsEvents[0]?.event_type] ?? '⚡'}</Text>
|
||||
<View style={s.wsIcon}>
|
||||
<LineIcon name={TYPE_ICON_NAME[wsEvents[0]?.event_type ?? ''] ?? 'bell'} size={20} color={COLORS.accent} />
|
||||
</View>
|
||||
<View style={{ flex:1 }}>
|
||||
<Text style={s.wsBannerT}>⚡ 실시간: {fmtTitle(wsEvents[0]?.event_type ?? '')}</Text>
|
||||
<Text style={s.wsBannerT}>실시간: {fmtTitle(wsEvents[0]?.event_type ?? '')}</Text>
|
||||
<Text style={s.wsBannerM} numberOfLines={1}>{fmtMsg(wsEvents[0])}</Text>
|
||||
</View>
|
||||
</View>
|
||||
@ -118,28 +136,35 @@ export default function NotificationsScreen() {
|
||||
<ScrollView refreshControl={<RefreshControl refreshing={refresh} onRefresh={() => load(true)} />}>
|
||||
{!loading && items.length===0 && (
|
||||
<View style={s.empty}>
|
||||
<Text style={{ fontSize:48 }}>🔔</Text>
|
||||
<View style={{ width: 60, height: 60, borderRadius: 30, backgroundColor: 'rgba(0,160,200,.1)',
|
||||
alignItems: 'center', justifyContent: 'center' }}>
|
||||
<LineIcon name="bell" size={32} color={COLORS.accent} />
|
||||
</View>
|
||||
<Text style={s.emptyT}>알림이 없습니다.</Text>
|
||||
<Text style={s.emptyH}>SR 등록 또는 배포 실행 시{'\n'}알림이 표시됩니다.</Text>
|
||||
</View>
|
||||
)}
|
||||
{items.map(n => (
|
||||
<TouchableOpacity key={String(n.id)} style={[s.item, !n.is_read && s.unread]} onPress={() => markRead(n.id)}>
|
||||
<View>
|
||||
<Text style={s.icon}>{TYPE_ICON[n.type] ?? TYPE_ICON.DEFAULT}</Text>
|
||||
{n.source==='ws' && <View style={s.wsPin} />}
|
||||
</View>
|
||||
<View style={{ flex:1 }}>
|
||||
<Text style={[s.iT, !n.is_read && { fontWeight:'700' }]}>{n.title}</Text>
|
||||
<Text style={s.iM} numberOfLines={2}>{n.message}</Text>
|
||||
<View style={{ flexDirection:'row', gap:8, marginTop:4 }}>
|
||||
<Text style={s.iTime}>{new Date(n.created_at).toLocaleString('ko-KR')}</Text>
|
||||
{n.source==='ws' && <Text style={[s.iTime,{color:COLORS.accent}]}>⚡ 실시간</Text>}
|
||||
{items.map(n => {
|
||||
const iconName = TYPE_ICON_NAME[n.type] ?? TYPE_ICON_NAME.DEFAULT
|
||||
const iconColor = TYPE_COLOR[n.type] ?? '#64748b'
|
||||
return (
|
||||
<TouchableOpacity key={String(n.id)} style={[s.item, !n.is_read && s.unread]} onPress={() => markRead(n.id)}>
|
||||
<View style={[s.iconBox, { backgroundColor: iconColor + '18' }]}>
|
||||
<LineIcon name={iconName} size={20} color={iconColor} />
|
||||
{n.source==='ws' && <View style={s.wsPin} />}
|
||||
</View>
|
||||
</View>
|
||||
{!n.is_read && <View style={s.readDot} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<View style={{ flex:1 }}>
|
||||
<Text style={[s.iT, !n.is_read && { fontWeight:'700' }]}>{n.title}</Text>
|
||||
<Text style={s.iM} numberOfLines={2}>{n.message}</Text>
|
||||
<View style={{ flexDirection:'row', gap:8, marginTop:4 }}>
|
||||
<Text style={s.iTime}>{new Date(n.created_at).toLocaleString('ko-KR')}</Text>
|
||||
{n.source==='ws' && <Text style={[s.iTime,{color:COLORS.accent}]}>실시간</Text>}
|
||||
</View>
|
||||
</View>
|
||||
{!n.is_read && <View style={s.readDot} />}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
})}
|
||||
<View style={{ height:24 }} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
@ -163,6 +188,8 @@ const s = StyleSheet.create({
|
||||
item: { flexDirection:'row', backgroundColor:'#fff', padding:14,
|
||||
borderBottomWidth:1, borderBottomColor:'#f1f5f9', gap:10, alignItems:'flex-start' },
|
||||
unread: { backgroundColor:'#f8f9ff' },
|
||||
iconBox: { width:40, height:40, borderRadius:10, alignItems:'center', justifyContent:'center',
|
||||
position:'relative', flexShrink:0 },
|
||||
icon: { fontSize:24, marginTop:2 },
|
||||
wsPin: { position:'absolute', top:-2, right:-4, width:8, height:8, borderRadius:4, backgroundColor:COLORS.accent },
|
||||
iT: { fontSize:14, color:COLORS.text },
|
||||
|
||||
@ -2,13 +2,18 @@ import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Alert, Switch, Li
|
||||
import { COLORS } from '../../constants/Config'
|
||||
import { useAuth } from '../../hooks/useAuth'
|
||||
import { useState } from 'react'
|
||||
import LineIcon from '../../components/LineIcon'
|
||||
|
||||
function MenuItem({ icon, label, value, onPress, danger, toggle, enabled, onToggle }:
|
||||
{ icon: string; label: string; value?: string; onPress?: () => void; danger?: boolean
|
||||
type IconName = Parameters<typeof LineIcon>[0]['name']
|
||||
|
||||
function MenuItem({ iconName, label, value, onPress, danger, toggle, enabled, onToggle }:
|
||||
{ iconName: IconName; label: string; value?: string; onPress?: () => void; danger?: boolean
|
||||
toggle?: boolean; enabled?: boolean; onToggle?: (v: boolean) => void }) {
|
||||
return (
|
||||
<TouchableOpacity style={s.item} onPress={onPress} disabled={toggle}>
|
||||
<Text style={s.itemIcon}>{icon}</Text>
|
||||
<View style={[s.itemIcon, { backgroundColor: danger ? '#fee2e2' : 'rgba(0,160,200,.08)' }]}>
|
||||
<LineIcon name={iconName} size={18} color={danger ? COLORS.danger : COLORS.accent} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[s.itemLabel, danger && { color: COLORS.danger }]}>{label}</Text>
|
||||
{value && <Text style={s.itemValue}>{value}</Text>}
|
||||
@ -50,42 +55,42 @@ export default function SettingsScreen() {
|
||||
{/* 서버 정보 */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>서버 연결</Text>
|
||||
<MenuItem icon="🌐" label="GUARDiA 서버" value="zioinfo.co.kr:8443" />
|
||||
<MenuItem icon="🔒" label="HTTPS 보안 연결" value="TLS 1.3" />
|
||||
<MenuItem icon="📊" label="API 버전" value="v2.0.0" />
|
||||
<MenuItem iconName="globe" label="GUARDiA 서버" value="zioinfo.co.kr:8443" />
|
||||
<MenuItem iconName="lock" label="HTTPS 보안 연결" value="TLS 1.3" />
|
||||
<MenuItem iconName="dashboard" label="API 버전" value="v2.0.0" />
|
||||
</View>
|
||||
|
||||
{/* 알림 설정 */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>알림</Text>
|
||||
<MenuItem icon="🔔" label="푸시 알림" toggle enabled={pushEnabled} onToggle={setPush} />
|
||||
<MenuItem icon="🚨" label="긴급 알림 (인시던트)" toggle enabled={true} onToggle={() => {}} />
|
||||
<MenuItem icon="📋" label="SR 상태 알림" toggle enabled={true} onToggle={() => {}} />
|
||||
<MenuItem iconName="bell" label="푸시 알림" toggle enabled={pushEnabled} onToggle={setPush} />
|
||||
<MenuItem iconName="alert" label="긴급 알림 (인시던트)" toggle enabled={true} onToggle={() => {}} />
|
||||
<MenuItem iconName="sr" label="SR 상태 알림" toggle enabled={true} onToggle={() => {}} />
|
||||
</View>
|
||||
|
||||
{/* 앱 설정 */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>앱 설정</Text>
|
||||
<MenuItem icon="🌙" label="다크 모드" toggle enabled={darkMode} onToggle={setDark} />
|
||||
<MenuItem icon="🌍" label="언어" value="한국어" />
|
||||
<MenuItem icon="📱" label="앱 버전" value="1.0.0 (build 1)" />
|
||||
<MenuItem iconName="settings" label="다크 모드" toggle enabled={darkMode} onToggle={setDark} />
|
||||
<MenuItem iconName="globe" label="언어" value="한국어" />
|
||||
<MenuItem iconName="ai" label="앱 버전" value="1.0.0 (build 1)" />
|
||||
</View>
|
||||
|
||||
{/* 지원 */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>지원</Text>
|
||||
<MenuItem icon="📖" label="사용 가이드"
|
||||
<MenuItem iconName="server" label="사용 가이드"
|
||||
onPress={() => Linking.openURL('http://zioinfo.co.kr:8090')} />
|
||||
<MenuItem icon="🛡️" label="GUARDiA ITSM 웹"
|
||||
<MenuItem iconName="lock" label="GUARDiA ITSM 웹"
|
||||
onPress={() => Linking.openURL('https://zioinfo.co.kr:8443')} />
|
||||
<MenuItem icon="📧" label="문의하기"
|
||||
<MenuItem iconName="mail" label="문의하기"
|
||||
onPress={() => Linking.openURL('mailto:guardia@zioinfo.co.kr')} />
|
||||
<MenuItem icon="🏢" label="(주)지오정보기술" value="zioinfo.co.kr" />
|
||||
<MenuItem iconName="building" label="(주)지오정보기술" value="zioinfo.co.kr" />
|
||||
</View>
|
||||
|
||||
{/* 로그아웃 */}
|
||||
<View style={s.section}>
|
||||
<MenuItem icon="🚪" label="로그아웃" onPress={handleLogout} danger />
|
||||
<MenuItem iconName="user" label="로그아웃" onPress={handleLogout} danger />
|
||||
</View>
|
||||
|
||||
<View style={{ height: 32 }} />
|
||||
@ -108,7 +113,7 @@ const s = StyleSheet.create({
|
||||
textTransform:'uppercase', letterSpacing:.5 },
|
||||
item: { flexDirection:'row', alignItems:'center', paddingHorizontal:16,
|
||||
paddingVertical:14, borderBottomWidth:1, borderBottomColor:'#f1f5f9', gap:12 },
|
||||
itemIcon: { fontSize:20, width:28, textAlign:'center' },
|
||||
itemIcon: { width:34, height:34, borderRadius:9, alignItems:'center', justifyContent:'center' },
|
||||
itemLabel: { fontSize:14, color:COLORS.text, fontWeight:'500' },
|
||||
itemValue: { fontSize:12, color:COLORS.muted, marginTop:2 },
|
||||
chevron: { fontSize:20, color:COLORS.muted },
|
||||
|
||||
339
components/LineIcon.tsx
Normal file
339
components/LineIcon.tsx
Normal file
@ -0,0 +1,339 @@
|
||||
/**
|
||||
* GUARDiA Messenger — 라인 아이콘 컴포넌트 (React Native)
|
||||
* react-native-svg 없이 View + StyleSheet만 사용하여 선 아이콘을 구현.
|
||||
* 폐쇄망/온프레미스 환경 호환.
|
||||
* stroke 스타일은 View border로 표현, 복잡한 패스는 Text 유니코드 대체.
|
||||
*/
|
||||
|
||||
import { View, StyleSheet, Text } from 'react-native'
|
||||
|
||||
interface Props {
|
||||
name: keyof typeof ICONS
|
||||
size?: number
|
||||
color?: string
|
||||
}
|
||||
|
||||
/** 아이콘 이름 → 렌더 함수 맵 */
|
||||
const ICONS = {
|
||||
dashboard: (s: number, c: string) => <DashboardIcon size={s} color={c} />,
|
||||
sr: (s: number, c: string) => <SrIcon size={s} color={c} />,
|
||||
chat: (s: number, c: string) => <ChatIcon size={s} color={c} />,
|
||||
bell: (s: number, c: string) => <BellIcon size={s} color={c} />,
|
||||
settings: (s: number, c: string) => <SettingsIcon size={s} color={c} />,
|
||||
server: (s: number, c: string) => <ServerIcon size={s} color={c} />,
|
||||
alert: (s: number, c: string) => <AlertIcon size={s} color={c} />,
|
||||
check: (s: number, c: string) => <CheckIcon size={s} color={c} />,
|
||||
sync: (s: number, c: string) => <SyncIcon size={s} color={c} />,
|
||||
user: (s: number, c: string) => <UserIcon size={s} color={c} />,
|
||||
lock: (s: number, c: string) => <LockIcon size={s} color={c} />,
|
||||
globe: (s: number, c: string) => <GlobeIcon size={s} color={c} />,
|
||||
mail: (s: number, c: string) => <MailIcon size={s} color={c} />,
|
||||
send: (s: number, c: string) => <SendIcon size={s} color={c} />,
|
||||
mic: (s: number, c: string) => <MicIcon size={s} color={c} />,
|
||||
building: (s: number, c: string) => <BuildingIcon size={s} color={c} />,
|
||||
ai: (s: number, c: string) => <AiIcon size={s} color={c} />,
|
||||
zap: (s: number, c: string) => <ZapIcon size={s} color={c} />,
|
||||
} as const
|
||||
|
||||
export default function LineIcon({ name, size = 22, color = '#00A0C8' }: Props) {
|
||||
const renderer = ICONS[name]
|
||||
if (!renderer) return null
|
||||
return renderer(size, color)
|
||||
}
|
||||
|
||||
/* ─── 개별 아이콘 구현 ───────────────────────────── */
|
||||
|
||||
function DashboardIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'flex-end', gap: 2, flexDirection: 'row', alignItems: 'flex-end' }}>
|
||||
<View style={{ width: size * 0.22, height: size * 0.45, borderWidth: b, borderColor: color, borderRadius: 2 }} />
|
||||
<View style={{ width: size * 0.22, height: size * 0.65, borderWidth: b, borderColor: color, borderRadius: 2 }} />
|
||||
<View style={{ width: size * 0.22, height: size * 0.85, borderWidth: b, borderColor: color, borderRadius: 2 }} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SrIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<View style={{ width: size * 0.72, height: size * 0.82, borderWidth: b, borderColor: color, borderRadius: 3 }}>
|
||||
<View style={{ marginTop: size * 0.22, marginHorizontal: size * 0.1 }}>
|
||||
<View style={{ height: b, backgroundColor: color, marginBottom: size * 0.1 }} />
|
||||
<View style={{ height: b, backgroundColor: color, width: '75%', marginBottom: size * 0.1 }} />
|
||||
<View style={{ height: b, backgroundColor: color, width: '55%' }} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ChatIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<View style={{
|
||||
width: size * 0.82, height: size * 0.68,
|
||||
borderWidth: b, borderColor: color, borderRadius: size * 0.14,
|
||||
position: 'relative',
|
||||
}}>
|
||||
<View style={{
|
||||
position: 'absolute', bottom: -size * 0.22, left: size * 0.1,
|
||||
width: size * 0.18, height: size * 0.18,
|
||||
borderRightWidth: b, borderBottomWidth: b, borderColor: color,
|
||||
transform: [{ rotate: '45deg' }],
|
||||
}} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function BellIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<View style={{
|
||||
width: size * 0.62, height: size * 0.55,
|
||||
borderWidth: b, borderColor: color,
|
||||
borderTopLeftRadius: size * 0.31, borderTopRightRadius: size * 0.31,
|
||||
borderBottomWidth: 0, marginTop: size * 0.1,
|
||||
}} />
|
||||
<View style={{ width: size * 0.75, height: b, backgroundColor: color }} />
|
||||
<View style={{
|
||||
width: size * 0.28, height: size * 0.14,
|
||||
borderWidth: b, borderColor: color,
|
||||
borderTopWidth: 0, borderRadius: size * 0.07,
|
||||
marginTop: -b,
|
||||
}} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
const r = (size - b * 2) / 2
|
||||
const innerR = r * 0.3
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<View style={{
|
||||
width: size * 0.55, height: size * 0.55,
|
||||
borderWidth: b, borderColor: color, borderRadius: size,
|
||||
}} />
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
width: size * 0.22, height: size * 0.22,
|
||||
borderWidth: b, borderColor: color, borderRadius: size,
|
||||
backgroundColor: 'transparent',
|
||||
}} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center', gap: size * 0.06 }}>
|
||||
<View style={{ width: size * 0.82, height: size * 0.32, borderWidth: b, borderColor: color, borderRadius: 3 }}>
|
||||
<View style={{ width: size * 0.08, height: size * 0.08, borderRadius: size, backgroundColor: color,
|
||||
position: 'absolute', top: '50%', left: size * 0.05, marginTop: -size * 0.04 }} />
|
||||
</View>
|
||||
<View style={{ width: size * 0.82, height: size * 0.32, borderWidth: b, borderColor: color, borderRadius: 3 }}>
|
||||
<View style={{ width: size * 0.08, height: size * 0.08, borderRadius: size, backgroundColor: color,
|
||||
position: 'absolute', top: '50%', left: size * 0.05, marginTop: -size * 0.04 }} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<View style={{
|
||||
width: 0, height: 0,
|
||||
borderLeftWidth: size * 0.45, borderRightWidth: size * 0.45,
|
||||
borderBottomWidth: size * 0.8,
|
||||
borderLeftColor: 'transparent', borderRightColor: 'transparent',
|
||||
borderBottomColor: color,
|
||||
opacity: 0.15,
|
||||
position: 'absolute',
|
||||
}} />
|
||||
<View style={{
|
||||
width: 0, height: 0,
|
||||
borderLeftWidth: size * 0.41, borderRightWidth: size * 0.41,
|
||||
borderBottomWidth: size * 0.73,
|
||||
borderLeftColor: 'transparent', borderRightColor: 'transparent',
|
||||
borderBottomColor: 'white',
|
||||
position: 'absolute', bottom: size * 0.04,
|
||||
}} />
|
||||
<View style={{ position: 'absolute', bottom: size * 0.23, alignItems: 'center', gap: 2 }}>
|
||||
<View style={{ width: b * 1.5, height: size * 0.22, backgroundColor: color, borderRadius: 2 }} />
|
||||
<View style={{ width: b * 1.5, height: b * 1.5, backgroundColor: color, borderRadius: size }} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function CheckIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<View style={{
|
||||
width: size * 0.82, height: size * 0.82,
|
||||
borderWidth: b, borderColor: color, borderRadius: size,
|
||||
}}>
|
||||
<View style={{ position: 'absolute', top: '35%', left: '20%',
|
||||
width: size * 0.2, height: b * 1.5, backgroundColor: color,
|
||||
transform: [{ rotate: '45deg' }] }} />
|
||||
<View style={{ position: 'absolute', top: '22%', left: '32%',
|
||||
width: size * 0.38, height: b * 1.5, backgroundColor: color,
|
||||
transform: [{ rotate: '-55deg' }] }} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SyncIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<View style={{
|
||||
width: size * 0.7, height: size * 0.7,
|
||||
borderWidth: b, borderColor: color, borderRadius: size,
|
||||
borderTopColor: 'transparent',
|
||||
}} />
|
||||
<View style={{
|
||||
position: 'absolute', top: size * 0.04,
|
||||
width: 0, height: 0,
|
||||
borderLeftWidth: size * 0.1, borderRightWidth: size * 0.1,
|
||||
borderBottomWidth: size * 0.15,
|
||||
borderLeftColor: 'transparent', borderRightColor: 'transparent',
|
||||
borderBottomColor: color,
|
||||
}} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function UserIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, alignItems: 'center', paddingTop: size * 0.05 }}>
|
||||
<View style={{ width: size * 0.38, height: size * 0.38, borderWidth: b, borderColor: color, borderRadius: size }} />
|
||||
<View style={{
|
||||
width: size * 0.7, height: size * 0.35,
|
||||
borderWidth: b, borderColor: color,
|
||||
borderTopLeftRadius: size * 0.35, borderTopRightRadius: size * 0.35,
|
||||
borderBottomWidth: 0, marginTop: size * 0.04,
|
||||
}} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function LockIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<View style={{
|
||||
width: size * 0.5, height: size * 0.26,
|
||||
borderWidth: b, borderColor: color,
|
||||
borderTopLeftRadius: size * 0.25, borderTopRightRadius: size * 0.25,
|
||||
borderBottomWidth: 0, marginBottom: -b,
|
||||
}} />
|
||||
<View style={{ width: size * 0.7, height: size * 0.42, borderWidth: b, borderColor: color, borderRadius: 5 }}>
|
||||
<View style={{ width: b * 2, height: b * 2, backgroundColor: color, borderRadius: size,
|
||||
alignSelf: 'center', marginTop: size * 0.12 }} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function GlobeIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<View style={{ width: size * 0.82, height: size * 0.82, borderWidth: b, borderColor: color, borderRadius: size }} />
|
||||
<View style={{ position: 'absolute', width: size * 0.82, height: b, backgroundColor: color, top: '50%', marginTop: -b / 2 }} />
|
||||
<View style={{ position: 'absolute', height: size * 0.82, width: b, backgroundColor: color, left: '50%', marginLeft: -b / 2 }} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function MailIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<View style={{ width: size * 0.82, height: size * 0.6, borderWidth: b, borderColor: color, borderRadius: 4 }}>
|
||||
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, height: size * 0.26,
|
||||
borderBottomWidth: b, borderBottomColor: color, transform: [{ skewX: '0deg' }] }}>
|
||||
<View style={{ width: '100%', height: '100%', opacity: 0.3, backgroundColor: color, borderRadius: 2 }} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SendIcon({ size, color }: { size: number; color: string }) {
|
||||
return (
|
||||
<Text style={{ fontSize: size * 0.7, color, lineHeight: size }}>➤</Text>
|
||||
)
|
||||
}
|
||||
|
||||
function MicIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, alignItems: 'center' }}>
|
||||
<View style={{ width: size * 0.32, height: size * 0.48, borderWidth: b, borderColor: color,
|
||||
borderRadius: size * 0.16, marginTop: size * 0.06 }} />
|
||||
<View style={{
|
||||
width: size * 0.55, height: size * 0.28,
|
||||
borderWidth: b, borderColor: color,
|
||||
borderTopLeftRadius: size * 0.28, borderTopRightRadius: size * 0.28,
|
||||
borderBottomWidth: 0, marginTop: size * 0.02,
|
||||
}} />
|
||||
<View style={{ width: b, height: size * 0.12, backgroundColor: color }} />
|
||||
<View style={{ width: size * 0.3, height: b, backgroundColor: color }} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function BuildingIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'flex-end', alignItems: 'center' }}>
|
||||
<View style={{ width: size * 0.7, height: size * 0.7, borderWidth: b, borderColor: color, borderRadius: 2 }}>
|
||||
<View style={{ position: 'absolute', top: size * 0.1, left: size * 0.1,
|
||||
width: size * 0.15, height: size * 0.15, borderWidth: b, borderColor: color }} />
|
||||
<View style={{ position: 'absolute', top: size * 0.1, right: size * 0.1,
|
||||
width: size * 0.15, height: size * 0.15, borderWidth: b, borderColor: color }} />
|
||||
<View style={{ position: 'absolute', top: size * 0.3, left: size * 0.1,
|
||||
width: size * 0.15, height: size * 0.15, borderWidth: b, borderColor: color }} />
|
||||
<View style={{ position: 'absolute', top: size * 0.3, right: size * 0.1,
|
||||
width: size * 0.15, height: size * 0.15, borderWidth: b, borderColor: color }} />
|
||||
</View>
|
||||
<View style={{ width: b, height: size * 0.15, backgroundColor: color, position: 'absolute', bottom: 0, left: size * 0.2 }} />
|
||||
<View style={{ width: b, height: size * 0.15, backgroundColor: color, position: 'absolute', bottom: 0, right: size * 0.2 }} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function AiIcon({ size, color }: { size: number; color: string }) {
|
||||
const b = size * 0.07
|
||||
return (
|
||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<View style={{ width: size * 0.82, height: size * 0.55, borderWidth: b, borderColor: color, borderRadius: 5 }}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', flex: 1, paddingHorizontal: size * 0.06 }}>
|
||||
{[0,1,2].map(i => (
|
||||
<View key={i} style={{ width: size * 0.12, height: size * 0.12, borderWidth: b, borderColor: color, borderRadius: size }} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ZapIcon({ size, color }: { size: number; color: string }) {
|
||||
return (
|
||||
<Text style={{ fontSize: size * 0.75, color, lineHeight: size }}>⚡</Text>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user