itsm_ysm/mobile/app/request-new.tsx
itms-merge-dev efc3e66635 feat(itms): 모바일 앱 신규 (Expo, 12화면, OAuth password grant)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:10:46 +09:00

103 lines
4.4 KiB
TypeScript

import { useEffect, useState } from 'react'
import { ScrollView, StyleSheet, Text, View, TouchableOpacity, Alert } from 'react-native'
import { router } from 'expo-router'
import { useTheme } from '../constants/ThemeContext'
import { type ThemeTokens } from '../constants/ItmsTheme'
import { Field, Btn, ErrText, SectionTitle } from '../components/ui'
import { pick } from '../constants/rows'
import { itmsAddReqIncident, itmsServiceCombo, listRows, itmsErrorMessage, type RowMap } from './itmsApi'
/*
* 서비스 요청 등록(SvcDeskReqController /srm/addReqIncident.do — body {incidentReqVo:{...}}).
* 제목/내용 + (선택)서비스 선택. 백엔드 VO 컬럼명이 기관별 상이하므로 흔한 별칭을 함께 전송(방어적).
* 성공 판정: resultCd===0(또는 '0'). 실패 시 resultMsg 노출.
*/
export default function RequestNew() {
const { t } = useTheme()
const s = makeStyles(t)
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [services, setServices] = useState<RowMap[]>([])
const [svc, setSvc] = useState<RowMap | null>(null)
const [busy, setBusy] = useState(false)
const [err, setErr] = useState('')
useEffect(() => {
let alive = true
itmsServiceCombo()
.then((p) => { if (alive) setServices(listRows(p)) })
.catch(() => { /* 콤보 없으면 서비스 선택 생략 */ })
return () => { alive = false }
}, [])
const svcId = (r: RowMap) => pick(r, ['svcSeq', 'serviceSeq', 'svcId', 'serviceId', 'inSvcSeq', 'seq'])
const svcNm = (r: RowMap) => pick(r, ['svcNm', 'serviceNm', 'svcName', 'name'])
const submit = async () => {
if (!title.trim()) { setErr('제목을 입력하세요.'); return }
if (!content.trim()) { setErr('요청 내용을 입력하세요.'); return }
setBusy(true); setErr('')
// 흔한 컬럼 별칭을 함께 채워 기관별 스키마 편차 흡수.
const vo: RowMap = {
reqTitle: title.trim(), title: title.trim(), incidentTitle: title.trim(),
reqCn: content.trim(), contents: content.trim(), incidentCn: content.trim(), reqContents: content.trim(),
}
if (svc) {
const id = svcId(svc)
Object.assign(vo, { svcSeq: id, serviceSeq: id, svcId: id, serviceId: id, svcNm: svcNm(svc) })
}
try {
const res = await itmsAddReqIncident({ incidentReqVo: vo, fileList: [] })
const ok = res?.resultCd === 0 || res?.resultCd === '0'
if (ok || res?.inIncidentSeq) {
Alert.alert('완료', '서비스 요청이 등록되었습니다.', [{ text: '확인', onPress: () => router.back() }])
} else {
setErr(String(res?.resultMsg || '등록에 실패했습니다.'))
}
} catch (e) { setErr(itmsErrorMessage(e)) }
finally { setBusy(false) }
}
return (
<ScrollView style={s.wrap} contentContainerStyle={s.content} keyboardShouldPersistTaps="handled">
<Field label="제목" value={title} onChangeText={setTitle} placeholder="요청 제목" />
<Field label="요청 내용" value={content} onChangeText={setContent} placeholder="상세 내용을 입력하세요" multiline />
{services.length > 0 && (
<>
<SectionTitle style={s.mt}> ()</SectionTitle>
<View style={s.chips}>
{services.slice(0, 20).map((r, i) => {
const on = svc && svcId(svc) === svcId(r)
return (
<TouchableOpacity
key={i}
style={[s.chip, on && { borderColor: t.brand, backgroundColor: t.brand + '14' }]}
onPress={() => setSvc(on ? null : r)}
>
<Text style={[s.chipText, { color: on ? t.brand : t.muted }]} numberOfLines={1}>{svcNm(r) || svcId(r)}</Text>
</TouchableOpacity>
)
})}
</View>
</>
)}
<ErrText msg={err} />
<View style={{ marginTop: 22 }}>
<Btn label="등록" icon="send" busy={busy} onPress={submit} />
</View>
<View style={{ height: 24 }} />
</ScrollView>
)
}
const makeStyles = (t: ThemeTokens) => StyleSheet.create({
wrap: { flex: 1, backgroundColor: t.bg },
content: { padding: 16 },
mt: { marginTop: 20 },
chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
chip: { borderWidth: 1, borderColor: t.border, borderRadius: 20, paddingHorizontal: 14, paddingVertical: 8, backgroundColor: t.card, maxWidth: '100%' },
chipText: { fontSize: 13, fontWeight: '700' },
})