upload of profile image is implemented

This commit is contained in:
병준 박 2019-11-29 18:24:51 +09:00
parent 4da93a5351
commit eb542bd923
11 changed files with 310 additions and 74 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

@ -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

@ -113,3 +113,10 @@ export const userPasswordSetFailure = createAction(
'[Account::Authentication] User Password Set Failure', '[Account::Authentication] User Password Set Failure',
props<{ error: any }>() props<{ error: any }>()
); );
export const updateLoginRes = createAction(
'[Account::Authentication] Update LoginRes',
props<{
loginRes: LoginResponse;
}>()
);

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

@ -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,19 @@ 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();
this.logger.debug('ucapImage.ngOnChanges', changes);
}
}
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);