import { useEffect, useState } from 'react' import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, ActivityIndicator, Alert, Image, ToastAndroid, Platform, } from 'react-native' import { router } from 'expo-router' import * as ImagePicker from 'expo-image-picker' import { COLORS, PRIORITY_COLOR } from '../../constants/Config' import { createSRRaw } from '../../services/api' import { useDuplicateSR } from '../../hooks/useDuplicateSR' import { useAIClassify } from '../../hooks/useAIClassify' import SRTemplates, { SRTemplate } from '../../components/SRTemplates' const CATEGORIES = ['DEPLOY', 'RESTART', 'LOG', 'INQUIRY', 'OTHER'] function toast(msg: string) { if (Platform.OS === 'android') ToastAndroid.show(msg, ToastAndroid.LONG) else Alert.alert(msg) } /** * 기능 #1 — 빠른 SR 등록 (3-tap: 제목 + 카테고리 + 사진) * + 기능 #9 중복 감지, #10 템플릿, #11 AI 자동 분류 연동 */ export default function SRQuickScreen() { const [title, setTitle] = useState('') const [category, setCategory] = useState('OTHER') const [priority, setPriority] = useState('MEDIUM') const [photo, setPhoto] = useState(null) const [saving, setSaving] = useState(false) const [tplOpen, setTplOpen] = useState(false) const [aiApplied, setAiApplied] = useState(false) const { duplicates, hasDuplicates } = useDuplicateSR(title) const ai = useAIClassify(title) // AI 분류 결과 자동 채움 (사용자가 아직 손대지 않았을 때만) useEffect(() => { if (!aiApplied && (ai.category || ai.priority)) { if (ai.category) setCategory(ai.category) if (ai.priority) setPriority(ai.priority) } }, [ai.category, ai.priority, aiApplied]) const pickPhoto = async () => { try { const perm = await ImagePicker.requestMediaLibraryPermissionsAsync() if (!perm.granted) { Alert.alert('권한 필요', '사진 접근 권한을 허용해주세요.'); return } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, quality: 0.6, }) if (!result.canceled && result.assets && result.assets[0]) { setPhoto(result.assets[0].uri) } } catch { Alert.alert('오류', '사진을 불러올 수 없습니다.') } } const applyTemplate = (tpl: SRTemplate) => { if (tpl.title) setTitle(tpl.title) if (tpl.category || tpl.sr_type) setCategory((tpl.category ?? tpl.sr_type)!) if (tpl.priority) setPriority(tpl.priority) setAiApplied(true) } const submit = async () => { if (!title.trim()) { Alert.alert('제목을 입력하세요.'); return } setSaving(true) try { const payload: Record = { title: title.trim(), sr_type: category, priority, description: '', } if (photo) payload.attachment_uri = photo const res = await createSRRaw(payload) const srId = res.data?.sr_id ?? res.data?.id ?? '' toast(`SR ${srId} 등록 완료`) router.back() } catch (e: any) { Alert.alert('오류', e.response?.data?.detail ?? 'SR 등록 실패') } finally { setSaving(false) } } return ( 빠른 SR 등록 setTplOpen(true)}> 📑 템플릿 {/* 1) 제목 */} ① 제목 * { setTitle(v); setAiApplied(false) }} placeholder="무엇을 요청하시나요?" placeholderTextColor={COLORS.muted} /> {ai.loading && 🤖 AI가 분류 중...} {!ai.loading && (ai.category || ai.priority) && ( 🤖 AI 추천: {ai.category} / {ai.priority} )} {/* 중복 경고 (#9) */} {hasDuplicates && ( ⚠️ 유사한 미처리 SR이 있습니다 {duplicates.map(d => ( router.push({ pathname: '/(tabs)/sr_detail', params: { id: String(d.id) } })} > • {d.sr_id ?? `#${d.id}`} {d.title} ))} )} {/* 2) 카테고리 */} ② 카테고리 {CATEGORIES.map(c => ( { setCategory(c); setAiApplied(true) }} > {c} ))} 우선순위 {['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].map(p => ( { setPriority(p); setAiApplied(true) }} > {p} ))} {/* 3) 사진 */} ③ 사진 첨부 {photo ? ( setPhoto(null)}> ✕ 삭제 ) : ( 📷 사진 선택 )} {saving ? : SR 등록} setTplOpen(false)} onSelect={applyTemplate} /> ) } const s = StyleSheet.create({ headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14 }, heading: { fontSize: 18, fontWeight: '800', color: COLORS.text }, tplBtn: { backgroundColor: COLORS.light, paddingHorizontal: 12, paddingVertical: 7, borderRadius: 8 }, tplBtnText: { color: COLORS.accent, fontWeight: '700', fontSize: 12 }, label: { fontSize: 12, fontWeight: '700', color: COLORS.muted, marginTop: 16, marginBottom: 7, textTransform: 'uppercase', letterSpacing: 0.4 }, input: { borderWidth: 1.5, borderColor: COLORS.border, borderRadius: 10, padding: 13, fontSize: 15, color: COLORS.text, backgroundColor: '#fff' }, aiHint: { fontSize: 12, color: COLORS.accent, marginTop: 6 }, dupBox: { backgroundColor: '#FFF7ED', borderRadius: 10, padding: 12, marginTop: 10, borderWidth: 1, borderColor: COLORS.warning }, dupTitle: { fontSize: 12, fontWeight: '700', color: COLORS.warning, marginBottom: 6 }, dupItem: { fontSize: 12, color: COLORS.text, paddingVertical: 3 }, chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, chip: { paddingHorizontal: 14, paddingVertical: 8, borderRadius: 20, borderWidth: 1, borderColor: COLORS.border, backgroundColor: '#fff' }, chipActive: { backgroundColor: COLORS.accent, borderColor: COLORS.accent }, chipText: { fontSize: 13, color: COLORS.text }, chipTextActive: { color: '#fff', fontWeight: '700' }, photoBtn: { borderWidth: 1.5, borderColor: COLORS.border, borderStyle: 'dashed', borderRadius: 10, padding: 22, alignItems: 'center', backgroundColor: '#fff' }, photoBtnText:{ color: COLORS.muted, fontSize: 14 }, photoWrap: { position: 'relative' }, photo: { width: '100%', height: 180, borderRadius: 10, backgroundColor: COLORS.border }, removePhoto: { position: 'absolute', top: 8, right: 8, backgroundColor: 'rgba(0,0,0,0.6)', paddingHorizontal: 10, paddingVertical: 5, borderRadius: 8 }, submit: { backgroundColor: COLORS.primary, borderRadius: 12, padding: 16, alignItems: 'center', marginTop: 28, marginBottom: 40 }, submitText: { color: '#fff', fontSize: 16, fontWeight: '800' }, })