This commit is contained in:
leejinho 2019-12-02 15:31:41 +09:00
commit e6cdde6c31
34 changed files with 920 additions and 458 deletions

View File

@ -2,35 +2,67 @@ import { DeviceType } from '@ucap-webmessenger/core';
import { import {
APIRequest, APIRequest,
APIResponse, APIResponse,
APIEncoder,
APIDecoder, APIDecoder,
ParameterUtil ParameterUtil,
APIFormDataEncoder,
JsonAnalization,
StatusCode
} from '@ucap-webmessenger/api'; } from '@ucap-webmessenger/api';
import { FileUploadItem } from '../models/file-upload-item';
export interface FileProfileSaveRequest extends APIRequest { export interface FileProfileSaveRequest extends APIRequest {
userSeq: number; userSeq: number;
deviceType: DeviceType; deviceType: DeviceType;
token: string; token: string;
file?: File; file?: File;
fileUploadItem: FileUploadItem;
intro?: string; intro?: string;
initProfileImage?: boolean; initProfileImage?: boolean;
} }
export interface FileProfileSaveResponse extends APIResponse { export interface FileProfileSaveResponse extends APIResponse {
ProfileURL?: string; profileURL?: string;
ProfileSubDir?: string; profileSubDir?: string;
returnJson?: any;
} }
const fileProfileEncodeMap = {}; const fileProfileEncodeMap = {
userSeq: 'p_user_seq',
deviceType: 'p_device_type',
token: 'p_token',
file: 'file',
intro: 'p_intro',
initProfileImage: 'p_init_profile_img_yn'
};
export const encodeFileProfileSave: APIEncoder<FileProfileSaveRequest> = ( export const encodeFileProfileSave: APIFormDataEncoder<FileProfileSaveRequest> = (
req: FileProfileSaveRequest req: FileProfileSaveRequest
) => { ) => {
return ParameterUtil.encode(fileProfileEncodeMap, req); const extraParams: any = {};
extraParams.userSeq = String(req.userSeq);
if (!!req.initProfileImage) {
extraParams.initProfileImage = req.initProfileImage ? 'Y' : 'N';
}
return ParameterUtil.encodeFormData(fileProfileEncodeMap, req, extraParams);
}; };
export const decodeFileProfileSave: APIDecoder<FileProfileSaveResponse> = ( export const decodeFileProfileSave: APIDecoder<FileProfileSaveResponse> = (
res: any res: any
) => { ) => {
return {} as FileProfileSaveResponse; try {
const json = JsonAnalization.receiveAnalization(res);
return {
statusCode: json.StatusCode,
profileURL: json.ProfileURL,
profileSubDir: json.ProfileSubDir,
returnJson: res
} as FileProfileSaveResponse;
} catch (e) {
return {
statusCode: StatusCode.Fail,
errorMessage: e
} as FileProfileSaveResponse;
}
}; };

View File

@ -94,15 +94,29 @@ export class CommonApiService {
req: FileProfileSaveRequest, req: FileProfileSaveRequest,
fileProfileSaveUrl?: string fileProfileSaveUrl?: string
): Observable<FileProfileSaveResponse> { ): Observable<FileProfileSaveResponse> {
return this.httpClient const httpReq = new HttpRequest(
.post<any>( 'POST',
!!fileProfileSaveUrl ? fileProfileSaveUrl : this.urls.fileProfileSave, !!fileProfileSaveUrl ? fileProfileSaveUrl : this.urls.fileProfileSave,
{}, encodeFileProfileSave(req),
{ { reportProgress: true, responseType: 'text' as 'json' }
params: encodeFileProfileSave(req) );
const progress = req.fileUploadItem.uploadStart();
return this.httpClient.request(httpReq).pipe(
filter(event => {
if (event instanceof HttpResponse) {
return true;
} else if (HttpEventType.UploadProgress === event.type) {
progress.next(Math.round((100 * event.loaded) / event.total));
} }
) return false;
.pipe(map(res => decodeFileProfileSave(res))); }),
map((event: HttpResponse<any>) => {
req.fileUploadItem.uploadComplete();
return decodeFileProfileSave(event.body);
})
);
} }
public urlForFileTalkDownload( public urlForFileTalkDownload(

View File

@ -16,7 +16,7 @@ import {
CreateChatDialogData, CreateChatDialogData,
CreateChatDialogResult CreateChatDialogResult
} from '@app/layouts/messenger/dialogs/chat/create-chat.dialog.component'; } from '@app/layouts/messenger/dialogs/chat/create-chat.dialog.component';
import { Observable, Subscription, of } from 'rxjs'; import { Subscription, of } from 'rxjs';
import { Store, select } from '@ngrx/store'; import { Store, select } from '@ngrx/store';
import * as AppStore from '@app/store'; import * as AppStore from '@app/store';
@ -28,8 +28,7 @@ import {
UserInfoF, UserInfoF,
UserInfoDN UserInfoDN
} from '@ucap-webmessenger/protocol-query'; } from '@ucap-webmessenger/protocol-query';
import { MatTabChangeEvent, MatTabGroup } from '@angular/material'; import { MatTabChangeEvent } from '@angular/material';
import { RightDrawer } from '@app/types';
import { LoginResponse } from '@ucap-webmessenger/protocol-authentication'; import { LoginResponse } from '@ucap-webmessenger/protocol-authentication';
import { SessionStorageService } from '@ucap-webmessenger/web-storage'; import { SessionStorageService } from '@ucap-webmessenger/web-storage';
import { KEY_LOGIN_RES_INFO } from '@app/types/login-res-info.type'; import { KEY_LOGIN_RES_INFO } from '@app/types/login-res-info.type';
@ -38,7 +37,7 @@ import { KEY_VER_INFO } from '@app/types/ver-info.type';
import { MessageApiService } from '@ucap-webmessenger/api-message'; import { MessageApiService } from '@ucap-webmessenger/api-message';
import { DeviceType } from '@ucap-webmessenger/core'; import { DeviceType } from '@ucap-webmessenger/core';
import { UnreadCountRequest } from 'projects/ucap-webmessenger-api-message/src/lib/apis/unread-count'; import { UnreadCountRequest } from 'projects/ucap-webmessenger-api-message/src/lib/apis/unread-count';
import { map, catchError } from 'rxjs/operators'; import { map, catchError, tap } from 'rxjs/operators';
import { MessageStatusCode } from '@ucap-webmessenger/api'; import { MessageStatusCode } from '@ucap-webmessenger/api';
import { import {
EditMessageDialogComponent, EditMessageDialogComponent,
@ -87,6 +86,7 @@ export class LeftSideComponent implements OnInit, OnDestroy {
sessionVerinfo: VersionInfo2Response; sessionVerinfo: VersionInfo2Response;
loginRes: LoginResponse; loginRes: LoginResponse;
loginResSubscription: Subscription;
constructor( constructor(
private store: Store<any>, private store: Store<any>,
@ -95,9 +95,6 @@ export class LeftSideComponent implements OnInit, OnDestroy {
private messageApiService: MessageApiService, private messageApiService: MessageApiService,
private logger: NGXLogger private logger: NGXLogger
) { ) {
this.loginRes = this.sessionStorageService.get<LoginResponse>(
KEY_LOGIN_RES_INFO
);
this.sessionVerinfo = this.sessionStorageService.get<VersionInfo2Response>( this.sessionVerinfo = this.sessionStorageService.get<VersionInfo2Response>(
KEY_VER_INFO KEY_VER_INFO
); );
@ -112,6 +109,15 @@ export class LeftSideComponent implements OnInit, OnDestroy {
this.badgeChatUnReadCount = count; this.badgeChatUnReadCount = count;
}); });
this.loginResSubscription = this.store
.pipe(
select(AppStore.AccountSelector.AuthenticationSelector.loginRes),
tap(loginRes => {
this.loginRes = loginRes;
})
)
.subscribe();
this.getMessageUnreadCount(); this.getMessageUnreadCount();
this.badgeMessageInterval = setInterval( this.badgeMessageInterval = setInterval(
() => this.getMessageUnreadCount(), () => this.getMessageUnreadCount(),
@ -129,6 +135,9 @@ export class LeftSideComponent implements OnInit, OnDestroy {
if (!!this.badgeMessageUnReadCountSubscription) { if (!!this.badgeMessageUnReadCountSubscription) {
this.badgeMessageUnReadCountSubscription.unsubscribe(); this.badgeMessageUnReadCountSubscription.unsubscribe();
} }
if (!!this.loginResSubscription) {
this.loginResSubscription.unsubscribe();
}
if (!!this.badgeMessageInterval) { if (!!this.badgeMessageInterval) {
clearInterval(this.badgeMessageInterval); clearInterval(this.badgeMessageInterval);

View File

@ -5,6 +5,9 @@
<mat-card-content> <mat-card-content>
<div fxFlex class="setting-tab"> <div fxFlex class="setting-tab">
<ucap-account-change-password <ucap-account-change-password
[loginId]="data.loginId"
[phoneNumber]="data.phoneNumber"
[encryptedLoginPw]="data.encryptedLoginPw"
(changePassword)="onChangePassword($event)" (changePassword)="onChangePassword($event)"
></ucap-account-change-password> ></ucap-account-change-password>
</div> </div>

View File

@ -1,24 +1,18 @@
import { Component, OnInit, Inject, OnDestroy } from '@angular/core'; import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { KEY_LOGIN_RES_INFO } from '@app/types/login-res-info.type';
import { KEY_VER_INFO } from '@app/types/ver-info.type';
import { SessionStorageService } from '@ucap-webmessenger/web-storage';
import { Store } from '@ngrx/store';
import { DialogService } from '@ucap-webmessenger/ui'; import { DialogService } from '@ucap-webmessenger/ui';
import { VersionInfo2Response } from '@ucap-webmessenger/api-public';
import { LoginResponse } from '@ucap-webmessenger/protocol-authentication';
import { AppAuthenticationService } from '@app/services/authentication.service';
import { StringUtil, CharactorType } from '@ucap-webmessenger/core';
import * as ServiceStore from '@app/store/messenger/service'; export interface ChangePasswordDialogData {
import CryptoJS from 'crypto-js'; loginId: string;
phoneNumber?: string;
export interface ChangePasswordDialogData {} encryptedLoginPw?: string;
}
export interface ChangePasswordDialogResult { export interface ChangePasswordDialogResult {
choice: boolean; choice: boolean;
currentLoginPw?: string;
newLoginPw?: string;
} }
@Component({ @Component({
@ -27,170 +21,25 @@ export interface ChangePasswordDialogResult {
styleUrls: ['./change-password.dialog.component.scss'] styleUrls: ['./change-password.dialog.component.scss']
}) })
export class ChangePasswordDialogComponent implements OnInit, OnDestroy { export class ChangePasswordDialogComponent implements OnInit, OnDestroy {
loginRes: LoginResponse;
sessionVerinfo: VersionInfo2Response;
isMe: boolean;
constructor( constructor(
public dialogRef: MatDialogRef< public dialogRef: MatDialogRef<
ChangePasswordDialogData, ChangePasswordDialogData,
ChangePasswordDialogResult ChangePasswordDialogResult
>, >,
@Inject(MAT_DIALOG_DATA) public data: ChangePasswordDialogData, @Inject(MAT_DIALOG_DATA) public data: ChangePasswordDialogData,
private dialogService: DialogService, private dialogService: DialogService
private sessionStorageService: SessionStorageService, ) {}
private appAuthenticationService: AppAuthenticationService,
private store: Store<any>
) {
this.sessionVerinfo = this.sessionStorageService.get<VersionInfo2Response>(
KEY_VER_INFO
);
this.loginRes = this.sessionStorageService.get<LoginResponse>(
KEY_LOGIN_RES_INFO
);
}
ngOnInit() {} ngOnInit() {}
ngOnDestroy(): void {} ngOnDestroy(): void {}
onChangePassword(info: { onChangePassword(info: { currentLoginPw: string; newLoginPw: string }): void {
currentLoginPw: string; this.dialogRef.close({
newLoginPw: string; choice: true,
newConfirmLoginPw: string; currentLoginPw: info.currentLoginPw,
notValid: () => void; newLoginPw: info.newLoginPw
}): void { });
if (!info.currentLoginPw || '' === info.currentLoginPw.trim()) {
// 현재 비밀번호를 입력해주세요
info.notValid();
return;
}
if (!this.appAuthenticationService.isSameForPassword(info.currentLoginPw)) {
// "현재 비밀번호가 다릅니다"
info.notValid();
return;
}
if (!info.newLoginPw || '' === info.newLoginPw.trim()) {
// 신규 비밀번호를 입력해주세요
info.notValid();
return;
}
if (-1 < info.newLoginPw.indexOf(this.loginRes.userId)) {
// "사용자 ID를 비밀번호에 포함할 수 없습니다."
info.notValid();
return;
}
const phoneNumberOnly = !!this.loginRes.userInfo.hpNumber
? this.loginRes.userInfo.hpNumber.trim().replace(new RegExp('-', 'g'), '')
: '';
switch (phoneNumberOnly.length) {
case 11:
if (-1 < info.newLoginPw.indexOf(phoneNumberOnly.substr(3, 4))) {
// "사용자 휴대폰번호를 비밀번호에 포함할 수 없습니다."
info.notValid();
return;
}
if (-1 < info.newLoginPw.indexOf(phoneNumberOnly.substr(7, 4))) {
// "사용자 휴대폰번호를 비밀번호에 포함할 수 없습니다."
info.notValid();
return;
}
break;
case 10:
if (-1 < info.newLoginPw.indexOf(phoneNumberOnly.substr(3, 3))) {
// "사용자 휴대폰번호를 비밀번호에 포함할 수 없습니다."
info.notValid();
return;
}
if (-1 < info.newLoginPw.indexOf(phoneNumberOnly.substr(6, 4))) {
// "사용자 휴대폰번호를 비밀번호에 포함할 수 없습니다."
info.notValid();
return;
}
break;
default:
break;
}
if (!info.newConfirmLoginPw || '' === info.newConfirmLoginPw.trim()) {
// "신규 비밀번호 확인을 입력해주세요"
info.notValid();
return;
}
if (info.newLoginPw !== info.newConfirmLoginPw.trim()) {
// "신규 비밀번호와 신규 비밀번호 확인이 다릅니다"
info.notValid();
return;
}
if (info.currentLoginPw === info.newLoginPw.trim()) {
// "현재 비밀번호와 새로운 비밀번호가 같습니다."
info.notValid();
return;
}
if (0 < StringUtil.includes(info.newLoginPw, CharactorType.Space)) {
// "비밀번호에는 공백을 입력할 수 없습니다."
info.notValid();
return;
}
if (StringUtil.isRepeat(info.newLoginPw, 3)) {
// "숫자나 문자를 3번이상 반복적으로 사용할 수 없습니다."
info.notValid();
return;
}
if (
StringUtil.isIncrements(info.newLoginPw, 3) ||
StringUtil.isDecrements(info.newLoginPw, 3)
) {
// "연속되는 숫자나 문자를 3번이상 사용할 수 없습니다."
info.notValid();
return;
}
let combinationCount = 0;
if (0 < StringUtil.includes(info.newLoginPw, CharactorType.Special)) {
combinationCount++;
}
if (0 < StringUtil.includes(info.newLoginPw, CharactorType.Number)) {
combinationCount++;
}
if (0 < StringUtil.includes(info.newLoginPw, CharactorType.Charactor)) {
combinationCount++;
}
let minPwLen = 0;
if (2 < combinationCount) {
minPwLen = 8;
} else if (2 === combinationCount) {
minPwLen = 10;
} else if (2 > combinationCount) {
// "2종류 이상 조합을 해야 합니다."
info.notValid();
return;
}
if (info.newLoginPw.length < minPwLen) {
// "비밀번호는 %d가지가 조합된 경우 %d자를 넘어야 합니다."
info.notValid();
return;
}
this.store.dispatch(
ServiceStore.userPasswordSet({
req: {
loginId: this.loginRes.userId,
companyCode: this.loginRes.companyCode,
oldLoginPw: CryptoJS.enc.Hex.stringify(
CryptoJS.SHA256(info.currentLoginPw)
),
newLoginPw: CryptoJS.enc.Hex.stringify(
CryptoJS.SHA256(info.newLoginPw)
)
}
})
);
this.dialogRef.close({ choice: true });
} }
onClickChoice(choice: boolean): void { onClickChoice(choice: boolean): void {

View File

@ -1,5 +1,5 @@
<ucap-profile-profile <ucap-profile-profile
[userInfo]="data.userInfo" [userInfo]="userInfo"
[profileImageRoot]="sessionVerinfo.profileRoot" [profileImageRoot]="sessionVerinfo.profileRoot"
[isMe]="isMe" [isMe]="isMe"
[isBuddy]="isBuddy" [isBuddy]="isBuddy"
@ -7,5 +7,6 @@
(openChat)="onClickChat($event)" (openChat)="onClickChat($event)"
(toggleFavorit)="onClickToggleFavorit($event)" (toggleFavorit)="onClickToggleFavorit($event)"
(toggleBuddy)="onClickToggleBuddy($event)" (toggleBuddy)="onClickToggleBuddy($event)"
(uploadProfileImage)="onUploadProfileImage($event)"
> >
</ucap-profile-profile> </ucap-profile-profile>

View File

@ -8,6 +8,7 @@ import { Store, select } from '@ngrx/store';
import * as AppStore from '@app/store'; import * as AppStore from '@app/store';
import * as ChatStore from '@app/store/messenger/chat'; import * as ChatStore from '@app/store/messenger/chat';
import * as SyncStore from '@app/store/messenger/sync'; import * as SyncStore from '@app/store/messenger/sync';
import * as AuthenticationStore from '@app/store/account/authentication';
import { UserInfo, GroupDetailData } from '@ucap-webmessenger/protocol-sync'; import { UserInfo, GroupDetailData } from '@ucap-webmessenger/protocol-sync';
import { import {
@ -15,12 +16,28 @@ import {
UserInfoF, UserInfoF,
UserInfoDN UserInfoDN
} from '@ucap-webmessenger/protocol-query'; } from '@ucap-webmessenger/protocol-query';
import { DialogService, ConfirmDialogComponent, ConfirmDialogData, ConfirmDialogResult } from '@ucap-webmessenger/ui'; import {
DialogService,
ConfirmDialogComponent,
ConfirmDialogData,
ConfirmDialogResult,
SnackBarService
} from '@ucap-webmessenger/ui';
import { VersionInfo2Response } from '@ucap-webmessenger/api-public'; import { VersionInfo2Response } from '@ucap-webmessenger/api-public';
import { LoginResponse } from '@ucap-webmessenger/protocol-authentication'; import { LoginResponse } from '@ucap-webmessenger/protocol-authentication';
import { map } from 'rxjs/operators'; import { map, take, finalize } from 'rxjs/operators';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { SelectGroupDialogComponent, SelectGroupDialogData, SelectGroupDialogResult } from '../group/select-group.dialog.component'; import {
SelectGroupDialogComponent,
SelectGroupDialogData,
SelectGroupDialogResult
} from '../group/select-group.dialog.component';
import {
FileUploadItem,
CommonApiService
} from '@ucap-webmessenger/api-common';
import { EnvironmentsInfo, KEY_ENVIRONMENTS_INFO } from '@app/types';
import { StatusCode } from '@ucap-webmessenger/api';
export interface ProfileDialogData { export interface ProfileDialogData {
userInfo: UserInfo | UserInfoSS | UserInfoF | UserInfoDN; userInfo: UserInfo | UserInfoSS | UserInfoF | UserInfoDN;
@ -34,8 +51,11 @@ export interface ProfileDialogResult {}
styleUrls: ['./profile.dialog.component.scss'] styleUrls: ['./profile.dialog.component.scss']
}) })
export class ProfileDialogComponent implements OnInit, OnDestroy { export class ProfileDialogComponent implements OnInit, OnDestroy {
userInfo: UserInfo | UserInfoSS | UserInfoF | UserInfoDN;
loginRes: LoginResponse; loginRes: LoginResponse;
sessionVerinfo: VersionInfo2Response; sessionVerinfo: VersionInfo2Response;
environmentsInfo: EnvironmentsInfo;
isMe: boolean; isMe: boolean;
isBuddy: boolean; isBuddy: boolean;
isFavorit: boolean; isFavorit: boolean;
@ -47,6 +67,8 @@ export class ProfileDialogComponent implements OnInit, OnDestroy {
@Inject(MAT_DIALOG_DATA) public data: ProfileDialogData, @Inject(MAT_DIALOG_DATA) public data: ProfileDialogData,
private dialogService: DialogService, private dialogService: DialogService,
private sessionStorageService: SessionStorageService, private sessionStorageService: SessionStorageService,
private commonApiService: CommonApiService,
private snackBarService: SnackBarService,
private store: Store<any> private store: Store<any>
) { ) {
this.sessionVerinfo = this.sessionStorageService.get<VersionInfo2Response>( this.sessionVerinfo = this.sessionStorageService.get<VersionInfo2Response>(
@ -55,6 +77,11 @@ export class ProfileDialogComponent implements OnInit, OnDestroy {
this.loginRes = this.sessionStorageService.get<LoginResponse>( this.loginRes = this.sessionStorageService.get<LoginResponse>(
KEY_LOGIN_RES_INFO KEY_LOGIN_RES_INFO
); );
this.environmentsInfo = this.sessionStorageService.get<EnvironmentsInfo>(
KEY_ENVIRONMENTS_INFO
);
this.userInfo = data.userInfo;
} }
ngOnInit() { ngOnInit() {
@ -156,8 +183,68 @@ export class ProfileDialogComponent implements OnInit, OnDestroy {
this.store.dispatch( this.store.dispatch(
SyncStore.delBuddyAndClear({ seq: param.userInfo.seq }) SyncStore.delBuddyAndClear({ seq: param.userInfo.seq })
); );
this.isBuddy = false this.isBuddy = false;
} }
} }
} }
onUploadProfileImage(profileImageFileUploadItem: FileUploadItem) {
this.commonApiService
.fileProfileSave(
{
userSeq: this.loginRes.userSeq,
deviceType: this.environmentsInfo.deviceType,
token: this.loginRes.tokenString,
file: profileImageFileUploadItem.file,
fileUploadItem: profileImageFileUploadItem
},
this.sessionVerinfo.profileUploadUrl
)
.pipe(
take(1),
map(res => {
if (!res) {
return;
}
if (StatusCode.Success === res.statusCode) {
return res;
} else {
throw res;
}
}),
finalize(() => {
setTimeout(() => {
profileImageFileUploadItem.uploadingProgress$ = undefined;
}, 1000);
})
)
.subscribe(
res => {
const userInfo = {
...this.loginRes.userInfo,
profileImageFile: res.profileSubDir
};
this.store.dispatch(
AuthenticationStore.updateLoginRes({
loginRes: {
...this.loginRes,
userInfo
}
})
);
this.userInfo = userInfo as any;
},
error => {
this.snackBarService.open(
`프로필 이미지 변경중에 문제가 발생하였습니다.`,
'',
{
duration: 3000,
verticalPosition: 'bottom'
}
);
}
);
}
} }

View File

@ -69,7 +69,7 @@ export class LoginPageComponent implements OnInit, OnDestroy {
>(AlertDialogComponent, { >(AlertDialogComponent, {
width: '360px', width: '360px',
data: { data: {
title: 'Alert', title: '로그인',
html: `아이디 또는 패스워드가<br/>일치하지 않습니다.` html: `아이디 또는 패스워드가<br/>일치하지 않습니다.`
} }
}); });
@ -83,7 +83,7 @@ export class LoginPageComponent implements OnInit, OnDestroy {
>(AlertDialogComponent, { >(AlertDialogComponent, {
width: '360px', width: '360px',
data: { data: {
title: 'Alert', title: '로그인',
html: `비밀번호 오류 횟수 초과입니다.<br/>비밀번호를 확인하신 후<br/>잠시 후 다시 시작해 주세요.` html: `비밀번호 오류 횟수 초과입니다.<br/>비밀번호를 확인하신 후<br/>잠시 후 다시 시작해 주세요.`
} }
}); });

View File

@ -16,7 +16,7 @@ import {
import { Store, select } from '@ngrx/store'; import { Store, select } from '@ngrx/store';
import { ProtocolService } from '@ucap-webmessenger/protocol'; import { ProtocolService, ServerErrorCode } from '@ucap-webmessenger/protocol';
import { SessionStorageService } from '@ucap-webmessenger/web-storage'; import { SessionStorageService } from '@ucap-webmessenger/web-storage';
import { import {
PublicApiService, PublicApiService,
@ -51,6 +51,7 @@ import { KEY_VER_INFO } from '@app/types/ver-info.type';
import { KEY_LOGIN_RES_INFO } from '@app/types/login-res-info.type'; import { KEY_LOGIN_RES_INFO } from '@app/types/login-res-info.type';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { SnackBarService } from '@ucap-webmessenger/ui';
@Injectable() @Injectable()
export class AppMessengerResolver implements Resolve<void> { export class AppMessengerResolver implements Resolve<void> {
@ -63,6 +64,7 @@ export class AppMessengerResolver implements Resolve<void> {
private optionProtocolService: OptionProtocolService, private optionProtocolService: OptionProtocolService,
private authenticationProtocolService: AuthenticationProtocolService, private authenticationProtocolService: AuthenticationProtocolService,
private innerProtocolService: InnerProtocolService, private innerProtocolService: InnerProtocolService,
private snackBarService: SnackBarService,
private logger: NGXLogger private logger: NGXLogger
) {} ) {}
@ -101,25 +103,38 @@ export class AppMessengerResolver implements Resolve<void> {
}), }),
switchMap(() => this.innerProtocolService.conn({})), switchMap(() => this.innerProtocolService.conn({})),
switchMap(res => { switchMap(res => {
return this.authenticationProtocolService.login({ return this.authenticationProtocolService
loginId: loginInfo.loginId, .login({
loginPw: loginInfo.loginPw, loginId: loginInfo.loginId,
deviceType: environmentsInfo.deviceType, loginPw: loginInfo.loginPw,
deviceId: ' ', deviceType: environmentsInfo.deviceType,
token: '', deviceId: ' ',
localeCode: loginInfo.localeCode, token: '',
pushId: ' ', localeCode: loginInfo.localeCode,
companyCode: loginInfo.companyCode, pushId: ' ',
passwordEncodingType: 1, companyCode: loginInfo.companyCode,
clientVersion: '', passwordEncodingType: 1,
reconnect: false, clientVersion: '',
ip: 'localhost', reconnect: false,
hostName: '', ip: 'localhost',
ssoMode: SSOMode.AUTH, hostName: '',
userSpecificInformation: 'PRO_000482', ssoMode: SSOMode.AUTH,
productId: environment.productConfig.productId, userSpecificInformation: 'PRO_000482',
productName: environment.productConfig.productName productId: environment.productConfig.productId,
}); productName: environment.productConfig.productName
})
.pipe(
catchError(err => {
switch (err as ServerErrorCode) {
case ServerErrorCode.ERRCD_IDPW:
break;
default:
break;
}
return throwError(err);
})
);
}), }),
switchMap(res => { switchMap(res => {
loginRes = res; loginRes = res;

View File

@ -1,7 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import CryptoJS from 'crypto-js';
import { import {
SessionStorageService, SessionStorageService,
LocalStorageService LocalStorageService
@ -10,6 +8,7 @@ import { LocaleCode } from '@ucap-webmessenger/core';
import { LoginInfo, KEY_LOGIN_INFO } from '../types'; import { LoginInfo, KEY_LOGIN_INFO } from '../types';
import { KEY_VER_INFO } from '@app/types/ver-info.type'; import { KEY_VER_INFO } from '@app/types/ver-info.type';
import { KEY_LOGIN_RES_INFO } from '@app/types/login-res-info.type'; import { KEY_LOGIN_RES_INFO } from '@app/types/login-res-info.type';
import { PasswordUtil } from '@ucap-webmessenger/pi';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -33,7 +32,7 @@ export class AppAuthenticationService {
this.sessionStorageService.set<LoginInfo>(KEY_LOGIN_INFO, { this.sessionStorageService.set<LoginInfo>(KEY_LOGIN_INFO, {
...loginInfo, ...loginInfo,
initPw: loginInfo.loginId === loginInfo.loginPw, initPw: loginInfo.loginId === loginInfo.loginPw,
loginPw: CryptoJS.enc.Hex.stringify(CryptoJS.SHA256(loginInfo.loginPw)) loginPw: PasswordUtil.encrypt(loginInfo.loginPw)
}); });
if (rememberMe) { if (rememberMe) {
@ -51,11 +50,4 @@ export class AppAuthenticationService {
this.sessionStorageService.remove(KEY_VER_INFO); this.sessionStorageService.remove(KEY_VER_INFO);
this.sessionStorageService.remove(KEY_LOGIN_INFO); this.sessionStorageService.remove(KEY_LOGIN_INFO);
} }
isSameForPassword(pw: string): boolean {
const loginInfo = this.sessionStorageService.get<LoginInfo>(KEY_LOGIN_INFO);
return (
loginInfo.loginPw === CryptoJS.enc.Hex.stringify(CryptoJS.SHA256(pw))
);
}
} }

View File

@ -96,3 +96,27 @@ export const privacyAgreeFailure = createAction(
export const privacyDisagree = createAction( export const privacyDisagree = createAction(
'[Account::Authentication] Privacy Disagree' '[Account::Authentication] Privacy Disagree'
); );
export const userPasswordSet = createAction(
'[Account::Authentication] User Password Set',
props<{ req: UserPasswordSetRequest }>()
);
export const userPasswordSetSuccess = createAction(
'[Account::Authentication] User Password Set Success',
props<{
res: UserPasswordSetResponse;
}>()
);
export const userPasswordSetFailure = createAction(
'[Account::Authentication] User Password Set Failure',
props<{ error: any }>()
);
export const updateLoginRes = createAction(
'[Account::Authentication] Update LoginRes',
props<{
loginRes: LoginResponse;
}>()
);

View File

@ -16,7 +16,11 @@ import {
DialogService, DialogService,
ConfirmDialogComponent, ConfirmDialogComponent,
ConfirmDialogData, ConfirmDialogData,
ConfirmDialogResult ConfirmDialogResult,
SnackBarService,
AlertDialogComponent,
AlertDialogData,
AlertDialogResult
} from '@ucap-webmessenger/ui'; } from '@ucap-webmessenger/ui';
import { import {
@ -34,7 +38,10 @@ import {
privacyAgreeSuccess, privacyAgreeSuccess,
increaseLoginFailCount, increaseLoginFailCount,
initialLoginFailCount, initialLoginFailCount,
logoutInitialize logoutInitialize,
userPasswordSet,
userPasswordSetSuccess,
userPasswordSetFailure
} from './actions'; } from './actions';
import { import {
LoginInfo, LoginInfo,
@ -60,6 +67,10 @@ import {
ChangePasswordDialogData, ChangePasswordDialogData,
ChangePasswordDialogResult ChangePasswordDialogResult
} from '@app/layouts/messenger/dialogs/account/change-password.dialog.component'; } from '@app/layouts/messenger/dialogs/account/change-password.dialog.component';
import {
ServiceProtocolService,
UserPasswordSetResponse
} from '@ucap-webmessenger/protocol-service';
@Injectable() @Injectable()
export class Effects { export class Effects {
@ -273,10 +284,24 @@ export class Effects {
width: '500px', width: '500px',
height: '500px', height: '500px',
disableClose: false, disableClose: false,
data: {} data: {
loginId: loginInfo.loginId,
encryptedLoginPw: loginInfo.loginPw,
phoneNumber: loginRes.userInfo.hpNumber
}
}); });
if (!!result && result.choice) { if (!!result && result.choice) {
this.store.dispatch(
userPasswordSet({
req: {
companyCode: loginInfo.companyCode,
loginId: loginInfo.loginId,
oldLoginPw: result.currentLoginPw,
newLoginPw: result.newLoginPw
}
})
);
} else { } else {
return; return;
} }
@ -324,6 +349,67 @@ export class Effects {
) )
); );
userPasswordSet$ = createEffect(() =>
this.actions$.pipe(
ofType(userPasswordSet),
map(action => action.req),
exhaustMap(req =>
this.serviceProtocolService.userPasswordSet(req).pipe(
map((res: UserPasswordSetResponse) => {
return userPasswordSetSuccess({
res
});
}),
catchError(error => of(userPasswordSetFailure({ error })))
)
)
)
);
userPasswordSetSuccess$ = createEffect(
() => {
return this.actions$.pipe(
ofType(userPasswordSetSuccess),
tap(async action => {
await this.dialogService.open<
AlertDialogComponent,
AlertDialogData,
AlertDialogResult
>(AlertDialogComponent, {
width: '360px',
disableClose: true,
data: {
title: '비밀번호 변경',
message: '비밀번호가 변경되었습니다. 다시 로그인하여 주십시오'
}
});
this.store.dispatch(logout());
})
);
},
{ dispatch: false }
);
userPasswordSetFailure$ = createEffect(
() => {
return this.actions$.pipe(
ofType(userPasswordSetFailure),
tap(action => {
this.snackBarService.open(
`비밀번호 변경 중에 문제가 발생하였습니다.`,
'',
{
duration: 3000,
verticalPosition: 'bottom'
}
);
})
);
},
{ dispatch: false }
);
constructor( constructor(
private actions$: Actions, private actions$: Actions,
private ngZone: NgZone, private ngZone: NgZone,
@ -334,8 +420,9 @@ export class Effects {
private appAuthenticationService: AppAuthenticationService, private appAuthenticationService: AppAuthenticationService,
private protocolService: ProtocolService, private protocolService: ProtocolService,
private authenticationProtocolService: AuthenticationProtocolService, private authenticationProtocolService: AuthenticationProtocolService,
private serviceProtocolService: ServiceProtocolService,
private dialogService: DialogService, private dialogService: DialogService,
private snackBarService: SnackBarService,
@Inject(UCAP_NATIVE_SERVICE) private nativeService: NativeService, @Inject(UCAP_NATIVE_SERVICE) private nativeService: NativeService,
private logger: NGXLogger private logger: NGXLogger
) {} ) {}

View File

@ -5,7 +5,8 @@ import {
increaseLoginFailCount, increaseLoginFailCount,
initialLoginFailCount, initialLoginFailCount,
logout, logout,
logoutInitialize logoutInitialize,
updateLoginRes
} from './actions'; } from './actions';
export const reducer = createReducer( export const reducer = createReducer(
@ -17,6 +18,13 @@ export const reducer = createReducer(
}; };
}), }),
on(updateLoginRes, (state, action) => {
return {
...state,
loginRes: action.loginRes
};
}),
on(increaseLoginFailCount, (state, action) => { on(increaseLoginFailCount, (state, action) => {
return { return {
...state, ...state,

View File

@ -6,7 +6,6 @@ import * as EventStore from './event';
import * as OptionStore from './option'; import * as OptionStore from './option';
import * as QueryStore from './query'; import * as QueryStore from './query';
import * as RoomStore from './room'; import * as RoomStore from './room';
import * as ServiceStore from './service';
import * as StatusStore from './status'; import * as StatusStore from './status';
import * as SyncStore from './sync'; import * as SyncStore from './sync';
import * as SettingsStore from './settings'; import * as SettingsStore from './settings';
@ -17,7 +16,6 @@ export interface State {
option: OptionStore.State; option: OptionStore.State;
query: QueryStore.State; query: QueryStore.State;
room: RoomStore.State; room: RoomStore.State;
service: ServiceStore.State;
status: StatusStore.State; status: StatusStore.State;
sync: SyncStore.State; sync: SyncStore.State;
settings: SettingsStore.State; settings: SettingsStore.State;
@ -29,7 +27,6 @@ export const effects: Type<any>[] = [
OptionStore.Effects, OptionStore.Effects,
QueryStore.Effects, QueryStore.Effects,
RoomStore.Effects, RoomStore.Effects,
ServiceStore.Effects,
StatusStore.Effects, StatusStore.Effects,
SyncStore.Effects, SyncStore.Effects,
SettingsStore.Effects SettingsStore.Effects
@ -42,7 +39,6 @@ export function reducers(state: State | undefined, action: Action) {
option: OptionStore.reducer, option: OptionStore.reducer,
query: QueryStore.reducer, query: QueryStore.reducer,
room: RoomStore.reducer, room: RoomStore.reducer,
service: ServiceStore.reducer,
status: StatusStore.reducer, status: StatusStore.reducer,
sync: SyncStore.reducer, sync: SyncStore.reducer,
settings: SettingsStore.reducer settings: SettingsStore.reducer
@ -66,9 +62,6 @@ export function selectors<S>(selector: Selector<any, State>) {
QuerySelector: QueryStore.selectors( QuerySelector: QueryStore.selectors(
createSelector(selector, (state: State) => state.query) createSelector(selector, (state: State) => state.query)
), ),
ServiceSelector: ServiceStore.selectors(
createSelector(selector, (state: State) => state.service)
),
StatusSelector: StatusStore.selectors( StatusSelector: StatusStore.selectors(
createSelector(selector, (state: State) => state.status) createSelector(selector, (state: State) => state.status)
), ),

View File

@ -1,23 +0,0 @@
import { createAction, props } from '@ngrx/store';
import {
UserPasswordSetRequest,
UserPasswordSetResponse
} from '@ucap-webmessenger/protocol-service';
export const userPasswordSet = createAction(
'[Account::Authentication] User Password Set',
props<{ req: UserPasswordSetRequest }>()
);
export const userPasswordSetSuccess = createAction(
'[Account::Authentication] User Password Set Success',
props<{
res: UserPasswordSetResponse;
}>()
);
export const userPasswordSetFailure = createAction(
'[Account::Authentication] User Password Set Failure',
props<{ error: any }>()
);

View File

@ -1,47 +0,0 @@
import { Injectable } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { NGXLogger } from 'ngx-logger';
import { catchError, exhaustMap, map } from 'rxjs/operators';
import { of } from 'rxjs';
import {
userPasswordSet,
userPasswordSetSuccess,
userPasswordSetFailure
} from './actions';
import {
ServiceProtocolService,
UserPasswordSetResponse
} from '@ucap-webmessenger/protocol-service';
@Injectable()
export class Effects {
userPasswordSet$ = createEffect(() =>
this.actions$.pipe(
ofType(userPasswordSet),
map(action => action.req),
exhaustMap(req =>
this.serviceProtocolService.userPasswordSet(req).pipe(
map((res: UserPasswordSetResponse) => {
return userPasswordSetSuccess({
res
});
}),
catchError(error => of(userPasswordSetFailure({ error })))
)
)
)
);
constructor(
private actions$: Actions,
private serviceProtocolService: ServiceProtocolService,
private store: Store<any>,
private logger: NGXLogger
) {}
}

View File

@ -1,4 +0,0 @@
export * from './actions';
export * from './effects';
export * from './reducers';
export * from './state';

View File

@ -1,4 +0,0 @@
import { createReducer, on } from '@ngrx/store';
import { initialState } from './state';
export const reducer = createReducer(initialState);

View File

@ -1,9 +0,0 @@
import { Selector, createSelector } from '@ngrx/store';
export interface State {}
export const initialState: State = {};
export function selectors<S>(selector: Selector<any, State>) {
return {};
}

View File

@ -0,0 +1,137 @@
import CryptoJS from 'crypto-js';
import { StringUtil, CharactorType } from '@ucap-webmessenger/core';
export interface PasswordValidationOption {
userId?: string;
phoneNumber?: string;
}
export enum PasswordValidationResult {
Valid = 'Valid',
Empty = 'Empty',
IncludeUserId = 'IncludeUserId',
IncludePhoneNumber = 'IncludePhoneNumber',
IncludeSpace = 'IncludeSpace',
IncludeRepeated = 'IncludeRepeated',
IncludeIncrements = 'IncludeIncrements',
IncludeDecrements = 'IncludeDecrements',
LackOfCombination = 'LackOfCombination',
TooShortCombinationLength = 'TooShortCombinationLength'
}
export class PasswordUtil {
static encrypt(password: string): string {
return CryptoJS.enc.Hex.stringify(CryptoJS.SHA256(password));
}
static validate(
password: string,
option?: PasswordValidationOption
): { code: PasswordValidationResult; extra?: any } {
if (!password || '' === password.trim()) {
return {
code: PasswordValidationResult.Empty
};
}
if (!!option && !!option.userId) {
if (-1 < password.indexOf(option.userId)) {
return {
code: PasswordValidationResult.IncludeUserId
};
}
}
if (!!option && !!option.phoneNumber) {
const phoneNumberOnly = option.phoneNumber
.trim()
.replace(new RegExp('-', 'g'), '');
switch (phoneNumberOnly.length) {
case 11:
if (-1 < password.indexOf(phoneNumberOnly.substr(3, 4))) {
return {
code: PasswordValidationResult.IncludePhoneNumber
};
}
if (-1 < password.indexOf(phoneNumberOnly.substr(7, 4))) {
return {
code: PasswordValidationResult.IncludePhoneNumber
};
}
break;
case 10:
if (-1 < password.indexOf(phoneNumberOnly.substr(3, 3))) {
return {
code: PasswordValidationResult.IncludePhoneNumber
};
}
if (-1 < password.indexOf(phoneNumberOnly.substr(6, 4))) {
return {
code: PasswordValidationResult.IncludePhoneNumber
};
}
break;
default:
break;
}
}
if (0 < StringUtil.includes(password, CharactorType.Space)) {
return {
code: PasswordValidationResult.IncludeSpace
};
}
if (StringUtil.isRepeat(password, 3)) {
return {
code: PasswordValidationResult.IncludeRepeated
};
}
if (StringUtil.isIncrements(password, 3)) {
return {
code: PasswordValidationResult.IncludeIncrements
};
}
if (StringUtil.isDecrements(password, 3)) {
return {
code: PasswordValidationResult.IncludeDecrements
};
}
let combinationCount = 0;
if (0 < StringUtil.includes(password, CharactorType.Special)) {
combinationCount++;
}
if (0 < StringUtil.includes(password, CharactorType.Number)) {
combinationCount++;
}
if (0 < StringUtil.includes(password, CharactorType.Charactor)) {
combinationCount++;
}
let minPwLen = 0;
if (2 < combinationCount) {
minPwLen = 8;
} else if (2 === combinationCount) {
minPwLen = 10;
} else if (2 > combinationCount) {
return {
code: PasswordValidationResult.LackOfCombination
};
}
if (password.length < minPwLen) {
return {
code: PasswordValidationResult.TooShortCombinationLength,
extra: {
combinationCount,
minPwLen
}
};
}
return {
code: PasswordValidationResult.Valid
};
}
}

View File

@ -12,6 +12,8 @@ export * from './lib/types/response-status.type';
export * from './lib/services/pi.service'; export * from './lib/services/pi.service';
export * from './lib/utils/password.util';
export * from './lib/ucap-pi.module'; export * from './lib/ucap-pi.module';
export * from './lib/config/urls'; export * from './lib/config/urls';

View File

@ -10,16 +10,17 @@ import { NGXLogger } from 'ngx-logger';
import { import {
makeWebSocketObservable, makeWebSocketObservable,
GetWebSocketResponses, GetWebSocketResponses,
NormalClosureMessage, NormalClosureMessage
} from '@ucap-webmessenger/web-socket'; } from '@ucap-webmessenger/web-socket';
import { PacketBody } from '../protocols/packet'; import { PacketBody } from '../protocols/packet';
import { import {
PacketBodyValueDivider, PacketBodyValueDivider,
PacketBodyDivider, PacketBodyDivider
} from '../types/packet-body-divider'; } from '../types/packet-body-divider';
import { PacketBodyValue } from '../types/packet-body-value.type'; import { PacketBodyValue } from '../types/packet-body-value.type';
import { SSVC_TYPE_ERROR_RES, ServerErrorCode } from '../types/service'; import { SSVC_TYPE_ERROR_RES } from '../types/service';
import { ServerErrorCode } from '../types/error-code';
import { ProtocolMessage } from '../protocols/protocol'; import { ProtocolMessage } from '../protocols/protocol';
import { _MODULE_CONFIG } from '../config/token'; import { _MODULE_CONFIG } from '../config/token';
@ -37,7 +38,7 @@ interface RequestState {
} }
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root'
}) })
export class ProtocolService { export class ProtocolService {
readonly urls: Urls; readonly urls: Urls;
@ -186,7 +187,7 @@ export class ProtocolService {
packet = this.encodePacket(serviceType, subServiceType, [ packet = this.encodePacket(serviceType, subServiceType, [
...bodyList, ...bodyList,
{ type: PacketBodyValue.RequestId, value: requestId }, { type: PacketBodyValue.RequestId, value: requestId }
]); ]);
responseSubject = new Subject<ProtocolMessage>().pipe( responseSubject = new Subject<ProtocolMessage>().pipe(
@ -209,7 +210,7 @@ export class ProtocolService {
this.pendingRequests.set(requestId, { this.pendingRequests.set(requestId, {
subject: responseSubject, subject: responseSubject,
request: { serviceType, subServiceType, bodyList }, request: { serviceType, subServiceType, bodyList }
}); });
} else { } else {
packet = this.encodePacket(serviceType, subServiceType, bodyList); packet = this.encodePacket(serviceType, subServiceType, bodyList);
@ -302,8 +303,8 @@ export class ProtocolService {
serviceType, serviceType,
subServiceType, subServiceType,
senderSeq, senderSeq,
bodyList, bodyList
}, }
}; };
} }

View File

@ -0,0 +1,48 @@
export enum ServerErrorCode {
/** 서버측에서 연결을 종료했습니다 */
ERRCD_FORCE_CLOSE = 10,
/** 통신 프로토콜이 일치하지않습니다 (최신 프로그램으로 업데이트 해주세요) */
ERRCD_PROTOCOL = 11,
/** 클라이언트 버전이 일치하지않습니다 (최신 프로그램으로 업데이트 해주세요) */
ERRCD_VERSION = 12,
/** 인증되지않은 기기입니다 */
ERRCD_DID = 13,
/** 로그인 아이디 또는 패스워드가 일치하지않습니다 */
ERRCD_IDPW = 14,
/** 다른 디바이스에서 로그인했습니다 */
ERRCD_DUPLICATE = 15,
/** 인증 토큰이 만료되었습니다 */
ERRCD_TOKEN = 16,
/** 프로그램을 재설치 바랍니다 */
ERRCD_SETUP_MAIN = 17,
/** 권한자/관리자에 의해 종료되었습니다 */
ERRCD_FORCE_INIT = 18,
/** 사용권한이 없습니다 */
ERRCD_NEED_AUTH_CLIENT = 19,
/** DEMO 서비스 기간이 종료되었습니다 */
ERRCD_SVC_EXPIRE = 99,
/** 요청 처리를 실패했습니다 */
ERRCD_FAILED = 100,
/** 데이터 처리를 실패했습니다 */
ERRCD_DATABASE = 101,
/** 최대치 넘음 */
ERRCD_EXCESS = 102,
/** 인증이 필요한 요청입니다 */
ERRCD_NEED_AUTH = 103,
/** */
ERRCD_USERINFO = 104,
/** 요청 처리를 실패했습니다(에러코드) */
ERRCD_INVALID_PARAM = 105,
/** 대화방에 함께 참여할 수 없는 사용자가 있습니다 */
ERRCD_INVALID_INVITE_USER = 106,
/** 상대방이 온라인 상태가 아닙니다 */
ERRCD_NOT_ONLINE = 200,
/** 패스워드 유효 기간이 만료 되었습니다 */
ERRCD_PW_EXPIRED = 300,
/** 요청을 처리할 권한이 없습니다 */
ERRCD_AUTH_DENY = 1000,
/** 암호화 처리 오류 */
ERRCD_ENCRYPT = 1001,
/** 다른 IP로 접속 요청 */
ERRCD_RECON = 1002
}

View File

@ -1,26 +1 @@
export const SSVC_TYPE_ERROR_RES = 1000; export const SSVC_TYPE_ERROR_RES = 1000;
export enum ServerErrorCode {
ERRCD_FORCE_CLOSE = 10,
ERRCD_PROTOCOL = 11,
ERRCD_VERSION = 12,
ERRCD_DID = 13,
ERRCD_IDPW = 14,
ERRCD_DUPLICATE = 15,
ERRCD_TOKEN = 16,
ERRCD_SETUP_MAIN = 17,
ERRCD_FORCE_INIT = 18,
ERRCD_SSO_AUTH = 19,
ERRCD_SSO_VALI = 20,
ERRCD_DEV_NOT_SUPPORTED = 21,
ERRCD_NO_MOBILE_PID = 22,
ERRCD_SVC_EXPIRE = 99,
ERRCD_FAILED = 100,
ERRCD_DATABASE = 101,
ERRCD_EXCESS = 102,
ERRCD_NEED_AUTH = 103,
ERRCD_USERINFO = 104, // ucap 은 없음.
ERRCD_AUTH_DENY = 1000,
ERRCD_ENCRYPT = 1001, // 패킷 암호화 오류
ERRCD_RECON = 1002 // 다른 IP로 접속 요청
}

View File

@ -10,6 +10,7 @@ export * from './lib/services/protocol.service';
export * from './lib/types/packet-body-divider'; export * from './lib/types/packet-body-divider';
export * from './lib/types/packet-body-value.type'; export * from './lib/types/packet-body-value.type';
export * from './lib/types/service'; export * from './lib/types/service';
export * from './lib/types/error-code';
export * from './lib/ucap-protocol.module'; export * from './lib/ucap-protocol.module';

View File

@ -5,43 +5,134 @@
<input <input
matInput matInput
type="password" type="password"
formControlName="currentLoginPw" [formControl]="currentLoginPwFormControl"
#currentLoginPw
/> />
<mat-error>
현재 패스워드를 입력해 주세요
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>신규 패스워드</mat-label> <mat-label>신규 패스워드</mat-label>
<input <input matInput type="password" [formControl]="newLoginPwFormControl" />
matInput
type="password"
formControlName="newLoginPw"
#newLoginPw
/>
<mat-error>
신규 패스워드를 입력해 주세요
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>신규 패스워드 확인</mat-label> <mat-label>신규 패스워드 확인</mat-label>
<input <input
matInput matInput
type="password" type="password"
formControlName="newConfirmLoginPw" [formControl]="newConfirmLoginPwFormControl"
#newConfirmLoginPw
/> />
<mat-error>
신규 패스워드 확인을 입력해 주세요
</mat-error>
</mat-form-field> </mat-form-field>
<div class="error-container">
<mat-error
*ngIf="
currentLoginPwFormControl.dirty &&
currentLoginPwFormControl.hasError('required')
"
>
현재 비밀번호를 입력해 주세요
</mat-error>
<mat-error
*ngIf="
currentLoginPwFormControl.dirty &&
currentLoginPwFormControl.hasError('ucapPasswordSame')
"
>
현재 비밀번호와 일치하지 않습니다
</mat-error>
<mat-error
*ngIf="
newLoginPwFormControl.dirty &&
newLoginPwFormControl.hasError('required')
"
>
신규 비밀번호를 입력해 주세요
</mat-error>
<mat-error
*ngIf="
newLoginPwFormControl.dirty &&
newLoginPwFormControl.hasError('ucapNotSameWith')
"
>
현재 비밀번호와 동일합니다
</mat-error>
<mat-error
*ngIf="
newLoginPwFormControl.dirty &&
newLoginPwFormControl.hasError('ucapPassword')
"
>
<ng-container
[ngSwitch]="
newLoginPwFormControl.getError('ucapPassword').result.code
"
>
<ng-container *ngSwitchCase="PasswordValidationResult.IncludeSpace">
비밀번호에는 공백을 입력할 수 없습니다
</ng-container>
<ng-container *ngSwitchCase="PasswordValidationResult.IncludeUserId">
사용자 ID를 비밀번호에 포함할 수 없습니다
</ng-container>
<ng-container
*ngSwitchCase="PasswordValidationResult.IncludePhoneNumber"
>
사용자 휴대폰번호를 비밀번호에 포함할 수 없습니다
</ng-container>
<ng-container
*ngSwitchCase="PasswordValidationResult.IncludeRepeated"
>
숫자나 문자를 3번이상 반복적으로 사용할 수 없습니다
</ng-container>
<ng-container
*ngSwitchCase="PasswordValidationResult.IncludeIncrements"
>
연속되는 숫자나 문자를 3번이상 사용할 수 없습니다
</ng-container>
<ng-container
*ngSwitchCase="PasswordValidationResult.IncludeDecrements"
>
연속되는 숫자나 문자를 3번이상 사용할 수 없습니다
</ng-container>
<ng-container
*ngSwitchCase="PasswordValidationResult.LackOfCombination"
>
문자, 숫자, 특수문자 중 2종류 이상 조합을 해야 합니다
</ng-container>
<ng-container
*ngSwitchCase="PasswordValidationResult.TooShortCombinationLength"
>
비밀번호는{{
newLoginPwFormControl.getError('ucapPassword').result.extra
.combinationCount
}}가지가 조합된 경우{{
newLoginPwFormControl.getError('ucapPassword').result.extra
.minPwLen
}}자를 넘어야 합니다
</ng-container>
</ng-container>
</mat-error>
<mat-error
*ngIf="
newConfirmLoginPwFormControl.dirty &&
newConfirmLoginPwFormControl.hasError('required')
"
>
신규 비밀번호 확인을 입력해 주세요
</mat-error>
<mat-error
*ngIf="
newConfirmLoginPwFormControl.dirty &&
newConfirmLoginPwFormControl.hasError('ucapSameWith')
"
>
신규 비밀번호와 신규 비밀번호 확인이 다릅니다
</mat-error>
</div>
<button <button
mat-raised-button mat-raised-button
class="submit-button bg-accent-dark" class="submit-button bg-accent-dark"
aria-label="패스워드 변경" aria-label="패스워드 변경"
[disabled]="changePasswordForm.invalid || !changePasswordBtnEnable" [disabled]="changePasswordForm.invalid"
(click)="onClickChangePassword()" (click)="onClickChangePassword()"
> >
패스워드 변경 패스워드 변경

View File

@ -17,6 +17,11 @@
width: 100%; width: 100%;
} }
.error-container {
height: 80px;
overflow: auto;
}
.submit-button { .submit-button {
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;

View File

@ -8,10 +8,66 @@ import {
ElementRef, ElementRef,
ChangeDetectorRef ChangeDetectorRef
} from '@angular/core'; } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import {
import { Company } from '@ucap-webmessenger/api-external'; FormControl,
import { LocalStorageService } from '@ucap-webmessenger/web-storage'; FormGroup,
import { LoginInfo, KEY_LOGIN_INFO } from '@app/types'; Validators,
ValidatorFn,
AbstractControl,
ValidationErrors,
FormBuilder
} from '@angular/forms';
import {
PasswordUtil,
PasswordValidationOption,
PasswordValidationResult
} from '@ucap-webmessenger/pi';
export function ucapPassword(option: PasswordValidationOption): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const result = PasswordUtil.validate(control.value, option);
return PasswordValidationResult.Valid !== result.code
? { ucapPassword: { result } }
: null;
};
}
export function ucapPasswordSame(value: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return !!value && value !== PasswordUtil.encrypt(control.value)
? { ucapPasswordSame: true }
: null;
};
}
export function ucapNotSameWith(targetControl: AbstractControl): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return !!targetControl &&
(targetControl.value as string) === (control.value as string)
? { ucapNotSameWith: true }
: null;
};
}
export function ucapSameWith(targetControl: AbstractControl): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return !!targetControl && targetControl.value !== control.value
? { ucapSameWith: true }
: null;
};
}
export function ucapNotSame(value: string | number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return !!value && value === control.value ? { ucapNotSame: true } : null;
};
}
export function ucapSame(value: string | number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return !!value && value !== control.value ? { ucapSame: true } : null;
};
}
@Component({ @Component({
selector: 'ucap-account-change-password', selector: 'ucap-account-change-password',
@ -20,48 +76,81 @@ import { LoginInfo, KEY_LOGIN_INFO } from '@app/types';
}) })
export class ChangePasswordComponent implements OnInit { export class ChangePasswordComponent implements OnInit {
@Input() @Input()
changePasswordBtnEnable: boolean; loginId: string;
@Input()
phoneNumber: string;
@Input()
encryptedLoginPw: string;
@Output() @Output()
changePassword = new EventEmitter<{ changePassword = new EventEmitter<{
currentLoginPw: string; currentLoginPw: string;
newLoginPw: string; newLoginPw: string;
newConfirmLoginPw: string;
notValid: () => void;
}>(); }>();
@ViewChild('currentLoginPw', { static: true })
currentLoginPwElementRef: ElementRef;
@ViewChild('newLoginPw', { static: true })
newLoginPwElementRef: ElementRef;
changePasswordForm: FormGroup; changePasswordForm: FormGroup;
currentLoginPwFormControl = new FormControl('');
newLoginPwFormControl = new FormControl('');
newConfirmLoginPwFormControl = new FormControl('');
PasswordValidationResult = PasswordValidationResult;
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef
private localStorageService: LocalStorageService
) {} ) {}
ngOnInit() { ngOnInit() {
const loginInfo: LoginInfo = this.localStorageService.get<LoginInfo>( const currentLoginPwValidators: ValidatorFn[] = [Validators.required];
KEY_LOGIN_INFO if (!!this.encryptedLoginPw) {
currentLoginPwValidators.push(ucapPasswordSame(this.encryptedLoginPw));
}
this.currentLoginPwFormControl.setValidators(currentLoginPwValidators);
let validateOption: PasswordValidationOption = {};
if (!!this.loginId) {
validateOption = {
...validateOption,
userId: this.loginId
};
}
if (!!this.phoneNumber) {
validateOption = {
...validateOption,
phoneNumber: this.phoneNumber
};
}
const newLoginPwValidators: ValidatorFn[] = [
Validators.required,
ucapNotSameWith(this.currentLoginPwFormControl),
ucapPassword(validateOption)
];
this.newLoginPwFormControl.setValidators(newLoginPwValidators);
const newConfirmLoginPwValidators: ValidatorFn[] = [
Validators.required,
ucapSameWith(this.newLoginPwFormControl)
];
this.newConfirmLoginPwFormControl.setValidators(
newConfirmLoginPwValidators
); );
this.changePasswordForm = this.formBuilder.group({ this.changePasswordForm = this.formBuilder.group({
currentLoginPw: ['', Validators.required], currentLoginPwFormControl: this.currentLoginPwFormControl,
newLoginPw: ['', Validators.required], newLoginPwFormControl: this.newLoginPwFormControl,
newConfirmLoginPw: ['', Validators.required] newConfirmLoginPwFormControl: this.newConfirmLoginPwFormControl
}); });
} }
onClickChangePassword() { onClickChangePassword() {
this.changePassword.emit({ this.changePassword.emit({
currentLoginPw: this.changePasswordForm.get('currentLoginPw').value, currentLoginPw: PasswordUtil.encrypt(
newLoginPw: this.changePasswordForm.get('newLoginPw').value, this.currentLoginPwFormControl.value
newConfirmLoginPw: this.changePasswordForm.get('newConfirmLoginPw').value, ),
notValid: () => { newLoginPw: PasswordUtil.encrypt(this.newLoginPwFormControl.value)
this.currentLoginPwElementRef.nativeElement.focus();
}
}); });
} }
} }

View File

@ -15,6 +15,39 @@
[path]="userInfo.profileImageFile" [path]="userInfo.profileImageFile"
[default]="'assets/images/img_nophoto_50.png'" [default]="'assets/images/img_nophoto_50.png'"
/> />
<mat-spinner
*ngIf="
profileImageFileUploadItem &&
profileImageFileUploadItem.uploadingProgress$
"
mode="determinate"
strokeWidth="5"
diameter="84"
[value]="profileImageFileUploadItem.uploadingProgress$ | async"
class="upload-profile-image-spinner"
></mat-spinner>
<button
mat-mini-fab
class="mat-elevation-z6 upload-profile-image-btn"
*ngIf="isMe"
matTooltip="프로필 이미지 변경"
matTooltipPosition="above"
[disabled]="
profileImageFileUploadItem &&
profileImageFileUploadItem.uploadingProgress$
"
(click)="profileImageFileInput.click()"
>
<span class="mdi mdi-upload mdi-24px"></span>
</button>
<input
type="file"
#profileImageFileInput
style="display: none"
(change)="onChangeFileInput()"
/>
</div> </div>
<div *ngIf="!isMe" class="profile-option"> <div *ngIf="!isMe" class="profile-option">

View File

@ -12,12 +12,12 @@
word-wrap: break-word; word-wrap: break-word;
} }
} }
::ng-deep .mat-card-header-text{ ::ng-deep .mat-card-header-text {
width:100%; width: 100%;
.mat-card-subtitle{ .mat-card-subtitle {
color: rgb(256, 256, 256, 0.7) !important; color: rgb(256, 256, 256, 0.7) !important;
text-align:center; text-align: center;
margin-top:10px !important; margin-top: 10px !important;
} }
} }
@ -25,78 +25,90 @@
width: 400px; width: 400px;
padding: 0 0 20px; padding: 0 0 20px;
position: relative; position: relative;
.mat-card-header{ .mat-card-header {
justify-content: center; justify-content: center;
padding-bottom: 40px; padding-bottom: 40px;
background: #76d9c5; background: #76d9c5;
/*background: linear-gradient(to right, #345385, #ef4c73);*/ /*background: linear-gradient(to right, #345385, #ef4c73);*/
color: #ffffff; color: #ffffff;
padding-top: 20px; padding-top: 20px;
width:100%; width: 100%;
.mat-card-title{ .mat-card-title {
margin-bottom: 12px; margin-bottom: 12px;
max-width: 100%; max-width: 100%;
justify-content: center; justify-content: center;
display: flex; display: flex;
margin:0 20px; margin: 0 20px;
span{ span {
@include ellipsis(1); @include ellipsis(1);
} }
} }
} }
.mat-card-content{ .mat-card-content {
margin-top:-40px; margin-top: -40px;
.profile-img{ .profile-img {
display:flex; display: flex;
height:80px; height: 80px;
justify-content: center; justify-content: center;
margin-bottom:20px; margin-bottom: 20px;
img{ img {
widows: 80px; widows: 80px;
height: 80px; height: 80px;
border-radius: 50%; border-radius: 50%;
background-color:#efefef; background-color: #efefef;
border:2px solid #ffffff; border: 2px solid #ffffff;
}
.upload-profile-image-spinner {
position: absolute;
top: 90px;
left: 160px;
}
.upload-profile-image-btn {
position: absolute;
top: 140px;
left: 210px;
} }
} }
.profile-option{ .profile-option {
display:flex; display: flex;
padding:0 20px; padding: 0 20px;
color:#ffffff; color: #ffffff;
margin-top: -100px; margin-top: -100px;
height: 120px; height: 120px;
.btn-favorite{ .btn-favorite {
cursor: pointer; cursor: pointer;
.on{ .on {
fill:yellow; fill: yellow;
} }
} }
.btn-groupadd{ .btn-groupadd {
margin-left:auto; margin-left: auto;
cursor: pointer; cursor: pointer;
svg{ svg {
display:none; display: none;
&.on{ &.on {
display:block; display: block;
} }
} }
} }
} }
ul{ ul {
padding:0 20px; padding: 0 20px;
display:flex; display: flex;
flex-flow: column; flex-flow: column;
margin-top:-20px; margin-top: -20px;
li{ li {
display:inline-flex; display: inline-flex;
height:30px; height: 30px;
align-items: center; align-items: center;
flex-flow:row; flex-flow: row;
margin-bottom:20px; margin-bottom: 20px;
svg{ svg {
margin-right:10px; margin-right: 10px;
color:#777777; color: #777777;
} }
} }
} }

View File

@ -1,11 +1,16 @@
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core'; import {
Component,
OnInit,
Input,
EventEmitter,
Output,
ViewChild,
ElementRef
} from '@angular/core';
import { UserInfo } from '@ucap-webmessenger/protocol-sync'; import { UserInfo } from '@ucap-webmessenger/protocol-sync';
import { import { UserInfoF } from '@ucap-webmessenger/protocol-query';
UserInfoSS, import { FileUploadItem } from '@ucap-webmessenger/api-common';
UserInfoF,
UserInfoDN
} from '@ucap-webmessenger/protocol-query';
@Component({ @Component({
selector: 'ucap-profile-profile', selector: 'ucap-profile-profile',
@ -37,6 +42,14 @@ export class ProfileComponent implements OnInit {
isBuddy: boolean; isBuddy: boolean;
}>(); }>();
@Output()
uploadProfileImage = new EventEmitter<FileUploadItem>();
@ViewChild('profileImageFileInput', { static: false })
profileImageFileInput: ElementRef<HTMLInputElement>;
profileImageFileUploadItem: FileUploadItem;
constructor() {} constructor() {}
ngOnInit() {} ngOnInit() {}
@ -73,4 +86,14 @@ export class ProfileComponent implements OnInit {
isBuddy: false isBuddy: false
}); });
} }
onChangeFileInput() {
this.profileImageFileUploadItem = FileUploadItem.fromFiles(
this.profileImageFileInput.nativeElement.files
)[0];
this.uploadProfileImage.emit(this.profileImageFileUploadItem);
this.profileImageFileInput.nativeElement.value = '';
}
} }

View File

@ -8,6 +8,7 @@ import { FlexLayoutModule } from '@angular/flex-layout';
import { MatRippleModule, MatCheckboxModule } from '@angular/material'; import { MatRippleModule, MatCheckboxModule } from '@angular/material';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { UCapUiModule } from '@ucap-webmessenger/ui'; import { UCapUiModule } from '@ucap-webmessenger/ui';
@ -35,6 +36,7 @@ const SERVICES = [];
MatCheckboxModule, MatCheckboxModule,
MatCardModule, MatCardModule,
MatTooltipModule, MatTooltipModule,
MatProgressSpinnerModule,
UCapUiModule UCapUiModule
], ],

View File

@ -5,15 +5,19 @@ import {
Output, Output,
Input, Input,
AfterViewInit, AfterViewInit,
OnInit OnInit,
OnChanges,
SimpleChanges
} from '@angular/core'; } from '@angular/core';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
const PATH = 'path';
@Directive({ @Directive({
selector: 'img[ucapImage]' selector: 'img[ucapImage]'
}) })
export class ImageDirective implements OnInit, AfterViewInit { export class ImageDirective implements OnInit, AfterViewInit, OnChanges {
@Input() @Input()
base: string; base: string;
@ -50,6 +54,18 @@ export class ImageDirective implements OnInit, AfterViewInit {
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.loadImage();
}
ngOnChanges(changes: SimpleChanges): void {
const pathChanges = changes[PATH];
if (!!pathChanges && !pathChanges.firstChange) {
this.loadImage();
}
}
private loadImage(): void {
if (this.imageSrc === this.default) { if (this.imageSrc === this.default) {
this.elementRef.nativeElement.src = this.default; this.elementRef.nativeElement.src = this.default;
this.loaded.emit(this.elementRef.nativeElement); this.loaded.emit(this.elementRef.nativeElement);

View File

@ -1,12 +1,12 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { BottomSheetService } from './bottom-sheet.service'; import { SnackBarService } from './snack-bar.service';
describe('ui::BottomSheetService', () => { describe('ui::SnackBarService', () => {
beforeEach(() => TestBed.configureTestingModule({})); beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => { it('should be created', () => {
const service: BottomSheetService = TestBed.get(BottomSheetService); const service: SnackBarService = TestBed.get(SnackBarService);
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
}); });