로그인 페이지 로그인 실패시 Alert 처리.

This commit is contained in:
leejinho 2019-11-14 14:19:59 +09:00
parent 6550c419a1
commit 9ec163bac6
9 changed files with 201 additions and 30 deletions

View File

@ -2,6 +2,8 @@
<div class="login-wrapper" fxLayout="column" fxLayoutAlign="center center"> <div class="login-wrapper" fxLayout="column" fxLayoutAlign="center center">
<ucap-account-login <ucap-account-login
[companyList]="companyList$ | async" [companyList]="companyList$ | async"
[loginBtnEnable]="loginBtnEnable"
[loginBtnText]="loginBtnText"
(login)="onLogin($event)" (login)="onLogin($event)"
> >
</ucap-account-login> </ucap-account-login>

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { Store, select } from '@ngrx/store'; import { Store, select } from '@ngrx/store';
@ -7,20 +7,45 @@ import { Company } from '@ucap-webmessenger/api-external';
import * as AppStore from '@app/store'; import * as AppStore from '@app/store';
import * as AuthenticationStore from '@app/store/account/authentication'; import * as AuthenticationStore from '@app/store/account/authentication';
import * as CompanyStore from '@app/store/setting/company'; import * as CompanyStore from '@app/store/setting/company';
import { Observable } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { tap, map } from 'rxjs/operators';
import {
DialogService,
AlertDialogComponent,
AlertDialogData,
AlertDialogResult
} from '@ucap-webmessenger/ui';
import { StringUtil } from '@ucap-webmessenger/core';
@Component({ @Component({
selector: 'app-page-account-login', selector: 'app-page-account-login',
templateUrl: './login.page.component.html', templateUrl: './login.page.component.html',
styleUrls: ['./login.page.component.scss'] styleUrls: ['./login.page.component.scss']
}) })
export class LoginPageComponent implements OnInit { export class LoginPageComponent implements OnInit, OnDestroy {
companyList$: Observable<Company[]>; companyList$: Observable<Company[]>;
constructor(private store: Store<any>, private router: Router) {} loginFailureCount: Subscription;
defatulLoginBtnText: string;
loginBtnText: string;
loginBtnEnable: boolean;
timeChecker: any;
defatulWaitingTime: number;
waitingTime: number;
constructor(
private store: Store<any>,
private router: Router,
private dialogService: DialogService
) {}
ngOnInit(): void { ngOnInit(): void {
this.defatulLoginBtnText = 'LOGIN';
this.defatulWaitingTime = 5 * 60; // sec
this.store.dispatch( this.store.dispatch(
CompanyStore.companyList({ CompanyStore.companyList({
companyGroupCode: 'LG' companyGroupCode: 'LG'
@ -30,6 +55,80 @@ export class LoginPageComponent implements OnInit {
this.companyList$ = this.store.pipe( this.companyList$ = this.store.pipe(
select(AppStore.SettingSelector.CompanySelector.companyList) select(AppStore.SettingSelector.CompanySelector.companyList)
); );
this.loginFailureCount = this.store
.pipe(
select(AppStore.AccountSelector.AuthenticationSelector.loginFailCount),
map(count => {
if (count > 0) {
if (count < 5) {
this.dialogService.open<
AlertDialogComponent,
AlertDialogData,
AlertDialogResult
>(AlertDialogComponent, {
width: '360px',
data: {
title: 'Alert',
html: `아이디 또는 패스워드가<br/>일치하지 않습니다.`
}
});
}
if (count === 5) {
this.dialogService.open<
AlertDialogComponent,
AlertDialogData,
AlertDialogResult
>(AlertDialogComponent, {
width: '360px',
data: {
title: 'Alert',
html: `비밀번호 오류 횟수 초과입니다.<br/>비밀번호를 확인하신 후<br/>잠시 후 다시 시작해 주세요.`
}
});
this.timeChecker = setInterval(() => this.getCheckTime(), 1000);
}
return;
} else {
if (!!this.timeChecker) {
clearInterval(this.timeChecker);
}
this.waitingTime = this.defatulWaitingTime;
this.loginBtnText = this.defatulLoginBtnText;
this.loginBtnEnable = true;
}
})
)
.subscribe();
}
ngOnDestroy(): void {
if (!!this.loginFailureCount) {
this.loginFailureCount.unsubscribe();
}
}
getCheckTime() {
if (this.waitingTime <= 0) {
// reset.
if (!!this.timeChecker) {
clearInterval(this.timeChecker);
}
this.waitingTime = this.defatulWaitingTime;
this.loginBtnText = this.defatulLoginBtnText;
this.loginBtnEnable = true;
} else {
// wait.
this.waitingTime = this.waitingTime - 1;
this.loginBtnText = `${StringUtil.zeroFill(
Math.floor(this.waitingTime / 60),
2
)}:${StringUtil.zeroFill(this.waitingTime % 60, 2)}`;
this.loginBtnEnable = false;
}
} }
onLogin(value: { onLogin(value: {

View File

@ -6,7 +6,7 @@ import { LoginResponse } from '@ucap-webmessenger/protocol-authentication';
import { LoginInfo } from '@app/types'; import { LoginInfo } from '@app/types';
import { import {
UserPasswordSetRequest, UserPasswordSetRequest,
UserPasswordSetResponse, UserPasswordSetResponse
} from '@ucap-webmessenger/protocol-service'; } from '@ucap-webmessenger/protocol-service';
export const webLogin = createAction( export const webLogin = createAction(
@ -43,6 +43,15 @@ export const loginFailure = createAction(
props<{ error: any }>() props<{ error: any }>()
); );
export const increaseLoginFailCount = createAction(
'[Account::Authentication] Increase Login Failure Count',
props()
);
export const initialLoginFailCount = createAction(
'[Account::Authentication] Initialize Login Failure Count',
props()
);
export const loginRedirect = createAction( export const loginRedirect = createAction(
'[Account::Authentication] Login Redirect' '[Account::Authentication] Login Redirect'
); );

View File

@ -9,14 +9,14 @@ import { Actions, ofType, createEffect } from '@ngrx/effects';
import { import {
PiService, PiService,
Login2Response, Login2Response,
UserTermsActionResponse, UserTermsActionResponse
} from '@ucap-webmessenger/pi'; } from '@ucap-webmessenger/pi';
import { NativeService, UCAP_NATIVE_SERVICE } from '@ucap-webmessenger/native'; import { NativeService, UCAP_NATIVE_SERVICE } from '@ucap-webmessenger/native';
import { import {
DialogService, DialogService,
ConfirmDialogComponent, ConfirmDialogComponent,
ConfirmDialogData, ConfirmDialogData,
ConfirmDialogResult, ConfirmDialogResult
} from '@ucap-webmessenger/ui'; } from '@ucap-webmessenger/ui';
import { import {
@ -35,12 +35,14 @@ import {
changePassword, changePassword,
changePasswordFailure, changePasswordFailure,
changePasswordSuccess, changePasswordSuccess,
increaseLoginFailCount,
initialLoginFailCount
} from './actions'; } from './actions';
import { import {
LoginInfo, LoginInfo,
KEY_LOGIN_INFO, KEY_LOGIN_INFO,
EnvironmentsInfo, EnvironmentsInfo,
KEY_ENVIRONMENTS_INFO, KEY_ENVIRONMENTS_INFO
} from '@app/types'; } from '@app/types';
import { AppAuthenticationService } from '@app/services/authentication.service'; import { AppAuthenticationService } from '@app/services/authentication.service';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
@ -48,7 +50,7 @@ import { Store } from '@ngrx/store';
import { SessionStorageService } from '@ucap-webmessenger/web-storage'; import { SessionStorageService } from '@ucap-webmessenger/web-storage';
import { import {
ServiceProtocolService, ServiceProtocolService,
UserPasswordSetResponse, UserPasswordSetResponse
} from '@ucap-webmessenger/protocol-service'; } from '@ucap-webmessenger/protocol-service';
@Injectable() @Injectable()
@ -62,17 +64,19 @@ export class Effects {
.login2({ .login2({
loginId: params.loginInfo.loginId, loginId: params.loginInfo.loginId,
loginPw: params.loginInfo.loginPw, loginPw: params.loginInfo.loginPw,
companyCode: params.loginInfo.companyCode, companyCode: params.loginInfo.companyCode
}) })
.pipe( .pipe(
map((res: Login2Response) => { map((res: Login2Response) => {
if ('success' !== res.status.toLowerCase()) { if ('success' !== res.status.toLowerCase()) {
this.store.dispatch(increaseLoginFailCount({}));
return webLoginFailure({ error: 'Failed' }); return webLoginFailure({ error: 'Failed' });
} else { } else {
this.store.dispatch(initialLoginFailCount({}));
return webLoginSuccess({ return webLoginSuccess({
loginInfo: params.loginInfo, loginInfo: params.loginInfo,
rememberMe: params.rememberMe, rememberMe: params.rememberMe,
login2Response: res, login2Response: res
}); });
} }
}), }),
@ -147,8 +151,8 @@ export class Effects {
width: '220px', width: '220px',
data: { data: {
title: 'Logout', title: 'Logout',
message: 'Logout ?', message: 'Logout ?'
}, }
}); });
return result.choice; return result.choice;
@ -177,7 +181,7 @@ export class Effects {
token: loginRes.tokenString, token: loginRes.tokenString,
deviceType: environmentsInfo.deviceType, deviceType: environmentsInfo.deviceType,
localeCode: loginInfo.localeCode, localeCode: loginInfo.localeCode,
textOnly: 'true', textOnly: 'true'
}); });
const result = await this.dialogService.open< const result = await this.dialogService.open<
@ -189,9 +193,9 @@ export class Effects {
height: '500px', height: '500px',
disableClose: true, disableClose: true,
data: { data: {
title: '개인정보 동의', title: '개인정보 동의'
// html: `<iframe id="ifm_privacy" src="${privacyTotalUrl}" style="width: 100%;height: 300px;" />` // html: `<iframe id="ifm_privacy" src="${privacyTotalUrl}" style="width: 100%;height: 300px;" />`
}, }
}); });
if (result.choice) { if (result.choice) {
@ -213,8 +217,8 @@ export class Effects {
disableClose: true, disableClose: true,
data: { data: {
title: '패스워드 만료', title: '패스워드 만료',
message: '', message: ''
}, }
}); });
if (result.choice) { if (result.choice) {
@ -241,7 +245,7 @@ export class Effects {
return { return {
loginInfo, loginInfo,
environmentsInfo, environmentsInfo,
loginResponse: action.loginRes, loginResponse: action.loginRes
}; };
}), }),
exhaustMap(params => exhaustMap(params =>
@ -249,7 +253,7 @@ export class Effects {
.userTermsAction({ .userTermsAction({
userSeq: params.loginResponse.userSeq, userSeq: params.loginResponse.userSeq,
token: params.loginResponse.tokenString, token: params.loginResponse.tokenString,
deviceType: params.environmentsInfo.deviceType, deviceType: params.environmentsInfo.deviceType
}) })
.pipe( .pipe(
map((res: UserTermsActionResponse) => { map((res: UserTermsActionResponse) => {
@ -273,7 +277,7 @@ export class Effects {
this.serviceProtocolService.userPasswordSet(req).pipe( this.serviceProtocolService.userPasswordSet(req).pipe(
map((res: UserPasswordSetResponse) => { map((res: UserPasswordSetResponse) => {
return changePasswordSuccess({ return changePasswordSuccess({
res, res
}); });
}), }),
catchError(error => of(changePasswordFailure({ error }))) catchError(error => of(changePasswordFailure({ error })))

View File

@ -1,13 +1,30 @@
import { Action, combineReducers, createReducer, on } from '@ngrx/store'; import { Action, combineReducers, createReducer, on } from '@ngrx/store';
import { State, initialState } from './state'; import { State, initialState } from './state';
import { loginSuccess } from './actions'; import {
loginSuccess,
increaseLoginFailCount,
initialLoginFailCount
} from './actions';
export const reducer = createReducer( export const reducer = createReducer(
initialState, initialState,
on(loginSuccess, (state, action) => { on(loginSuccess, (state, action) => {
return { return {
...state, ...state,
loginRes: action.loginRes, loginRes: action.loginRes
};
}),
on(increaseLoginFailCount, (state, action) => {
return {
...state,
loginFailCount: state.loginFailCount + 1
};
}),
on(initialLoginFailCount, (state, action) => {
return {
...state,
loginFailCount: 0
}; };
}) })
); );

View File

@ -4,14 +4,20 @@ import { LoginResponse } from '@ucap-webmessenger/protocol-authentication';
// tslint:disable-next-line: no-empty-interface // tslint:disable-next-line: no-empty-interface
export interface State { export interface State {
loginRes: LoginResponse | null; loginRes: LoginResponse | null;
loginFailCount: number;
} }
export const initialState: State = { export const initialState: State = {
loginRes: null, loginRes: null,
loginFailCount: 0
}; };
export function selectors<S>(selector: Selector<any, State>) { export function selectors<S>(selector: Selector<any, State>) {
return { return {
loginRes: createSelector(selector, (state: State) => state.loginRes), loginRes: createSelector(selector, (state: State) => state.loginRes),
loginFailCount: createSelector(
selector,
(state: State) => state.loginFailCount
)
}; };
} }

View File

@ -1 +1,18 @@
export class StringUtil {} export class StringUtil {
/**
* prefix zero fill
* @param str target string
* @param len fill in length
*/
public static zeroFill(str: any, len: number): string {
if (typeof str === 'string') {
let fillin = '';
for (let i = 0; i < len - str.length; i++) {
fillin += '0';
}
return fillin + str;
} else if (typeof str === 'number') {
return StringUtil.zeroFill(str.toString(), len);
}
}
}

View File

@ -2,7 +2,7 @@
<div class="mat-title">LOGIN TO YOUR ACCOUNT</div> <div class="mat-title">LOGIN TO YOUR ACCOUNT</div>
<form name="loginForm" [formGroup]="loginForm" novalidate> <form name="loginForm" [formGroup]="loginForm" novalidate>
<mat-form-field > <mat-form-field>
<mat-label>Company</mat-label> <mat-label>Company</mat-label>
<mat-select formControlName="companyCode" required> <mat-select formControlName="companyCode" required>
<mat-option <mat-option
@ -27,7 +27,7 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field > <mat-form-field>
<mat-label>Password</mat-label> <mat-label>Password</mat-label>
<input matInput type="password" formControlName="loginPw" #loginPw /> <input matInput type="password" formControlName="loginPw" #loginPw />
<mat-error> <mat-error>
@ -35,8 +35,17 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<div class="remember-forgot-password" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="space-between center"> <div
<mat-checkbox class="remember-me" formControlName="remember" aria-label="Remember Me"> class="remember-forgot-password"
fxLayout="row"
fxLayout.xs="column"
fxLayoutAlign="space-between center"
>
<mat-checkbox
class="remember-me"
formControlName="remember"
aria-label="Remember Me"
>
Remember Me Remember Me
</mat-checkbox> </mat-checkbox>
@ -49,10 +58,10 @@
mat-raised-button mat-raised-button
class="submit-button bg-accent-dark" class="submit-button bg-accent-dark"
aria-label="LOG IN" aria-label="LOG IN"
[disabled]="loginForm.invalid" [disabled]="loginForm.invalid || !loginBtnEnable"
(click)="onClickLogin()" (click)="onClickLogin()"
> >
LOGIN {{ loginBtnText }}
</button> </button>
</form> </form>

View File

@ -20,6 +20,10 @@ import { LoginInfo, KEY_LOGIN_INFO } from '@app/types';
export class LoginComponent implements OnInit { export class LoginComponent implements OnInit {
@Input() @Input()
companyList?: Company[]; companyList?: Company[];
@Input()
loginBtnText?: string;
@Input()
loginBtnEnable: boolean;
@Output() @Output()
login = new EventEmitter<{ login = new EventEmitter<{
@ -57,6 +61,10 @@ export class LoginComponent implements OnInit {
loginPw: ['', Validators.required], loginPw: ['', Validators.required],
remember: [remember] remember: [remember]
}); });
if (!this.loginBtnText || this.loginBtnText.trim().length === 0) {
this.loginBtnText = 'LOGIN';
}
} }
onClickLogin() { onClickLogin() {