diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-13 18:24:00 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-13 18:24:00 +0900 |
| commit | 80e3d0befed487e0447bacffd76ed6539f01e992 (patch) | |
| tree | de5762bea7161e3dd949401b2d985b6723fd32ee /components | |
| parent | ff8a168f9fc67b345f4d32065e55f0901ba05b4c (diff) | |
(김준회) S-GIPS 로그인시 유저 선택해 sms 전송 처리
Diffstat (limited to 'components')
| -rw-r--r-- | components/login/login-form.tsx | 215 |
1 files changed, 186 insertions, 29 deletions
diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx index 8e9509c8..b0a0e574 100644 --- a/components/login/login-form.tsx +++ b/components/login/login-form.tsx @@ -24,6 +24,15 @@ import Loading from "../common/loading/loading"; type LoginMethod = 'username' | 'sgips'; +type OtpUser = { + name: string; + vndrcd: string; + phone: string; + email: string; + nation_cd: string; + userId: number; // 백엔드에서 생성된 로컬 DB 사용자 ID +}; + export function LoginForm() { const params = useParams() || {}; const pathname = usePathname() || ''; @@ -44,7 +53,7 @@ export function LoginForm() { const [showMfaForm, setShowMfaForm] = useState(false); const [mfaToken, setMfaToken] = useState(''); const [tempAuthKey, setTempAuthKey] = useState(''); - const [mfaUserId, setMfaUserId] = useState(null); + const [mfaUserId, setMfaUserId] = useState<number | null>(null); const [mfaUserEmail, setMfaUserEmail] = useState(''); const [mfaCountdown, setMfaCountdown] = useState(0); @@ -52,10 +61,15 @@ export function LoginForm() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); - // S-Gips 로그인 폼 데이터 + // S-Gips 로그인 폼 데이터 const [sgipsUsername, setSgipsUsername] = useState(''); const [sgipsPassword, setSgipsPassword] = useState(''); + // OTP 사용자 선택 관련 상태 + const [otpUsers, setOtpUsers] = useState<OtpUser[]>([]); + const [showUserSelectionDialog, setShowUserSelectionDialog] = useState(false); + const [selectedOtpUser, setSelectedOtpUser] = useState<OtpUser | null>(null); + const [isMfaLoading, setIsMfaLoading] = useState(false); const [isSmsLoading, setIsSmsLoading] = useState(false); @@ -189,25 +203,40 @@ export function LoginForm() { } }; - // SMS 토큰 전송 (userId 파라미터 추가) + // SMS 토큰 전송 const handleSendSms = async (userIdParam?: number) => { const targetUserId = userIdParam || mfaUserId; if (!targetUserId || mfaCountdown > 0) return; setIsSmsLoading(true); try { + const requestBody: any = { userId: targetUserId }; + + // S-GIPS 사용자인 경우 추가 정보 포함 + if (selectedOtpUser) { + requestBody.phone = selectedOtpUser.phone; + requestBody.name = selectedOtpUser.name; + } + const response = await fetch('/api/auth/send-sms', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ userId: targetUserId }), + body: JSON.stringify(requestBody), }); if (response.ok) { setMfaCountdown(60); - toast({ - title: t('smsSent'), - description: t('smsCodeSent'), - }); + if (selectedOtpUser) { + toast({ + title: t('smsSent'), + description: t('smsCodeSentTo', { name: selectedOtpUser.name, phone: selectedOtpUser.phone }), + }); + } else { + toast({ + title: t('smsSent'), + description: t('smsCodeSent'), + }); + } } else { const errorData = await response.json(); toast({ @@ -391,30 +420,24 @@ export function LoginForm() { const authResult = await performFirstAuth(sgipsUsername, sgipsPassword, 'sgips'); if (authResult.success) { - toast({ - title: t('sgipsAuthComplete'), - description: t('proceedingSmsAuth'), - }); - - // MFA 화면으로 전환 - setTempAuthKey(authResult.tempAuthKey); - setMfaUserId(authResult.userId); - setMfaUserEmail(authResult.email); - setShowMfaForm(true); - - // 자동으로 SMS 전송 (userId 직접 전달) - setTimeout(() => { - handleSendSms(authResult.userId); - }, 500); + const users = authResult.otpUsers || []; + + if (users.length === 0) { + toast({ + title: t('errorTitle'), + description: t('noUsersFound'), + variant: 'destructive', + }); + return; + } - toast({ - title: t('smsAuthStarted'), - description: t('sendingCodeToSgipsPhone'), - }); + // 사용자 선택 다이얼로그 표시 (항상) + setOtpUsers(users); + setShowUserSelectionDialog(true); } } catch (error: unknown) { console.error('S-Gips login error:', error); - + const errorMessage = getErrorMessage(error as { errorCode?: string; message?: string }, 'sgips'); toast({ @@ -427,6 +450,78 @@ export function LoginForm() { } }; + // 선택된 OTP 사용자와 함께 MFA 진행 + const proceedWithSelectedUser = async (user: OtpUser, tempAuthKey: string) => { + try { + // 사용자 정보를 기반으로 MFA 진행 + setTempAuthKey(tempAuthKey); + setSelectedOtpUser(user); + setMfaUserId(user.userId); // 선택된 사용자의 userId 설정 + setMfaUserEmail(user.email); + setShowMfaForm(true); + + // 선택된 사용자의 정보를 이용해 SMS 전송 준비 + // 실제로는 userId가 필요하므로 API에서 받아와야 함 + // 여기서는 임시로 user 객체를 저장하고 SMS 전송 시 사용 + setTimeout(() => { + // 실제 구현에서는 user 정보를 기반으로 SMS 전송 + // 현재는 기존 로직 유지하되, 선택된 사용자 정보 활용 + handleSendSms(); + }, 500); + + toast({ + title: t('sgipsAuthComplete'), + description: t('sendingCodeToSelectedUser', { name: user.name }), + }); + } catch (error) { + console.error('Proceeding with selected user error:', error); + toast({ + title: t('errorTitle'), + description: t('mfaSetupError'), + variant: 'destructive', + }); + } + }; + + // OTP 사용자 선택 처리 + const handleUserSelection = async (user: OtpUser) => { + setShowUserSelectionDialog(false); + + try { + // 선택된 사용자에 대한 임시 인증 세션 생성 요청 + const response = await fetch('/api/auth/select-sgips-user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: user.userId, + email: user.email, + name: user.name + }), + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + toast({ + title: t('errorTitle'), + description: result.error || t('mfaSetupError'), + variant: 'destructive', + }); + return; + } + + // 임시 인증 세션 생성 성공, MFA 진행 + await proceedWithSelectedUser(user, result.tempAuthKey); + } catch (error) { + console.error('User selection error:', error); + toast({ + title: t('errorTitle'), + description: t('mfaSetupError'), + variant: 'destructive', + }); + } + }; + // MFA 화면에서 뒤로 가기 const handleBackToLogin = () => { setShowMfaForm(false); @@ -435,6 +530,9 @@ export function LoginForm() { setMfaUserId(null); setMfaUserEmail(''); setMfaCountdown(0); + setSelectedOtpUser(null); + setShowUserSelectionDialog(false); + setOtpUsers([]); }; // 세션 로딩 중이거나 이미 인증된 상태에서는 로딩 표시 @@ -491,7 +589,10 @@ export function LoginForm() { </div> <h1 className="text-2xl font-bold">{t('smsVerification')}</h1> <p className="text-sm text-muted-foreground mt-2"> - {t('firstAuthCompleteFor', { email: mfaUserEmail })} + {selectedOtpUser + ? t('firstAuthCompleteForSgips', { name: selectedOtpUser.name, email: mfaUserEmail }) + : t('firstAuthCompleteFor', { email: mfaUserEmail }) + } </p> <p className="text-xs text-muted-foreground mt-1"> {t('enterSixDigitCodeInstructions')} @@ -738,6 +839,62 @@ export function LoginForm() { </div> )} + {/* OTP 사용자 선택 다이얼로그 */} + {showUserSelectionDialog && !showMfaForm && ( + <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> + <div className="bg-white rounded-lg p-6 w-full max-w-md mx-4 max-h-[80vh] overflow-y-auto"> + <div className="flex justify-between items-center mb-4"> + <h3 className="text-lg font-semibold">{t('selectUser')}</h3> + <button + onClick={() => setShowUserSelectionDialog(false)} + className="text-gray-400 hover:text-gray-600" + > + ✕ + </button> + </div> + <div className="space-y-3"> + <p className="text-sm text-gray-600 mb-4"> + {t('selectUserDescription')} + </p> + {otpUsers.map((user, index) => ( + <div + key={index} + className="border rounded-lg p-4 hover:bg-gray-50 cursor-pointer" + onClick={() => handleUserSelection(user)} + > + <div className="flex justify-between items-start"> + <div className="flex-1"> + <div className="font-medium text-gray-900">{user.name}</div> + <div className="text-sm text-gray-600">{user.email}</div> + <div className="text-sm text-gray-500">{user.phone}</div> + <div className="text-xs text-gray-400 mt-1">Vendor: {user.vndrcd}</div> + </div> + <Button + size="sm" + variant="outline" + onClick={(e) => { + e.stopPropagation(); + handleUserSelection(user); + }} + > + {t('select')} + </Button> + </div> + </div> + ))} + </div> + <div className="flex justify-end mt-6"> + <Button + variant="outline" + onClick={() => setShowUserSelectionDialog(false)} + > + {t('cancel')} + </Button> + </div> + </div> + </div> + )} + {/* 비밀번호 재설정 다이얼로그 */} {showForgotPassword && !showMfaForm && ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> |
