diff --git a/projects/ucap-webmessenger-api-common/src/lib/apis/file-profile-save.ts b/projects/ucap-webmessenger-api-common/src/lib/apis/file-profile-save.ts index 22ef07f0..9d42a1b2 100644 --- a/projects/ucap-webmessenger-api-common/src/lib/apis/file-profile-save.ts +++ b/projects/ucap-webmessenger-api-common/src/lib/apis/file-profile-save.ts @@ -2,35 +2,67 @@ import { DeviceType } from '@ucap-webmessenger/core'; import { APIRequest, APIResponse, - APIEncoder, APIDecoder, - ParameterUtil + ParameterUtil, + APIFormDataEncoder, + JsonAnalization, + StatusCode } from '@ucap-webmessenger/api'; +import { FileUploadItem } from '../models/file-upload-item'; export interface FileProfileSaveRequest extends APIRequest { userSeq: number; deviceType: DeviceType; token: string; file?: File; + fileUploadItem: FileUploadItem; intro?: string; initProfileImage?: boolean; } export interface FileProfileSaveResponse extends APIResponse { - ProfileURL?: string; - ProfileSubDir?: string; + profileURL?: 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 = ( +export const encodeFileProfileSave: APIFormDataEncoder = ( 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 = ( 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; + } }; diff --git a/projects/ucap-webmessenger-api-common/src/lib/services/common-api.service.ts b/projects/ucap-webmessenger-api-common/src/lib/services/common-api.service.ts index 53c6a91b..79db4fef 100644 --- a/projects/ucap-webmessenger-api-common/src/lib/services/common-api.service.ts +++ b/projects/ucap-webmessenger-api-common/src/lib/services/common-api.service.ts @@ -94,15 +94,29 @@ export class CommonApiService { req: FileProfileSaveRequest, fileProfileSaveUrl?: string ): Observable { - return this.httpClient - .post( - !!fileProfileSaveUrl ? fileProfileSaveUrl : this.urls.fileProfileSave, - {}, - { - params: encodeFileProfileSave(req) + const httpReq = new HttpRequest( + 'POST', + !!fileProfileSaveUrl ? fileProfileSaveUrl : this.urls.fileProfileSave, + encodeFileProfileSave(req), + { reportProgress: true, responseType: 'text' as 'json' } + ); + + 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)); } - ) - .pipe(map(res => decodeFileProfileSave(res))); + return false; + }), + map((event: HttpResponse) => { + req.fileUploadItem.uploadComplete(); + return decodeFileProfileSave(event.body); + }) + ); } public urlForFileTalkDownload( diff --git a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/profile/profile.dialog.component.html b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/profile/profile.dialog.component.html index c8ab5d39..8649814b 100644 --- a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/profile/profile.dialog.component.html +++ b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/profile/profile.dialog.component.html @@ -1,5 +1,5 @@ diff --git a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/profile/profile.dialog.component.ts b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/profile/profile.dialog.component.ts index e387083b..2368e59b 100644 --- a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/profile/profile.dialog.component.ts +++ b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/profile/profile.dialog.component.ts @@ -8,6 +8,7 @@ import { Store, select } from '@ngrx/store'; import * as AppStore from '@app/store'; import * as ChatStore from '@app/store/messenger/chat'; 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 { @@ -15,12 +16,28 @@ import { UserInfoF, UserInfoDN } 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 { LoginResponse } from '@ucap-webmessenger/protocol-authentication'; -import { map } from 'rxjs/operators'; +import { map, take, finalize } from 'rxjs/operators'; 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 { userInfo: UserInfo | UserInfoSS | UserInfoF | UserInfoDN; @@ -34,8 +51,11 @@ export interface ProfileDialogResult {} styleUrls: ['./profile.dialog.component.scss'] }) export class ProfileDialogComponent implements OnInit, OnDestroy { + userInfo: UserInfo | UserInfoSS | UserInfoF | UserInfoDN; loginRes: LoginResponse; sessionVerinfo: VersionInfo2Response; + environmentsInfo: EnvironmentsInfo; + isMe: boolean; isBuddy: boolean; isFavorit: boolean; @@ -47,6 +67,8 @@ export class ProfileDialogComponent implements OnInit, OnDestroy { @Inject(MAT_DIALOG_DATA) public data: ProfileDialogData, private dialogService: DialogService, private sessionStorageService: SessionStorageService, + private commonApiService: CommonApiService, + private snackBarService: SnackBarService, private store: Store ) { this.sessionVerinfo = this.sessionStorageService.get( @@ -55,6 +77,11 @@ export class ProfileDialogComponent implements OnInit, OnDestroy { this.loginRes = this.sessionStorageService.get( KEY_LOGIN_RES_INFO ); + this.environmentsInfo = this.sessionStorageService.get( + KEY_ENVIRONMENTS_INFO + ); + + this.userInfo = data.userInfo; } ngOnInit() { @@ -156,8 +183,68 @@ export class ProfileDialogComponent implements OnInit, OnDestroy { this.store.dispatch( 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' + } + ); + } + ); + } } diff --git a/projects/ucap-webmessenger-app/src/app/store/account/authentication/actions.ts b/projects/ucap-webmessenger-app/src/app/store/account/authentication/actions.ts index 26c09e5c..b2ddbf81 100644 --- a/projects/ucap-webmessenger-app/src/app/store/account/authentication/actions.ts +++ b/projects/ucap-webmessenger-app/src/app/store/account/authentication/actions.ts @@ -113,3 +113,10 @@ export const userPasswordSetFailure = createAction( '[Account::Authentication] User Password Set Failure', props<{ error: any }>() ); + +export const updateLoginRes = createAction( + '[Account::Authentication] Update LoginRes', + props<{ + loginRes: LoginResponse; + }>() +); diff --git a/projects/ucap-webmessenger-app/src/app/store/account/authentication/reducers.ts b/projects/ucap-webmessenger-app/src/app/store/account/authentication/reducers.ts index a00d9ba7..9b305d30 100644 --- a/projects/ucap-webmessenger-app/src/app/store/account/authentication/reducers.ts +++ b/projects/ucap-webmessenger-app/src/app/store/account/authentication/reducers.ts @@ -5,7 +5,8 @@ import { increaseLoginFailCount, initialLoginFailCount, logout, - logoutInitialize + logoutInitialize, + updateLoginRes } from './actions'; 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) => { return { ...state, diff --git a/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.html b/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.html index 240fca97..d5a1936a 100644 --- a/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.html +++ b/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.html @@ -15,6 +15,39 @@ [path]="userInfo.profileImageFile" [default]="'assets/images/img_nophoto_50.png'" /> + + + + +
diff --git a/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.scss b/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.scss index 60953038..2a1c3ef6 100644 --- a/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.scss +++ b/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.scss @@ -12,12 +12,12 @@ word-wrap: break-word; } } -::ng-deep .mat-card-header-text{ - width:100%; - .mat-card-subtitle{ +::ng-deep .mat-card-header-text { + width: 100%; + .mat-card-subtitle { color: rgb(256, 256, 256, 0.7) !important; - text-align:center; - margin-top:10px !important; + text-align: center; + margin-top: 10px !important; } } @@ -25,78 +25,90 @@ width: 400px; padding: 0 0 20px; position: relative; - .mat-card-header{ + .mat-card-header { justify-content: center; padding-bottom: 40px; background: #76d9c5; /*background: linear-gradient(to right, #345385, #ef4c73);*/ color: #ffffff; padding-top: 20px; - width:100%; - .mat-card-title{ + width: 100%; + .mat-card-title { margin-bottom: 12px; max-width: 100%; justify-content: center; display: flex; - margin:0 20px; - span{ + margin: 0 20px; + span { @include ellipsis(1); } } } - .mat-card-content{ - margin-top:-40px; - .profile-img{ - display:flex; - height:80px; + .mat-card-content { + margin-top: -40px; + .profile-img { + display: flex; + height: 80px; justify-content: center; - margin-bottom:20px; - img{ + margin-bottom: 20px; + img { widows: 80px; height: 80px; border-radius: 50%; - background-color:#efefef; - border:2px solid #ffffff; + background-color: #efefef; + 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{ - display:flex; - padding:0 20px; - color:#ffffff; + .profile-option { + display: flex; + padding: 0 20px; + color: #ffffff; margin-top: -100px; height: 120px; - .btn-favorite{ + .btn-favorite { cursor: pointer; - .on{ - fill:yellow; + .on { + fill: yellow; } } - .btn-groupadd{ - margin-left:auto; + .btn-groupadd { + margin-left: auto; cursor: pointer; - svg{ - display:none; - &.on{ - display:block; + svg { + display: none; + &.on { + display: block; } } } } - ul{ - padding:0 20px; - display:flex; + ul { + padding: 0 20px; + display: flex; flex-flow: column; - margin-top:-20px; - li{ - display:inline-flex; - height:30px; + margin-top: -20px; + li { + display: inline-flex; + height: 30px; align-items: center; - flex-flow:row; - margin-bottom:20px; + flex-flow: row; + margin-bottom: 20px; - svg{ - margin-right:10px; - color:#777777; + svg { + margin-right: 10px; + color: #777777; } } } diff --git a/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.ts b/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.ts index a7612d97..e1a84944 100644 --- a/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.ts +++ b/projects/ucap-webmessenger-ui-profile/src/lib/components/profile.component.ts @@ -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 { - UserInfoSS, - UserInfoF, - UserInfoDN -} from '@ucap-webmessenger/protocol-query'; +import { UserInfoF } from '@ucap-webmessenger/protocol-query'; +import { FileUploadItem } from '@ucap-webmessenger/api-common'; @Component({ selector: 'ucap-profile-profile', @@ -37,6 +42,14 @@ export class ProfileComponent implements OnInit { isBuddy: boolean; }>(); + @Output() + uploadProfileImage = new EventEmitter(); + + @ViewChild('profileImageFileInput', { static: false }) + profileImageFileInput: ElementRef; + + profileImageFileUploadItem: FileUploadItem; + constructor() {} ngOnInit() {} @@ -73,4 +86,14 @@ export class ProfileComponent implements OnInit { isBuddy: false }); } + + onChangeFileInput() { + this.profileImageFileUploadItem = FileUploadItem.fromFiles( + this.profileImageFileInput.nativeElement.files + )[0]; + + this.uploadProfileImage.emit(this.profileImageFileUploadItem); + + this.profileImageFileInput.nativeElement.value = ''; + } } diff --git a/projects/ucap-webmessenger-ui-profile/src/lib/ucap-ui-profile.module.ts b/projects/ucap-webmessenger-ui-profile/src/lib/ucap-ui-profile.module.ts index 1849a2ad..feb12152 100644 --- a/projects/ucap-webmessenger-ui-profile/src/lib/ucap-ui-profile.module.ts +++ b/projects/ucap-webmessenger-ui-profile/src/lib/ucap-ui-profile.module.ts @@ -8,6 +8,7 @@ import { FlexLayoutModule } from '@angular/flex-layout'; import { MatRippleModule, MatCheckboxModule } from '@angular/material'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { UCapUiModule } from '@ucap-webmessenger/ui'; @@ -35,6 +36,7 @@ const SERVICES = []; MatCheckboxModule, MatCardModule, MatTooltipModule, + MatProgressSpinnerModule, UCapUiModule ], diff --git a/projects/ucap-webmessenger-ui/src/lib/directives/image.directive.ts b/projects/ucap-webmessenger-ui/src/lib/directives/image.directive.ts index 412f15fb..64d91329 100644 --- a/projects/ucap-webmessenger-ui/src/lib/directives/image.directive.ts +++ b/projects/ucap-webmessenger-ui/src/lib/directives/image.directive.ts @@ -5,15 +5,19 @@ import { Output, Input, AfterViewInit, - OnInit + OnInit, + OnChanges, + SimpleChanges } from '@angular/core'; import { NGXLogger } from 'ngx-logger'; +const PATH = 'path'; + @Directive({ selector: 'img[ucapImage]' }) -export class ImageDirective implements OnInit, AfterViewInit { +export class ImageDirective implements OnInit, AfterViewInit, OnChanges { @Input() base: string; @@ -50,6 +54,19 @@ export class ImageDirective implements OnInit, AfterViewInit { } 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) { this.elementRef.nativeElement.src = this.default; this.loaded.emit(this.elementRef.nativeElement);