itsm_ysm/mobile/app/(auth)/login.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

76 lines
3.6 KiB
TypeScript

import { useState } from 'react'
import {
View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, ActivityIndicator, KeyboardAvoidingView, Platform,
} from 'react-native'
import { router } from 'expo-router'
import { useTheme } from '../../constants/ThemeContext'
import { type ThemeTokens } from '../../constants/ItmsTheme'
import { itmsLogin, itmsErrorMessage } from '../itmsApi'
/*
* ITMS 로그인 — 레거시 OAuth2 password grant(웹 front 와 동일 계약).
* userId + password → POST /oauth/token → 토큰/프로필 저장 → /home.
* 비밀번호는 웹과 동일하게 평문 전송(TLS 보호). 클라 암호화 안 함.
*/
export default function Login() {
const { t: th } = useTheme()
const s = makeStyles(th)
const [userId, setUserId] = useState('')
const [password, setPassword] = useState('')
const [busy, setBusy] = useState(false)
const [err, setErr] = useState('')
const doLogin = async () => {
if (!userId.trim() || !password) { setErr('아이디와 비밀번호를 입력하세요.'); return }
setBusy(true); setErr('')
try {
await itmsLogin(userId.trim(), password)
router.replace('/home')
} catch (e) { setErr(itmsErrorMessage(e)) }
finally { setBusy(false) }
}
return (
<KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<ScrollView style={s.wrap} contentContainerStyle={s.content} keyboardShouldPersistTaps="handled">
<Text style={s.logo}>ITMS</Text>
<Text style={s.sub}>IT </Text>
<View style={s.card}>
<Text style={s.lbl}></Text>
<TextInput
style={s.input} value={userId} onChangeText={setUserId}
placeholder="사용자 아이디" placeholderTextColor={th.muted}
autoCapitalize="none" autoCorrect={false}
/>
<Text style={[s.lbl, { marginTop: 12 }]}></Text>
<TextInput
style={s.input} value={password} onChangeText={setPassword}
placeholder="비밀번호" placeholderTextColor={th.muted} secureTextEntry
onSubmitEditing={doLogin} returnKeyType="go"
/>
{!!err && <Text style={s.err}>{err}</Text>}
<TouchableOpacity style={[s.btn, busy && { opacity: 0.5 }]} onPress={doLogin} disabled={busy}>
{busy ? <ActivityIndicator color={th.onBrand} /> : <Text style={s.btnText}></Text>}
</TouchableOpacity>
</View>
<Text style={s.foot}> · WISE Platform</Text>
</ScrollView>
</KeyboardAvoidingView>
)
}
const makeStyles = (th: ThemeTokens) => StyleSheet.create({
wrap: { flex: 1, backgroundColor: th.bg },
content: { flexGrow: 1, justifyContent: 'center', padding: 24 },
logo: { color: th.primary, fontSize: 44, fontWeight: '900', textAlign: 'center', letterSpacing: 3 },
sub: { color: th.muted, fontSize: 13, textAlign: 'center', marginTop: 6, marginBottom: 28 },
card: { backgroundColor: th.card, borderRadius: 16, borderWidth: 1, borderColor: th.border, padding: 20 },
lbl: { color: th.muted, fontSize: 12, marginBottom: 6 },
input: { backgroundColor: th.cardAlt, borderRadius: 10, borderWidth: 1, borderColor: th.border, color: th.text, padding: 13, fontSize: 15 },
btn: { backgroundColor: th.brand, borderRadius: 12, paddingVertical: 14, alignItems: 'center', marginTop: 20 },
btnText: { color: th.onBrand, fontWeight: '800', fontSize: 15 },
err: { color: th.danger, fontSize: 12, marginTop: 12 },
foot: { color: th.muted, fontSize: 11, textAlign: 'center', marginTop: 28 },
})