guardia-messenger/components/RejectReason.tsx

132 lines
5.2 KiB
TypeScript

import { useState } from 'react'
import {
Modal, View, Text, TextInput, TouchableOpacity,
StyleSheet, ActivityIndicator, KeyboardAvoidingView, Platform,
} from 'react-native'
import { COLORS } from '../constants/Config'
/**
* 기능 #66 — 반려 사유 입력 모달
* - 반려 사유 템플릿 빠른 선택 + 직접 입력
* - 최소 10자 검증 (빈 값/10자 미만이면 제출 차단)
*/
const MIN_LEN = 10
const TEMPLATES = [
'요청 정보가 불충분하여 반려합니다.',
'변경 일정이 운영 정책과 충돌합니다.',
'영향 범위 분석이 누락되어 보완이 필요합니다.',
'롤백 계획이 명시되지 않았습니다.',
'승인 권한 범위를 초과하는 요청입니다.',
]
interface Props {
visible: boolean
targetTitle?: string
onClose: () => void
/** 반려 확정 — reason 은 10자 이상 보장됨 */
onSubmit: (reason: string) => Promise<void> | void
}
export default function RejectReason({ visible, targetTitle, onClose, onSubmit }: Props) {
const [reason, setReason] = useState('')
const [submitting, setSubmitting] = useState(false)
const trimmed = reason.trim()
const valid = trimmed.length >= MIN_LEN
const reset = () => { setReason(''); setSubmitting(false) }
const handleClose = () => { reset(); onClose() }
const handleSubmit = async () => {
if (!valid || submitting) return
setSubmitting(true)
try {
await onSubmit(trimmed)
reset()
} catch {
setSubmitting(false)
}
}
return (
<Modal visible={visible} transparent animationType="slide" onRequestClose={handleClose}>
<KeyboardAvoidingView
style={s.overlay}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<View style={s.sheet}>
<View style={s.handle} />
<Text style={s.title}> </Text>
{!!targetTitle && <Text style={s.subtitle} numberOfLines={1}>{targetTitle}</Text>}
<Text style={s.label}> 릿</Text>
<View style={s.chips}>
{TEMPLATES.map((t, i) => (
<TouchableOpacity key={i} style={s.chip} onPress={() => setReason(t)}>
<Text style={s.chipText} numberOfLines={1}>{t}</Text>
</TouchableOpacity>
))}
</View>
<Text style={s.label}> ( {MIN_LEN})</Text>
<TextInput
style={s.input}
value={reason}
onChangeText={setReason}
placeholder="반려 사유를 입력하세요"
placeholderTextColor={COLORS.muted}
multiline
textAlignVertical="top"
/>
<Text style={[s.counter, valid ? s.counterOk : s.counterBad]}>
{trimmed.length} / {MIN_LEN} {valid ? '✔' : '(부족)'}
</Text>
<View style={s.actions}>
<TouchableOpacity style={[s.btn, s.btnGhost]} onPress={handleClose} disabled={submitting}>
<Text style={s.btnGhostText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[s.btn, s.btnDanger, (!valid || submitting) && s.btnDisabled]}
onPress={handleSubmit}
disabled={!valid || submitting}>
{submitting
? <ActivityIndicator color="#fff" size="small" />
: <Text style={s.btnDangerText}></Text>}
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
)
}
const s = StyleSheet.create({
overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'flex-end' },
sheet: { backgroundColor: '#fff', borderTopLeftRadius: 18, borderTopRightRadius: 18,
paddingHorizontal: 18, paddingTop: 10, paddingBottom: 28 },
handle: { alignSelf: 'center', width: 40, height: 4, borderRadius: 2,
backgroundColor: COLORS.border, marginBottom: 12 },
title: { fontSize: 16, fontWeight: '800', color: COLORS.text },
subtitle: { fontSize: 12, color: COLORS.muted, marginTop: 2 },
label: { fontSize: 12, fontWeight: '700', color: COLORS.text, marginTop: 16, marginBottom: 8 },
chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 },
chip: { backgroundColor: COLORS.light, borderRadius: 14, paddingHorizontal: 10,
paddingVertical: 6, maxWidth: '100%' },
chipText: { fontSize: 11, color: COLORS.blue },
input: { borderWidth: 1, borderColor: COLORS.border, borderRadius: 10, padding: 12,
minHeight: 90, fontSize: 13, color: COLORS.text, backgroundColor: '#fff' },
counter: { fontSize: 11, marginTop: 6, textAlign: 'right' },
counterOk: { color: COLORS.success },
counterBad: { color: COLORS.danger },
actions: { flexDirection: 'row', gap: 10, marginTop: 18 },
btn: { flex: 1, borderRadius: 10, paddingVertical: 13, alignItems: 'center' },
btnGhost: { backgroundColor: '#f1f5f9' },
btnGhostText: { color: COLORS.text, fontWeight: '700', fontSize: 14 },
btnDanger: { backgroundColor: COLORS.danger },
btnDangerText: { color: '#fff', fontWeight: '700', fontSize: 14 },
btnDisabled: { opacity: 0.45 },
})