summaryrefslogtreecommitdiff
path: root/components/login
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-13 18:24:00 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-13 18:24:00 +0900
commit80e3d0befed487e0447bacffd76ed6539f01e992 (patch)
treede5762bea7161e3dd949401b2d985b6723fd32ee /components/login
parentff8a168f9fc67b345f4d32065e55f0901ba05b4c (diff)
(김준회) S-GIPS 로그인시 유저 선택해 sms 전송 처리
Diffstat (limited to 'components/login')
-rw-r--r--components/login/login-form.tsx215
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">