From 8630261f42b5f97ddbb1249a83e99fbbeef802cb Mon Sep 17 00:00:00 2001 From: Park Byung Eun Date: Fri, 14 Feb 2020 16:00:15 +0900 Subject: [PATCH] =?UTF-8?q?2=EC=9B=94=EC=A7=B8=EC=A3=BC=20=EC=97=85?= =?UTF-8?q?=EB=AC=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- documents/업무/2월/02_TODO | 91 +- .../업무/2월/{1번째주 => 1째주}/0206.txt | 0 .../업무/2월/{1번째주 => 1째주}/0207.txt | 0 .../업무/2월/{2번째주 => 2째주}/0210.txt | 3 +- .../업무/2월/{2번째주 => 2째주}/0211.txt | 0 .../업무/2월/{2번째주 => 2째주}/0212.txt | 3 +- documents/업무/2월/2째주/0213.txt | 151 ++ documents/업무/2월/2째주/0214.txt | 37 + .../업무/2월/2째주/LF_개인주소록_프로토콜.txt | 34 + .../2째주/묵음파일백업/album-box.component.html | 182 ++ .../2째주/묵음파일백업/album-box.component.ts | 264 +++ .../묵음파일백업/bundle-image.event-json.ts | 36 + .../2째주/묵음파일백업/common-api.service.ts | 382 +++ .../2째주/묵음파일백업/file-talk-save-multi.ts | 79 + .../묵음파일백업/file-upload-queue.component.html | 44 + .../묵음파일백업/file-upload-queue.component.scss | 55 + .../묵음파일백업/file-upload-queue.component.ts | 117 + .../2째주/묵음파일백업/messages.component.html | 384 +++ .../2째주/묵음파일백업/messages.component.scss | 233 ++ .../2째주/묵음파일백업/messages.component.ts | 2086 +++++++++++++++++ documents/업무/2월/2째주/묶음파일변경사항.txt | 1 + .../{ => 1월}/주간보고_박병은_2020.0110.pptx | Bin .../{ => 1월}/주간보고_박병은_2020.0117.pptx | Bin .../{ => 1월}/주간보고_박병은_2020.0123.pptx | Bin .../{ => 1월}/주간보고_박병은_2020.0131.pptx | Bin .../{ => 2월}/주간보고_박병은_2020.0207.pptx | Bin .../2월/주간보고_박병은_2020.0214.pptx | Bin 0 -> 26458 bytes 27 files changed, 4155 insertions(+), 27 deletions(-) rename documents/업무/2월/{1번째주 => 1째주}/0206.txt (100%) rename documents/업무/2월/{1번째주 => 1째주}/0207.txt (100%) rename documents/업무/2월/{2번째주 => 2째주}/0210.txt (98%) rename documents/업무/2월/{2번째주 => 2째주}/0211.txt (100%) rename documents/업무/2월/{2번째주 => 2째주}/0212.txt (99%) create mode 100644 documents/업무/2월/2째주/0213.txt create mode 100644 documents/업무/2월/2째주/0214.txt create mode 100644 documents/업무/2월/2째주/LF_개인주소록_프로토콜.txt create mode 100644 documents/업무/2월/2째주/묵음파일백업/album-box.component.html create mode 100644 documents/업무/2월/2째주/묵음파일백업/album-box.component.ts create mode 100644 documents/업무/2월/2째주/묵음파일백업/bundle-image.event-json.ts create mode 100644 documents/업무/2월/2째주/묵음파일백업/common-api.service.ts create mode 100644 documents/업무/2월/2째주/묵음파일백업/file-talk-save-multi.ts create mode 100644 documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.html create mode 100644 documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.scss create mode 100644 documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.ts create mode 100644 documents/업무/2월/2째주/묵음파일백업/messages.component.html create mode 100644 documents/업무/2월/2째주/묵음파일백업/messages.component.scss create mode 100644 documents/업무/2월/2째주/묵음파일백업/messages.component.ts create mode 100644 documents/업무/2월/2째주/묶음파일변경사항.txt rename weekly-report/{ => 1월}/주간보고_박병은_2020.0110.pptx (100%) rename weekly-report/{ => 1월}/주간보고_박병은_2020.0117.pptx (100%) rename weekly-report/{ => 1월}/주간보고_박병은_2020.0123.pptx (100%) rename weekly-report/{ => 1월}/주간보고_박병은_2020.0131.pptx (100%) rename weekly-report/{ => 2월}/주간보고_박병은_2020.0207.pptx (100%) create mode 100644 weekly-report/2월/주간보고_박병은_2020.0214.pptx diff --git a/documents/업무/2월/02_TODO b/documents/업무/2월/02_TODO index 57d6ac7..ea3c791 100644 --- a/documents/업무/2월/02_TODO +++ b/documents/업무/2월/02_TODO @@ -1,5 +1,5 @@ -묶음파일 - 업로드 +묶음파일 + 업로드 (부분 완료) 묶음파일 request 모델 정의 userSeq: number; deviceType: DeviceType; @@ -24,6 +24,12 @@ 이벤트 타입별 출력 묶음파일 타입 그리드 썸네일 컴포넌트 정의 그리드 알고리즘 작성 + 썸네일 출력(완료) + 출력 가이드 라인 + 최대 가로 출력 개수 3개 + 다음행에 출력 개수가 홀수 일때 + 빈 공간이 출력되지 않게 조정 + 최대 width, 최소 height 테스트 후 결정 앨범함 묶음파일 타입 처리 뷰어 컴포넌트 (슬라이드 기능) (진행) @@ -33,13 +39,6 @@ 일반 이미지 출력 동영상인 경우 일반 동영상 썸네일 출력 - 썸네일 출력 - 출력 가이드 라인 - 최대 가로 출력 개수 3개 - 다음행에 출력 개수가 홀수 일때 - 빈 공간이 출력되지 않게 조정 - 최대 width, 최소 height 테스트 후 결정 - 카톡 벤치마킹 묶음파일 전송 후 1개의 이미지 전송 @@ -47,6 +46,7 @@ 동영상 전송 동영상 썸네일 출력 동영상은 묶음파일 지원안함 + 묶음파일 기획은 카톡과 동일하게 진행 원본 출력 //원본 파일 호출할 때 리플레이스 @@ -93,26 +93,67 @@ 기능 목록 모바일 주소록 동기화 기존방식 (데이터가 많은 경우 중간 서버에서 끊길 가능성이 농후) - PC 서버 요청 - 모바일 노티 - for - 모바일 주소록 서버 전송 - 모바일 주소록 서버 응답 - 서버가 주소록 리시브 노티 + 1. PC -> Server + 모바일 주소록 동기화 시작 + SSVC_TYPE_SYNC_PHONEBOOK_READY_REQ + 2. Server -> Mobile + 모바일 주소록 동기화 노티 + 3. Mobile -> Server + 준비 완료 요청 + 4. Server -> PC + SSVC_TYPE_SYNC_PHONEBOOK_READY_NOTI + 5. PC -> Server + 모바일 주소록 받을 준비 완료 요청 + SSVC_TYPE_SYNC_PHONEBOOK_READY_OK_REQ + 6.Server -> PC + SSVC_TYPE_SYNC_PHONEBOOK_READY_OK_RES //사용없음 + SSVC_TYPE_SYNC_PHONEBOOK_READY_OK_NOTI + PC가 모바일로부터 준비 확인 NOTI를 받는 처리를 수행 + 7.Server -> PC + SSVC_TYPE_SYNC_PHONEBOOK_SND_RES //사용없음 + SSVC_TYPE_SYNC_PHONEBOOK_SND_NOTI //실데이터받는 프로토콜 박차장님이 제안한 방식 - PC 서버 요청 - 모바일 노티 - 모바일 주소록 JSON 으로 전송 - 새로운 방식 - 생각해봐야함 - 서버에 개인주소록을 저장할 경우 - PC에서 동기화 완료 후 서버 데이터 삭제 고려 - 서버에 개인주소록을 저장하지 않을 경우 - 서버 부하 고려 + PC -> 서버 + 개인 주소록 동기화 요청 + 프로토콜 정의 협의 + 서버 -> 모바일 + 개인 주소록 동기화 요청에 대한 푸시 + 모바일 -> 서버 (REST API) + 개인 주소록 POST 전송 + 데이터 JSON 형태 + 서버 -> 모바일 (REST API) + 개인 주소록 전송에 대한 응답 + (응답코드, 싱크번호, 시간, 사용자번호) + 모바일 -> 서버 + 개인 주소록 전송 동기화 프로토콜 요청 + (싱크번호, 시간, 사용자번호) + 서버 -> 모바일 + 개인 주소록 전송 동기화 프로토콜 요청에 대한 응답 + 서버 -> PC + 개인 주소록 동기화 요청 프로토콜에 대한 푸시 + (싱크번호, 사용자번호) + PC -> 서버 (REST API) + 개인 주소록 요청 + (싱크번호, 사용자번호) + 서버 -> PC + 개인 주소록 JSON + (싱크번호, 시간, 사용자 번호) + 모바일 주소록 초기화 PC에 저장된 모바일 주소록을 삭제 엑샐 Export/Import 템플릿 초기화 -MAC용 빌드 \ No newline at end of file +MAC용 빌드 + +대화 저장 + 두개다 대화내용 암호화? + 서버에서 대화내용을 제공 + 프로토콜 협의 + PC에서 대화내용 조회후 제공 + 서버 부하 + + 협의 필요 + + diff --git a/documents/업무/2월/1번째주/0206.txt b/documents/업무/2월/1째주/0206.txt similarity index 100% rename from documents/업무/2월/1번째주/0206.txt rename to documents/업무/2월/1째주/0206.txt diff --git a/documents/업무/2월/1번째주/0207.txt b/documents/업무/2월/1째주/0207.txt similarity index 100% rename from documents/업무/2월/1번째주/0207.txt rename to documents/업무/2월/1째주/0207.txt diff --git a/documents/업무/2월/2번째주/0210.txt b/documents/업무/2월/2째주/0210.txt similarity index 98% rename from documents/업무/2월/2번째주/0210.txt rename to documents/업무/2월/2째주/0210.txt index d37f824..60ab7ef 100644 --- a/documents/업무/2월/2번째주/0210.txt +++ b/documents/업무/2월/2째주/0210.txt @@ -33,4 +33,5 @@ tems[position].FILE_THUMB_URL .replace("WebFile", "AttFile") .replace(".thumb.jpg", "") -클라이언트 참고 사항입니ㅏㄷ..실제 파일 열기 하실때는 실제 업로드된 파일경로로 접속하셔야 합니다. \ No newline at end of file +클라이언트 참고 사항입니ㅏㄷ..실제 파일 열기 하실때는 실제 업로드된 파일경로로 접속하셔야 합니다. + \ No newline at end of file diff --git a/documents/업무/2월/2번째주/0211.txt b/documents/업무/2월/2째주/0211.txt similarity index 100% rename from documents/업무/2월/2번째주/0211.txt rename to documents/업무/2월/2째주/0211.txt diff --git a/documents/업무/2월/2번째주/0212.txt b/documents/업무/2월/2째주/0212.txt similarity index 99% rename from documents/업무/2월/2번째주/0212.txt rename to documents/업무/2월/2째주/0212.txt index 5998b69..4abab34 100644 --- a/documents/업무/2월/2번째주/0212.txt +++ b/documents/업무/2월/2째주/0212.txt @@ -13,7 +13,7 @@ fileInfo 조회 decodeInfoData-> - + WARNING in Circular dependency detected: projects\ucap-webmessenger-protocol-event\src\lib\protocols\event-json\codec.ts -> projects\ucap-webmessenger-protocol-event\src\lib\protocols\event-json\bundle-image.event-json.ts -> @@ -64,6 +64,7 @@ projects\ucap-webmessenger-protocol-file\src\public-api.ts -> projects\ucap-webm 1: "16F98292020-02-10 07:09:16{ + ↵"StatusCode":"200", ↵"ErrorMessage":"", ↵"RoomID":"76", diff --git a/documents/업무/2월/2째주/0213.txt b/documents/업무/2월/2째주/0213.txt new file mode 100644 index 0000000..50f95ba --- /dev/null +++ b/documents/업무/2월/2째주/0213.txt @@ -0,0 +1,151 @@ +// 그리드 함수 백업 + + makeGrid() { + const totalCount = this.bundleImageJson.fileCount; + const remainder = totalCount % 3; + const quotient = Math.floor(totalCount / 3); + + let tile; + + if (remainder === 0) { + this.bundleImageJson.thumbUrls.forEach((v, idx) => { + tile = {} as Tile; + tile.cols = 2; + this.getTile(tile, v); + }); + } else if (remainder === 1) { + if (quotient === 0) { + tile = {} as Tile; + this.bundleImageJson.thumbUrls.forEach(v => { + this.getTile(tile, v); + }); + tile.cols = 6; + return; + } + + this.bundleImageJson.thumbUrls.forEach((v, idx) => { + tile = {} as Tile; + if (quotient <= idx / 3 + 1) { + tile.cols = 3; + } else { + tile.cols = 2; + } + this.getTile(tile, v); + }); + } else { + this.bundleImageJson.thumbUrls.forEach((v, idx) => { + tile = {} as Tile; + if (quotient <= idx / 3) { + tile.cols = 3; + } else { + tile.cols = 2; + } + this.getTile(tile, v); + }); + } + } + + private getTile(tile: Tile, v: string) { + tile.imgSrc = this.baseURL + v; + tile.color = 'white'; + this.tiles.push(tile); + } + +모바일 개인 주소록을 PC 버전에도 공유 + PC->Mobile + 주소록 요청 + PC<-Mobile + 주소록 전송 + PC->Mobile + 주소록 완료 + +주소록 + UI 구성 + 검색 (이름, 전화번호) + 검색 결과 + 리스트 출력 + 모바일 주소록 동기화(PC-> Mobile 요청) + 모바일 주소록 초기화(PC에서 동기화된 주소록 초기화) + 엑샐 탬플릿 (주소록을 입력할 수 있는 액샐 템플릿을 제공) + 액샐 업로드 (주소록이 입력된 액샐 템플릿을 업로드 하여 PC 주소록 업데이트) + 액샐 데이터 초기화 (액샐 업로드 데이터 초기화) + 액셀 내려받기 (주소록 데이터 액셀로 다운로드) + 기능 목록 + 모바일 주소록 동기화 + 기존방식 (데이터가 많은 경우 중간 서버에서 끊길 가능성) + 1. PC -> Server + 모바일 주소록 동기화 시작 + SSVC_TYPE_SYNC_PHONEBOOK_READY_REQ + 2. Server -> Mobile + 모바일 주소록 동기화 노티 + 3. Mobile -> Server + 모바일 준비 완료 요청 및 데이터 전송 (추측) + 4. Server -> PC + 4. Server -> PC + SSVC_TYPE_SYNC_PHONEBOOK_READY_NOTI + 5. PC -> Server + PC 준비 완료 요청 + SSVC_TYPE_SYNC_PHONEBOOK_READY_OK_REQ + 6.Server -> PC + SSVC_TYPE_SYNC_PHONEBOOK_READY_OK_RES //사용없음 + SSVC_TYPE_SYNC_PHONEBOOK_READY_OK_NOTI + PC가 모바일로부터 준비 확인 NOTI를 받는 처리를 수행 + 7.Server -> PC + SSVC_TYPE_SYNC_PHONEBOOK_SND_RES //사용없음 + SSVC_TYPE_SYNC_PHONEBOOK_SND_NOTI //실데이터받는 프로토콜 + 박차장님이 제안한 방식 + PC -> 서버 + 개인 주소록 동기화 요청 + 프로토콜 정의 협의 + SSVC_TYPE_SYNC_PHONEBOOK_READY_REQ + (사용자번호) + 서버 -> 모바일 + 개인 주소록 동기화 요청에 대한 푸시 + 모바일 -> 서버 (REST API) + 개인 주소록 POST 전송 + 데이터 JSON 형태 + 서버 -> 모바일 (REST API) + 개인 주소록 전송에 대한 응답 + (응답코드, 싱크번호, 시간, 사용자번호) + 모바일 -> 서버 + 개인 주소록 전송 동기화 프로토콜 요청 + (싱크번호, 시간, 사용자번호) + 서버 -> 모바일 + 개인 주소록 전송 동기화 프로토콜 요청에 대한 응답 + 서버 -> PC + 모바일 개인 주소록 전송 완료 노티 + SSVC_TYPE_SYNC_PHONEBOOK_SND_NOTI + (싱크번호, 사용자번호) + PC -> 서버 (REST API) + 개인 주소록 요청 + (싱크번호, 사용자번호) + 서버 -> PC + 개인 주소록 JSON + (싱크번호, 시간, 사용자 번호) + + 모바일 주소록 초기화 + PC에 저장된 모바일 주소록을 삭제 + 엑샐 + Export/Import + 템플릿 + 초기화 + +대화방 + +TODO 완료 + 대화 묶음파일 그리드 컴포넌트 리팩토링 + + 파일 업로드 큐 컴포넌트 수정 + 묶음파일 처리 추가 + + 묶음파일 업로드 함수 리팩토링 + 묶음파일 모델링 수정 + 메세지 전송 컴포넌트 수정 + 묶음파일 전송 UI 추가 + 묶음파일 전송 로직 추가 + 이벤트 타입 추가 + BundleImage = 'b'; + 파일 타입 추가 + Bundle = 'b'; + 앨범함 컴포넌트 수정 + 묶음파일 타입 처리 추가 diff --git a/documents/업무/2월/2째주/0214.txt b/documents/업무/2월/2째주/0214.txt new file mode 100644 index 0000000..9196152 --- /dev/null +++ b/documents/업무/2월/2째주/0214.txt @@ -0,0 +1,37 @@ +내일 TODO + Call 컴포넌트 작성 + 다이얼러 컴포넌트 작성 + 주소록 컴포넌트 작성 + 최근통화내역 컴포넌트 작성 +요구사항 + 지역번호-3자리-4자리 /^(0(2|3[1-3]|4[1-4]|5[1-5]|6[1-4]))-(\d{3,4})-(\d{4})$/ + 010-4자리-4자리 || 011,017,016-3자리-4자리 /^(?:(010-\d{4})|(01[1|6|7|8|9]-\d{3,4}))-(\d{4})$/ + 기타번호4자리-4자리 + // 추후에 +국가코드3자리- + 입력번호, 출력번호 + 숫자 버튼, + 통화 버튼, + 삭제 버튼 + + 4행 * 3열 그리드 출력 + + 번호 함수 + 입력 이벤트 마다 지역번호, 핸드폰 번호 추출 + 하이픈 넣기 + 입력번호를 출력번호로 변환 + + 삭제 함수 + 이벤트 발생 마다 + 입력번호 삭제 + 번호함수 호출 + + 통화 함수 + 클릭투콜 API 호출 + +주간보고서 작성 + 묶음파일 업로드 추가 + 그리드 기능 추가 + 앨범함 기능 추가 + 묶음파일 전송 기능 추가 + 파일 업로드 기능 수정 + \ No newline at end of file diff --git a/documents/업무/2월/2째주/LF_개인주소록_프로토콜.txt b/documents/업무/2월/2째주/LF_개인주소록_프로토콜.txt new file mode 100644 index 0000000..b0477d6 --- /dev/null +++ b/documents/업무/2월/2째주/LF_개인주소록_프로토콜.txt @@ -0,0 +1,34 @@ +SSVC_TYPE_SYNC_PHONEBOOK_READY_REQ = 21, || 모바일 주소록 씽크 준비 요청 + PC -> Server + 모바일 온라인 확인 단계 필요 + +SSVC_TYPE_SYNC_PHONEBOOK_READY_RES = 22, || 모바일 주소록 씽크 준비 요청완료 + 서버 -> PC + SSVC_TYPE_SYNC_PHONEBOOK_SND_NOTI 프로토콜을 30초동안 기다림 + +SSVC_TYPE_SYNC_PHONEBOOK_READY_NOTI = 23, || 모바일 주소록 씽크 준비 알림 + +SSVC_TYPE_SYNC_PHONEBOOK_READY_OK_REQ = 24, || 모바일 주소록 씽크 준비 확인 요청 + 3 + +SSVC_TYPE_SYNC_PHONEBOOK_READY_OK_RES = 25, || 모바일 주소록 씽크 준비 확인 요청완료 + +SSVC_TYPE_SYNC_PHONEBOOK_READY_OK_NOTI = 26, || 모바일 주소록 씽크 준비 확인 알림 + +SSVC_TYPE_SYNC_PHONEBOOK_SND_REQ = 31, || 모바일 주소록 씽크 요청 +SSVC_TYPE_SYNC_PHONEBOOK_SND_RES = 32, || 모바일 주소록 씽크 완료 + 데이터는 M->PC 단방향 해당 RES는 PC 에서는 아무의미가 없다. + +SSVC_TYPE_SYNC_PHONEBOOK_SND_NOTI = 33, || 모바일 주소록 씽크 알림 + 해당 노티는 PC가 받는 노티(실데이터가 있음) + +//** 모바일에서 사용하는 프로토콜 **// +SSVC_TYPE_SYNC_PHONEBOOK_RCV_REQ = 34, || 모바일 주소록 씽크 확인 요청 + +SSVC_TYPE_SYNC_PHONEBOOK_RCV_RES = 35, || 모바일 주소록 씽크 확인 요청완료 + PC 사용 X +SSVC_TYPE_SYNC_PHONEBOOK_RCV_NOTI = 36, || 모바일 주소록 씽크 확인 알림 + PC 사용 X + 모바일에서 사용한는 프로토콜 +//** 모바일에서 사용하는 프로토콜 **// + diff --git a/documents/업무/2월/2째주/묵음파일백업/album-box.component.html b/documents/업무/2월/2째주/묵음파일백업/album-box.component.html new file mode 100644 index 0000000..9110e07 --- /dev/null +++ b/documents/업무/2월/2째주/묵음파일백업/album-box.component.html @@ -0,0 +1,182 @@ +
+
+ + + + +
+
+ +
+ + + + + + {{ 'common.file.selectFiles' | translate }} +
+ +
+ + + + + + + + + + + {{ 'common.file.selectFiles' | translate }} +
+
+ +
+ + +
+ {{ 'common.file.errors.cantPlay' | translate }} +
+
+
    +
  • {{ selectedFile.info.name }}
  • +
  • + size : + {{ selectedFile.info.size | ucapBytes }} +
  • +
  • + date : + {{ selectedFile.info.sendDate | ucapDate: 'YYYY.MM.DD' }} +
  • +
+
+
+
+ +
+
+
+
+ +
+
+ + + + +
+
+
+
+
+
+ + + + + + + +
+
+
+
+
+
+ + +
+
diff --git a/documents/업무/2월/2째주/묵음파일백업/album-box.component.ts b/documents/업무/2월/2째주/묵음파일백업/album-box.component.ts new file mode 100644 index 0000000..2a579ef --- /dev/null +++ b/documents/업무/2월/2째주/묵음파일백업/album-box.component.ts @@ -0,0 +1,264 @@ +import { + Component, + OnInit, + OnDestroy, + Inject, + ElementRef, + ViewChild +} from '@angular/core'; +import { + FileInfo, + FileDownloadInfo, + FileType +} from '@ucap-webmessenger/protocol-file'; +import { Subscription, combineLatest } from 'rxjs'; +import { Store, select } from '@ngrx/store'; + +import * as AppStore from '@app/store'; +import { tap } from 'rxjs/operators'; +import { FileUtil } from '@ucap-webmessenger/core'; +import { CommonApiService } from '@ucap-webmessenger/api-common'; +import { LoginResponse } from '@ucap-webmessenger/protocol-authentication'; +import { SessionStorageService } from '@ucap-webmessenger/web-storage'; +import { KEY_LOGIN_RES_INFO } from '@app/types/login-res-info.type'; +import { + EnvironmentsInfo, + KEY_ENVIRONMENTS_INFO, + KEY_VER_INFO +} from '@app/types'; +import { VersionInfo2Response } from '@ucap-webmessenger/api-public'; +import { UCAP_NATIVE_SERVICE, NativeService } from '@ucap-webmessenger/native'; +import { NGXLogger } from 'ngx-logger'; +import { FileDownloadItem } from '@ucap-webmessenger/api'; +import { ModuleConfig } from '@ucap-webmessenger/api-common'; +import { _MODULE_CONFIG } from 'projects/ucap-webmessenger-api-common/src/lib/config/token'; +import { AppFileService } from '@app/services/file.service'; + +export interface FileInfoTotal { + info: FileInfo; + checkInfo: FileDownloadInfo[]; + fileDownloadItem: FileDownloadItem; +} + +@Component({ + selector: 'app-layout-chat-right-drawer-album-box', + templateUrl: './album-box.component.html', + styleUrls: ['./album-box.component.scss'] +}) +export class AlbumBoxComponent implements OnInit, OnDestroy { + @ViewChild('videoPlayer', { static: false }) + videoPlayer: ElementRef; + + filteredList: FileInfoTotal[] = []; + fileInfoTotal: FileInfoTotal[]; + fileInfoList: FileInfo[]; + fileInfoListSubscription: Subscription; + + selectedFile: FileInfoTotal; + selectedFileList: FileInfoTotal[] = []; + + loginRes: LoginResponse; + environmentsInfo: EnvironmentsInfo; + sessionVerinfo: VersionInfo2Response; + + FileType = FileType; + currentTabIndex = 0; + + thumbBaseUrl: string; + + playable = true; + + constructor( + private store: Store, + private sessionStorageService: SessionStorageService, + private commonApiService: CommonApiService, + @Inject(UCAP_NATIVE_SERVICE) private nativeService: NativeService, + private appFileService: AppFileService, + @Inject(_MODULE_CONFIG) private moduleConfig: ModuleConfig, + private logger: NGXLogger + ) { + this.loginRes = this.sessionStorageService.get( + KEY_LOGIN_RES_INFO + ); + this.environmentsInfo = this.sessionStorageService.get( + KEY_ENVIRONMENTS_INFO + ); + this.sessionVerinfo = this.sessionStorageService.get( + KEY_VER_INFO + ); + + this.thumbBaseUrl = `${this.moduleConfig.hostConfig.protocol}://${ + this.moduleConfig.hostConfig.domain + }:${String(this.moduleConfig.hostConfig.port)}`; + } + + ngOnInit() { + this.fileInfoListSubscription = combineLatest([ + this.store.pipe(select(AppStore.MessengerSelector.RoomSelector.roomInfo)), + this.store.pipe( + select(AppStore.MessengerSelector.EventSelector.selectAllFileInfoList) + ), + this.store.pipe( + select( + AppStore.MessengerSelector.EventSelector.selectAllFileInfoCheckList + ) + ) + ]) + .pipe( + tap(() => (this.fileInfoTotal = [])), + tap(([roomInfo, fileInfoList, fileInfoCheckList]) => { + this.fileInfoList = fileInfoList.filter(fileInfo => { + if ( + !!roomInfo && + fileInfo.roomSeq === roomInfo.roomSeq && + (fileInfo.type === FileType.Image || + fileInfo.type === FileType.Video) + ) { + return true; + } else { + return false; + } + }); + + this.fileInfoList.map(fileInfo => { + this.fileInfoTotal.push({ + info: fileInfo, + checkInfo: fileInfoCheckList.filter( + checkInfo => checkInfo.seq === fileInfo.seq + ), + fileDownloadItem: new FileDownloadItem() + }); + }); + + this.onSelectedIndexChange(this.currentTabIndex); + }) + ) + .subscribe(); + } + + ngOnDestroy(): void { + if (!!this.fileInfoListSubscription) { + this.fileInfoListSubscription.unsubscribe(); + } + } + + getExtention(name: string): string { + return FileUtil.getExtension(name); + } + + getImageUrl(fileInfo: FileInfoTotal): string { + return this.commonApiService.urlForFileTalkDownload( + { + userSeq: this.loginRes.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginRes.tokenString, + attachmentsSeq: fileInfo.info.seq + }, + this.sessionVerinfo.downloadUrl + ); + } + + onErrorThumbnail(el: HTMLElement, fileInfo: FileInfoTotal): void { + const iconEl = document.createElement('div'); + iconEl.setAttribute( + 'class', + 'mime-icon light ico-' + this.getExtention(fileInfo.info.name) + ); + iconEl.innerHTML = `
`; + el.replaceWith(iconEl); + } + + onSelectedIndexChange(index: number) { + this.selectedFile = null; + this.currentTabIndex = index; + if (this.currentTabIndex === 0) { + // Image + this.filteredList = this.fileInfoTotal.filter( + fileInfo => fileInfo.info.type === FileType.Image + ); + } else { + // Video + this.filteredList = this.fileInfoTotal.filter( + fileInfo => fileInfo.info.type === FileType.Video + ); + } + } + + onClickImage(event: MouseEvent, fileInfo: FileInfoTotal) { + if (!!event) { + event.preventDefault(); + event.stopPropagation(); + } + + this.playable = true; + this.selectedFile = fileInfo; + } + + getCheckItem(fileInfo: FileInfoTotal) { + if (this.selectedFileList) { + if ( + this.selectedFileList.filter( + info => info.info.seq === fileInfo.info.seq + ).length > 0 + ) { + return true; + } else { + return false; + } + } else { + return false; + } + } + onCheckItem(value: boolean, fileInfo: FileInfoTotal) { + if (value) { + this.onClickImage(undefined, fileInfo); + this.selectedFileList.push(fileInfo); + } else { + this.selectedFileList = this.selectedFileList.filter( + info => info.info.seq !== fileInfo.info.seq + ); + } + } + + onClickDownload(fileInfo: FileInfoTotal) { + this.appFileService.fileTalkDownlod({ + req: { + userSeq: this.loginRes.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginRes.tokenString, + attachmentsSeq: fileInfo.info.seq, + fileDownloadItem: fileInfo.fileDownloadItem + }, + fileName: fileInfo.info.name + }); + } + + onClickDownloadAll(): void { + this.selectedFileList.forEach(fileInfo => { + this.onClickDownload(fileInfo); + }); + } + + onClickOpenDownloadFolder(): void { + this.nativeService + .openDefaultDownloadFolder() + .then(result => { + if (!!result) { + } else { + throw new Error('response Error'); + } + }) + .catch(reason => { + this.logger.error(reason); + }); + } + + onLoadedDataVideo(): void { + if ( + 0 === this.videoPlayer.nativeElement.videoWidth || + 0 === this.videoPlayer.nativeElement.videoHeight + ) { + this.playable = false; + } + } +} diff --git a/documents/업무/2월/2째주/묵음파일백업/bundle-image.event-json.ts b/documents/업무/2월/2째주/묵음파일백업/bundle-image.event-json.ts new file mode 100644 index 0000000..766272a --- /dev/null +++ b/documents/업무/2월/2째주/묵음파일백업/bundle-image.event-json.ts @@ -0,0 +1,36 @@ +import { StatusCode, JsonAnalization } from '@ucap-webmessenger/api'; +import { FileType } from '@ucap-webmessenger/protocol-file'; +import { EventJsonDecoder } from './event-json'; + +export interface BundleImageEventJson { + statusCode?: StatusCode; + errorMessage?: string; + roomSeq?: number; + attachmentSeq?: number; + fileCount?: number; + baseUrl?: string; + thumbUrls?: string[]; + fileType?: FileType; +} + +export const decodeBundleImageEventJson: EventJsonDecoder = ( + message: string +) => { + try { + const json = JsonAnalization.receiveAnalization(message); + + return { + statusCode: json.StatusCode, + errorMessage: json.ErrorMessage, + roomSeq: json.RoomID, + attachmentSeq: json.AttSEQ, + fileCount: json.FileCount, + thumbUrls: json.ThumbURL + } as BundleImageEventJson; + } catch (e) { + return { + statusCode: StatusCode.Fail, + errorMessage: e.toString() + } as BundleImageEventJson; + } +}; diff --git a/documents/업무/2월/2째주/묵음파일백업/common-api.service.ts b/documents/업무/2월/2째주/묵음파일백업/common-api.service.ts new file mode 100644 index 0000000..b955052 --- /dev/null +++ b/documents/업무/2월/2째주/묵음파일백업/common-api.service.ts @@ -0,0 +1,382 @@ +import { Injectable, Inject } from '@angular/core'; +import { + HttpClient, + HttpEventType, + HttpResponse, + HttpRequest +} from '@angular/common/http'; + +import { Observable, Subject } from 'rxjs'; +import { map, filter } from 'rxjs/operators'; + +import { + FileProfileSaveRequest, + FileProfileSaveResponse, + encodeFileProfileSave, + decodeFileProfileSave +} from '../apis/file-profile-save'; +import { + FileTalkDownloadRequest, + encodeFileTalkDownload, + encodeFormDataFileTalkDownload +} from '../apis/file-talk-download'; +import { + FileTalkSaveRequest, + FileTalkSaveResponse, + encodeFileTalkSave, + decodeFileTalkSave +} from '../apis/file-talk-save'; +import { + FileTalkSaveMultiRequest, + FileTalkSaveMultiResponse, + encodeFileTalkSaveMulti, + decodeFileTalkSaveMulti +} from '../apis/file-talk-save-multi'; +import { + FileTalkShareRequest, + FileTalkShareResponse, + encodeFileTalkShare, + decodeFileTalkShare +} from '../apis/file-talk-share'; +import { + MassTalkDownloadRequest, + MassTalkDownloadResponse, + encodeMassTalkDownload, + decodeMassTalkDownload +} from '../apis/mass-talk-download'; +import { + MassTalkSaveRequest, + MassTalkSaveResponse, + encodeMassTalkSave, + decodeMassTalkSave +} from '../apis/mass-talk-save'; +import { + TransMassTalkDownloadRequest, + TransMassTalkDownloadResponse, + encodeTransMassTalkDownload, + decodeTransMassTalkDownload +} from '../apis/trans-mass-talk-download'; +import { + TransMassTalkSaveRequest, + TransMassTalkSaveResponse, + encodeTransMassTalkSave, + decodeTransMassTalkSave +} from '../apis/trans-mass-talk-save'; +import { + TranslationReqRequest, + TranslationReqResponse, + encodeTranslationReq, + decodeTranslationReq +} from '../apis/translation-req'; +import { + TranslationSaveRequest, + TranslationSaveResponse, + encodeTranslationSave, + decodeTranslationSave +} from '../apis/translation-save'; + +import { _MODULE_CONFIG } from '../config/token'; +import { ModuleConfig } from '../config/module-config'; +import { Urls } from '../config/urls'; +import { UrlConfig } from '@ucap-webmessenger/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CommonApiService { + readonly urls: Urls; + + constructor( + @Inject(_MODULE_CONFIG) private moduleConfig: ModuleConfig, + private httpClient: HttpClient + ) { + this.urls = UrlConfig.getUrls( + this.moduleConfig.hostConfig, + this.moduleConfig.urls + ); + } + + public fileProfileSave( + req: FileProfileSaveRequest, + fileProfileSaveUrl?: string + ): Observable { + 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)); + } + return false; + }), + map((event: HttpResponse) => { + req.fileUploadItem.uploadComplete(); + return decodeFileProfileSave(event.body); + }) + ); + } + + public urlForFileTalkDownload( + req: FileTalkDownloadRequest, + fileTalkDownloadUrl?: string + ): string { + const httpReq = new HttpRequest( + 'GET', + !!fileTalkDownloadUrl ? fileTalkDownloadUrl : this.urls.fileTalkDownload, + {}, + { + params: encodeFileTalkDownload(req) + } + ); + + return httpReq.urlWithParams; + } + + public fileTalkDownload( + req: FileTalkDownloadRequest, + fileTalkDownloadUrl?: string + ): Observable { + const httpReq = new HttpRequest( + 'POST', + !!fileTalkDownloadUrl ? fileTalkDownloadUrl : this.urls.fileTalkDownload, + encodeFormDataFileTalkDownload(req), + { reportProgress: true, responseType: 'blob' } + ); + + let progress: Subject; + if (!!req.fileDownloadItem) { + progress = req.fileDownloadItem.downloadStart(); + } + + return this.httpClient.request(httpReq).pipe( + filter(event => { + if (event instanceof HttpResponse) { + return true; + } else if (HttpEventType.DownloadProgress === event.type) { + if (!!progress) { + progress.next(Math.round((100 * event.loaded) / event.total)); + } + } + return false; + }), + map((event: HttpResponse) => { + if (!!progress) { + req.fileDownloadItem.downloadComplete(); + } + return event.body; + }) + ); + } + + public fileTalkSaveMulti( + req: FileTalkSaveMultiRequest, + fileTalkSaveMultiUrl?: string + ): Observable { + const httpReq = new HttpRequest( + 'POST', + !!fileTalkSaveMultiUrl + ? fileTalkSaveMultiUrl + : this.urls.fileTalkSaveMulti, + encodeFileTalkSaveMulti(req), + { reportProgress: true, responseType: 'text' as 'json' } + ); + + const progressList: Subject[] = []; + for (const p of req.fileUploadItems) { + progressList.push(p.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)); + for (const progress of progressList) { + progress.next(Math.round((100 * event.loaded) / event.total)); + } + } + return false; + }), + map((event: HttpResponse) => { + for (const p of req.fileUploadItems) { + p.uploadComplete(); + } + return decodeFileTalkSaveMulti(event.body); + }) + ); + } + + public fileTalkSave( + req: FileTalkSaveRequest, + fileTalkSaveUrl?: string + ): Observable { + const httpReq = new HttpRequest( + 'POST', + !!fileTalkSaveUrl ? fileTalkSaveUrl : this.urls.fileTalkSave, + encodeFileTalkSave(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)); + } + return false; + }), + map((event: HttpResponse) => { + req.fileUploadItem.uploadComplete(); + return decodeFileTalkSave(event.body); + }) + ); + } + + public acceptableExtensionForFileTalk( + extensions: string[] + ): { accept: boolean; reject: string[] } { + let accept = true; + const reject: string[] = []; + for (const extension of extensions) { + if ( + -1 === + this.moduleConfig.acceptableFileExtensions.indexOf( + extension.toLowerCase() + ) + ) { + reject.push(extension); + accept = false; + } + } + return { + accept, + reject + }; + } + + public fileTalkShare( + req: FileTalkShareRequest + ): Observable { + return this.httpClient + .post( + this.urls.fileTalkShare, + {}, + { + params: encodeFileTalkShare(req) + } + ) + .pipe(map(res => decodeFileTalkShare(res))); + } + + public massTalkDownload( + req: MassTalkDownloadRequest + ): Observable { + return this.httpClient + .post( + this.urls.massTalkDownload, + {}, + { + params: encodeMassTalkDownload(req), + responseType: 'text' as 'json' + } + ) + .pipe(map(res => decodeMassTalkDownload(res))); + } + + public massTalkSave( + req: MassTalkSaveRequest + ): Observable { + const httpReq = new HttpRequest( + 'POST', + this.urls.massTalkSave, + encodeMassTalkSave(req), + { reportProgress: true, responseType: 'text' as 'json' } + ); + + return this.httpClient.request(httpReq).pipe( + filter(event => { + if (event instanceof HttpResponse) { + return true; + } + return false; + }), + map(res => decodeMassTalkSave((res as HttpResponse).body)) + ); + } + + public transMassTalkDownload( + req: TransMassTalkDownloadRequest + ): Observable { + return this.httpClient + .post( + this.urls.transMassTalkDownload, + {}, + { + params: encodeTransMassTalkDownload(req) + } + ) + .pipe(map(res => decodeTransMassTalkDownload(res))); + } + + public transMassTalkSave( + req: TransMassTalkSaveRequest + ): Observable { + return this.httpClient + .post( + this.urls.transMassTalkSave, + {}, + { + params: encodeTransMassTalkSave(req) + } + ) + .pipe(map(res => decodeTransMassTalkSave(res))); + } + + public translationReq( + req: TranslationReqRequest + ): Observable { + return this.httpClient + .post( + this.urls.translationReq, + {}, + { + params: encodeTranslationReq(req) + } + ) + .pipe(map(res => decodeTranslationReq(res))); + } + + public translationSave( + req: TranslationSaveRequest + ): Observable { + const httpReq = new HttpRequest( + 'POST', + this.urls.translationSave, + encodeTranslationSave(req), + { reportProgress: true } + ); + + return this.httpClient.request(httpReq).pipe( + filter(event => { + if (event instanceof HttpResponse) { + return true; + } + return false; + }), + map(res => decodeTranslationSave((res as HttpResponse).body)) + ); + } +} diff --git a/documents/업무/2월/2째주/묵음파일백업/file-talk-save-multi.ts b/documents/업무/2월/2째주/묵음파일백업/file-talk-save-multi.ts new file mode 100644 index 0000000..17a7c71 --- /dev/null +++ b/documents/업무/2월/2째주/묵음파일백업/file-talk-save-multi.ts @@ -0,0 +1,79 @@ +import { DeviceType } from '@ucap-webmessenger/core'; +import { + APIRequest, + APIResponse, + APIEncoder, + APIDecoder, + ParameterUtil, + StatusCode, + JsonAnalization, + APIFormDataEncoder +} from '@ucap-webmessenger/api'; +import { FileUploadItem } from '../../../../ucap-webmessenger-api/src/lib/models/file-upload-item'; + +export interface FileTalkSaveMultiRequest extends APIRequest { + userSeq: number; + deviceType: DeviceType; + token: string; + files: File[]; + fileUploadItems: FileUploadItem[]; + roomSeq?: string; + type?: string; +} + +export interface FileTalkSaveMultiResponse extends APIResponse { + roomSeq?: string; + attachmentSeq?: string; + fileCount?: string; + baseUrl?: string; + fileType?: string; + thumbnailUrls?: string[]; + returnJson?: string; +} + +const fileTalkSaveEncodeMapTemp = { + userSeq: 'p_user_seq', + deviceType: 'p_device_type', + token: 'p_token', + roomSeq: 'p_room_id', + files: 'file[]', + type: 'p_type' +}; + +export const encodeFileTalkSaveMulti: APIFormDataEncoder = ( + req: FileTalkSaveMultiRequest +) => { + const extraParams: any = {}; + extraParams.userSeq = String(req.userSeq); + + return ParameterUtil.encodeFormData( + fileTalkSaveEncodeMapTemp, + req, + extraParams + ); +}; + +export const decodeFileTalkSaveMulti: APIDecoder = ( + res: any +) => { + try { + const json = JsonAnalization.receiveAnalization(res); + const fileTypeDefault = + !json.fileType || json.fileType === '' ? 'b' : json.fileType; + return { + statusCode: json.StatusCode, + roomSeq: json.RoomID, + attachmentSeq: json.AttSEQ, + fileCount: json.FileCount, + fileType: fileTypeDefault, + baseUrl: json.BaseURL, + thumbnailUrls: json.ThumbURL, + returnJson: res + } as FileTalkSaveMultiResponse; + } catch (e) { + return { + statusCode: StatusCode.Fail, + errorMessage: e + } as FileTalkSaveMultiResponse; + } +}; diff --git a/documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.html b/documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.html new file mode 100644 index 0000000..4895086 --- /dev/null +++ b/documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.html @@ -0,0 +1,44 @@ +
+
+
+ + + + + + +
{{ fileUploadItem.file.name }}
+ +
+ +
+ + +
+
+ +
+
+ {{ 'common.file.dropZoneForUpload' | translate }} +
+
+
+
diff --git a/documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.scss b/documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.scss new file mode 100644 index 0000000..8d0975c --- /dev/null +++ b/documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.scss @@ -0,0 +1,55 @@ +@mixin ellipsis($row) { + overflow: hidden; + text-overflow: ellipsis; + @if $row == 1 { + display: block; + white-space: nowrap; + word-wrap: normal; + } @else if $row >= 2 { + display: -webkit-box; + -webkit-line-clamp: $row; + -webkit-box-orient: vertical; + word-wrap: break-word; + } +} + +.ucap-file-upload-queue-container { + width: 100%; + height: 100%; + .file-upload-item { + min-width: 200px; + margin: 0 1%; + margin-bottom: 10px; + width: 100%; + border-radius: 3px; + background-color: #f9f9f9; + border: 1px solid #dddddd; + .file-upload-info { + padding: 10px; + svg { + margin-right: 6px; + } + .file-upload-name { + height: 20px; + @include ellipsis(2); + } + } + .file-upload-progress { + padding: 6px 10px; + } + } + .uploadItems { + width: 100%; + font-size: 0.9em; + .msg-guide { + display: flex; + flex: row; + color: #ffffff; + justify-content: center; + align-items: center; + .icon-img { + margin-right: 6px; + } + } + } +} diff --git a/documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.ts b/documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.ts new file mode 100644 index 0000000..e62da98 --- /dev/null +++ b/documents/업무/2월/2째주/묵음파일백업/file-upload-queue.component.ts @@ -0,0 +1,117 @@ +import { + Component, + OnInit, + Input, + ElementRef, + AfterViewInit +} from '@angular/core'; + +import { NGXLogger } from 'ngx-logger'; +import { FileUploadItem } from '@ucap-webmessenger/api'; + +@Component({ + selector: 'ucap-file-upload-queue', + templateUrl: './file-upload-queue.component.html', + styleUrls: ['./file-upload-queue.component.scss'] +}) +export class FileUploadQueueComponent implements OnInit, AfterViewInit { + @Input() + dropZoneIncludeParent = false; + + fileUploadItems: FileUploadItem[]; + uploadItems: DataTransferItem[]; + + constructor( + private elementRef: ElementRef, + private logger: NGXLogger + ) {} + + ngOnInit() {} + + ngAfterViewInit(): void { + this.changeStyleDisplay(false); + } + + onDragEnter(items: DataTransferItemList): void { + if (!items || 0 === items.length) { + return; + } + + const uploadItems: DataTransferItem[] = []; + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < items.length; i++) { + uploadItems.push(items[i]); + } + this.uploadItems = [...uploadItems]; + this.changeStyleDisplay(true); + } + + onDragLeave(): void { + this.changeStyleDisplay(false); + } + + onDrop(fileUploadItems: FileUploadItem[]) { + if (!fileUploadItems || 0 === fileUploadItems.length) { + return; + } + this.fileUploadItems = fileUploadItems; + this.uploadItems = undefined; + } + + onFileSelected(fileUploadItems: FileUploadItem[]): void { + if (!fileUploadItems || 0 === fileUploadItems.length) { + return; + } + this.fileUploadItems = fileUploadItems; + this.uploadItems = undefined; + this.changeStyleDisplay(true); + } + + onUploadComplete(): void { + setTimeout(() => { + this.fileUploadItems = undefined; + this.changeStyleDisplay(false); + }, 1000); + } + + isEventInElement(event: DragEvent): boolean { + const rect = this.elementRef.nativeElement.getBoundingClientRect(); + // const rect: DOMRect = this.elementRef.nativeElement.getBoundingClientRect(); + + if ( + event.pageX >= rect.left && + event.pageX <= rect.left + rect.width && + event.pageY >= rect.top && + event.pageY <= rect.top + rect.height + ) { + return true; + } + return false; + } + + private changeStyleDisplay(show: boolean): void { + if (show || (!!this.fileUploadItems && 0 < this.fileUploadItems.length)) { + if (this.dropZoneIncludeParent) { + this.elementRef.nativeElement.parentElement.style.display = ''; + } else { + this.elementRef.nativeElement.style.display = ''; + } + } else { + if (this.dropZoneIncludeParent) { + this.elementRef.nativeElement.parentElement.style.display = 'none'; + } else { + this.elementRef.nativeElement.style.display = 'none'; + } + } + } + + // onClickClear(fileUploadItem: FileUploadItem) { + // this.fileUploadItems = this.fileUploadItems.filter(f => { + // return ( + // f.file.name !== fileUploadItem.file.name && + // f.file.path !== fileUploadItem.file.path + // ); + // }); + // this.filesChange.emit(this.fileUploadItems); + // } +} diff --git a/documents/업무/2월/2째주/묵음파일백업/messages.component.html b/documents/업무/2월/2째주/묵음파일백업/messages.component.html new file mode 100644 index 0000000..06b3d64 --- /dev/null +++ b/documents/업무/2월/2째주/묵음파일백업/messages.component.html @@ -0,0 +1,384 @@ + +
+ + +
+
+ + + + +
+
+
+ {{ getConvertTimer(roomInfoSubject.value.timeRoomInterval) }} + +
+

+ + {{ 'chat.getRoomNameInProgress' | translate }} + + + + + MyTalk + + + {{ _roomUserInfos | ucapTranslate: 'name':',' }} + + + + {{ roomInfoSubject.value.roomName }} + + + {{ getRoomNameByRoomUser(_roomUserInfos) }} + + + + +

+ + + +
+
+ + + +
+
+
+
+ +
+ +
+
+ + + +
+ + + + + + +
+ + +
+
+ + + +
+ +
+
+ + + + + + + +
+ + + + +
+ +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + diff --git a/documents/업무/2월/2째주/묵음파일백업/messages.component.scss b/documents/업무/2월/2째주/묵음파일백업/messages.component.scss new file mode 100644 index 0000000..1960830 --- /dev/null +++ b/documents/업무/2월/2째주/묵음파일백업/messages.component.scss @@ -0,0 +1,233 @@ +@charset 'utf-s'; +:host { + display: flex; + width: 100%; + height: 100%; +} +@mixin ellipsis($row) { + overflow: hidden; + text-overflow: ellipsis; + @if $row == 1 { + display: block; + white-space: nowrap; + word-wrap: normal; + } @else if $row >= 2 { + display: -webkit-box; + -webkit-line-clamp: $row; + -webkit-box-orient: vertical; + word-wrap: break-word; + } +} +.container { + position: relative; + width: 100%; +} +.chat-toolbar { + position: relative; + display: flex; + flex-flow: column; + width: 100%; + height: auto; + align-items: center; + background-color: #ffffff !important; + border-bottom: 1px solid #dddddd; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16); + z-index: 1; + padding: 0; + .chat-header { + width: 100%; + align-items: center; + display: flex; + justify-content: space-between; + justify-items: center; + padding: 4px 20px; + .profile-img { + margin-right: 10px; + width: 30px; + height: 30px; + .responsive-chats-button { + display: none; + line-height: normal; + cursor: unset; + &:last-child { + display: block; + padding: 0; + width: 30px; + height: 30px; + border-radius: 50%; + color: #efefef; + font-size: 1rem; + } + } + &.thumbnail-mask { + border-radius: 50%; + overflow: hidden; + img { + width: 50px; + height: auto; + } + } + } + .room-info { + display: flex; + flex-flow: row; + overflow: hidden; + align-items: center; + .room-name { + font-size: 0.94rem; + line-height: normal; + @include ellipsis(1); + } + .room-type { + font-size: 0.9rem; + line-height: normal; + height: 20px; + span { + border-radius: 10px; + padding: 2px 6px; + margin-right: 6px; + font-size: 0.7rem; + } + } + } + .room-option { + margin-left: auto; + margin-right: -10px; + .icon-button { + transform: translateY(-2px); + i { + font-size: 0.9em; + } + } + } + } + .chat-search-frame { + position: relative; + width: 100%; + .chat-search { + margin: 0 4px 4px; + } + } +} + +.chat-content { + position: relative; + background: transparent; + overflow: auto; + -webkit-overflow-scrolling: touch; + + .file-drop-zone-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: transparent; + + .file-drop-zone { + position: absolute; + padding: 10px; + background-color: rgb(54, 54, 54, 0.8); + bottom: 0; + width: 100%; + } + } + + .sticker-selector-container { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + background-color: transparent; + + .sticker-selector-zone { + position: absolute; + padding: 10px 10px 0 10px; + background-color: rgba(250, 255, 255, 0.8); + bottom: 0; + width: 100%; + } + } +} +.translation-container { + .translation-section { + display: flex; + flex-flow: column; + border-top: 1px solid #dddddd; + .translation-container { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + background-color: transparent; + background-color: rgba(250, 255, 255, 0.8); + border-top: 1px solid; + + .translation-zone { + position: absolute; + padding: 10px 10px 0 10px; + background-color: rgba(250, 255, 255, 0.8); + bottom: 0; + width: 100%; + } + } + } +} +//mat-snack-bar +::ng-deep .cdk-global-overlay-wrapper { + .mat-snack-bar-container { + margin: 0; + padding: 30px; + max-width: 60vw; + .mat-simple-snackbar { + display: flex; + justify-content: center; + span { + @include ellipsis(3); + display: inline-block; + padding: 7px 20px; + border: 1px solid #ffffff; + background-color: rgb(255, 255, 255, 0.2); + color: #ffffff; + margin-right: 4px; + flex: 1 1 auto; + max-width: 40vw; + } + &-action { + display: inline-flex; + margin-left: auto; + flex: 0 0 auto; + height: 100%; + button { + //background-color: #00b6d5; + border-radius: 2px; + span { + padding: 0 20px; + color: #ffffff; + background: none; + border: none; + font-weight: 500; + } + } + } + } + } +} + +::ng-deep .chat-header { + .profile-img { + .chat-timer { + .mat-button-wrapper { + display: flex; + justify-content: center; + justify-items: center; + .mat-icon { + line-height: normal; + color: #ffffff; + font-size: 20px; + transform: translateY(1px); + } + } + } + } +} diff --git a/documents/업무/2월/2째주/묵음파일백업/messages.component.ts b/documents/업무/2월/2째주/묵음파일백업/messages.component.ts new file mode 100644 index 0000000..fb0e95f --- /dev/null +++ b/documents/업무/2월/2째주/묵음파일백업/messages.component.ts @@ -0,0 +1,2086 @@ +import { + Component, + OnInit, + OnDestroy, + ViewChild, + AfterViewInit, + Output, + EventEmitter, + Inject, + ChangeDetectorRef, + ChangeDetectionStrategy +} from '@angular/core'; +import { + ucapAnimations, + SnackBarService, + ClipboardService, + DialogService, + ConfirmDialogComponent, + ConfirmDialogData, + ConfirmDialogResult, + AlertDialogComponent, + AlertDialogData, + AlertDialogResult, + FileUploadQueueComponent, + StringUtil +} from '@ucap-webmessenger/ui'; +import { Store, select } from '@ngrx/store'; +import { NGXLogger } from 'ngx-logger'; +import { Observable, Subscription, forkJoin, of, BehaviorSubject } from 'rxjs'; + +import { + Info, + EventType, + isRecalled, + isCopyable, + isRecallable, + isForwardable, + InfoResponse, + EventJson, + FileEventJson, + StickerEventJson, + MassTextEventJson, + TranslationEventJson, + MassTranslationEventJson +} from '@ucap-webmessenger/protocol-event'; + +import * as AppStore from '@app/store'; +import * as EventStore from '@app/store/messenger/event'; +import * as ChatStore from '@app/store/messenger/chat'; +import * as RoomStore from '@app/store/messenger/room'; +import * as SyncStore from '@app/store/messenger/sync'; +import { LoginResponse } from '@ucap-webmessenger/protocol-authentication'; +import { + SessionStorageService, + LocalStorageService +} from '@ucap-webmessenger/web-storage'; +import { + EnvironmentsInfo, + KEY_ENVIRONMENTS_INFO, + UserSelectDialogType, + RightDrawer, + KEY_STICKER_HISTORY +} from '@app/types'; +import { + RoomInfo, + UserInfo, + RoomType, + UserInfoShort +} from '@ucap-webmessenger/protocol-room'; +import { take, map, catchError } from 'rxjs/operators'; +import { + FormComponent as UCapUiChatFormComponent, + MessagesComponent as UCapUiChatMessagesComponent +} from '@ucap-webmessenger/ui-chat'; +import { KEY_VER_INFO } from '@app/types'; +import { VersionInfo2Response } from '@ucap-webmessenger/api-public'; +import { + MatMenuTrigger, + MatSnackBarRef, + SimpleSnackBar +} from '@angular/material'; +import { FileUploadItem, FileDownloadItem } from '@ucap-webmessenger/api'; +import { + CommonApiService, + FileTalkSaveRequest, + FileTalkSaveResponse, + FileTalkSaveMultiRequest +} from '@ucap-webmessenger/api-common'; +import { + CreateChatDialogComponent, + CreateChatDialogData, + CreateChatDialogResult +} from '../dialogs/chat/create-chat.dialog.component'; +import { + FileViewerDialogComponent, + FileViewerDialogData, + FileViewerDialogResult +} from '@app/layouts/common/dialogs/file-viewer.dialog.component'; +import { FileUtil, StickerFilesInfo } from '@ucap-webmessenger/core'; + +import { StatusCode } from '@ucap-webmessenger/api'; +import { + EditChatRoomDialogComponent, + EditChatRoomDialogResult, + EditChatRoomDialogData +} from '../dialogs/chat/edit-chat-room.dialog.component'; +import { + SelectGroupDialogComponent, + SelectGroupDialogResult, + SelectGroupDialogData +} from '../dialogs/group/select-group.dialog.component'; +import { GroupDetailData } from '@ucap-webmessenger/protocol-sync'; +import { environment } from '../../../../environments/environment'; +import { + MassDetailComponent, + MassDetailDialogData +} from '../dialogs/chat/mass-detail.component'; +import { NativeService, UCAP_NATIVE_SERVICE } from '@ucap-webmessenger/native'; +import { TranslateService } from '@ngx-translate/core'; +import { TranslatePipe } from 'projects/ucap-webmessenger-ui/src/lib/pipes/translate.pipe'; +import { TranslateService as UiTranslateService } from '@ucap-webmessenger/ui'; + +import { + ClipboardDialogComponent, + ClipboardDialogData, + ClipboardDialogResult +} from '../dialogs/chat/clipboard.dialog.component'; +import { AppFileService } from '@app/services/file.service'; + +@Component({ + selector: 'app-layout-messenger-messages', + templateUrl: './messages.component.html', + styleUrls: ['./messages.component.scss'], + animations: ucapAnimations, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit { + @Output() + openProfile = new EventEmitter<{ + userSeq: number; + }>(); + @Output() + closeRightDrawer = new EventEmitter(); + + @ViewChild('chatForm', { static: false }) + private chatForm: UCapUiChatFormComponent; + + @ViewChild('messageContextMenuTrigger', { static: true }) + messageContextMenuTrigger: MatMenuTrigger; + messageContextMenuPosition = { x: '0px', y: '0px' }; + + @ViewChild('fileUploadQueue', { static: true }) + fileUploadQueue: FileUploadQueueComponent; + + @ViewChild('chatMessages', { static: true }) + chatMessages: UCapUiChatMessagesComponent; + + environmentsInfo: EnvironmentsInfo; + + loginResSubscription: Subscription; + loginResSubject = new BehaviorSubject(undefined); + + roomInfoSubscription: Subscription; + roomInfoSubject = new BehaviorSubject(undefined); + + userInfoListSubscription: Subscription; + userInfoListSubject = new BehaviorSubject(undefined); + + eventListSubscription: Subscription; + eventListSubject = new BehaviorSubject[]>(undefined); + eventListNewSubject = new BehaviorSubject[]>(undefined); + + eventInfoStatusSubscription: Subscription; + eventInfoStatusSubject = new BehaviorSubject(undefined); + + eventRemainedSubscription: Subscription; + eventRemainedSubject = new BehaviorSubject(false); // 이전대화가 남아 있는지 여부 + + lockSubject = new BehaviorSubject(false); + + sessionVerInfo: VersionInfo2Response; + + baseEventSeq = 0; + eventListProcessing$: Observable; + searchEventListProcessing: boolean; + searchEventListProcessingSubscription: Subscription; + + isRecalledMessage = isRecalled; + isCopyableMessage = isCopyable; + isRecallableMessage = isRecallable; + isForwardableMessage = isForwardable; + + /** Timer 대화방의 대화 삭제를 위한 interval */ + interval: any; + + /** About Searching */ + isShowSearchArea = false; + moreSearchProcessing = false; + searchText = ''; + searchedListSubject = new BehaviorSubject[]>(undefined); + searchedFocusEvent: Info; + searchTotalCount = 0; + searchCurrentIndex = 0; + + /** About Sticker */ + isShowStickerSelector = false; + selectedSticker: StickerFilesInfo; + + /** 번역기능 비활성화 2020-02-07 + * About Translation + **/ + // isTranslationProcess = false; + // isShowTranslation = false; + // translationSimpleview = false; + // translationPreview = false; + // destLocale = 'en'; // default English :: en + // translationPreviewInfo: { + // previewInfo: TranslationSaveResponse | null; + // translationType: EventType.Translation | EventType.MassTranslation; + // }; + + /** About ReadHere */ + firstCheckReadHere = true; + clearReadHere = false; + initRoomLastEventSeq: number; + + snackBarPreviewEvent: MatSnackBarRef; + + RoomType = RoomType; + environment = environment; + + constructor( + private store: Store, + private sessionStorageService: SessionStorageService, + private localStorageService: LocalStorageService, + private commonApiService: CommonApiService, + private clipboardService: ClipboardService, + private uiTranslateService: UiTranslateService, + private translateService: TranslateService, + private changeDetectorRef: ChangeDetectorRef, + private dialogService: DialogService, + private snackBarService: SnackBarService, + @Inject(UCAP_NATIVE_SERVICE) private nativeService: NativeService, + private appFileService: AppFileService, + private logger: NGXLogger + ) { + this.sessionVerInfo = this.sessionStorageService.get( + KEY_VER_INFO + ); + + this.environmentsInfo = this.sessionStorageService.get( + KEY_ENVIRONMENTS_INFO + ); + } + + ngOnInit() { + this.loginResSubscription = this.store + .pipe(select(AppStore.AccountSelector.AuthenticationSelector.loginRes)) + .subscribe(loginRes => { + this.loginResSubject.next(loginRes); + }); + + this.roomInfoSubscription = this.store + .pipe(select(AppStore.MessengerSelector.RoomSelector.roomInfo)) + .subscribe(roomInfo => { + if ( + !this.roomInfoSubject.value || + (!!this.roomInfoSubject.value && + !!roomInfo && + this.roomInfoSubject.value.roomSeq !== roomInfo.roomSeq) + ) { + this.clearView(); + + if (!!this.roomInfoSubject.value && !!this.interval) { + clearInterval(this.interval); + this.interval = undefined; + } + + if ( + !!this.roomInfoSubject.value && + !!this.roomInfoSubject.value.isTimeRoom + ) { + this.interval = setInterval(() => { + this.store.dispatch(EventStore.infoIntervalClear({})); + }, 1000); + } + + this.readyToReply(); + } + + this.roomInfoSubject.next(roomInfo); + }); + + this.userInfoListSubscription = this.store + .pipe(select(AppStore.MessengerSelector.RoomSelector.selectUserinfolist)) + .subscribe(userInfoList => { + this.userInfoListSubject.next(userInfoList); + }); + + this.eventListProcessing$ = this.store.pipe( + select(AppStore.MessengerSelector.EventSelector.infoListProcessing) + ); + + this.searchEventListProcessingSubscription = this.store + .pipe( + select( + AppStore.MessengerSelector.EventSelector.infoSearchListProcessing + ) + ) + .subscribe(process => { + this.lockSubject.next(process); + this.searchEventListProcessing = process; + if (!process && this.isShowSearchArea) { + this.doSearchTextInEvent(this.searchText); + this.snackBarService.open( + this.translateService.instant('chat.searchEventByTextEnd'), + this.translateService.instant('common.messages.confirm'), + { + duration: 3000, + verticalPosition: 'top', + horizontalPosition: 'center' + } + ); + } + }); + + this.eventRemainedSubscription = this.store + .pipe(select(AppStore.MessengerSelector.EventSelector.remainInfo)) + .subscribe(remained => { + this.eventRemainedSubject.next(remained); + }); + + // [Daesang] + this.eventListSubscription = this.store + .pipe(select(AppStore.MessengerSelector.EventSelector.selectAllInfoList)) + .subscribe(infoList => { + if ( + !!this.eventListSubject.value && + this.eventListSubject.value.length > 0 + ) { + this.eventListNewSubject.next( + infoList.filter(info => { + if ( + info.seq <= + this.eventListSubject.value[ + this.eventListSubject.value.length - 1 + ].seq + ) { + return false; + } + return true; + }) + ); + + if ( + !!infoList && + infoList.length > 0 && + !!this.roomInfoSubject.value && + !!this.roomInfoSubject.value.lastReadEventSeq && + this.baseEventSeq <= this.roomInfoSubject.value.lastReadEventSeq + ) { + // 조회된 내용중에 read here 가 있을 경우. + this.firstCheckReadHere = false; + } + } + + this.eventListSubject.next(infoList); + + if (this.moreSearchProcessing) { + const baseseq = this.baseEventSeq; + // setTimeout(() => { + // this.doSearchTextInEvent(this.searchText, baseseq); + // }, 800); + this.baseEventSeq = infoList[0].seq; + } else { + if (!!infoList && infoList.length > 0) { + this.baseEventSeq = infoList[0].seq; + } + } + }); + + // // [GROUP] + // this.eventListSubscription = this.store + // .pipe( + // select(AppStore.MessengerSelector.EventSelector.selectAllInfoList), + // tap(infoList => { + // if (!!this.eventList && this.eventList.length > 0) { + // this.eventListNew = infoList.filter(info => { + // if (info.seq <= this.eventList[this.eventList.length - 1].seq) { + // return false; + // } + // return true; + // }); + // } + // this.eventList = infoList; + + // if (!!infoList && infoList.length > 0) { + // this.baseEventSeq = infoList[0].seq; + // this.readyToReply(); + // } + // }) + // ) + // .subscribe(); + + this.eventInfoStatusSubscription = this.store + .pipe(select(AppStore.MessengerSelector.EventSelector.infoStatus)) + .subscribe(res => { + this.eventInfoStatusSubject.next(res); + + if (!!res) { + const elist = this.eventListSubject.value; + if (res.baseSeq === 0 && elist.length > 0) { + this.initRoomLastEventSeq = elist[elist.length - 1].seq; + } + } + }); + } + + ngOnDestroy(): void { + if (!!this.loginResSubscription) { + this.loginResSubscription.unsubscribe(); + } + if (!!this.roomInfoSubscription) { + this.roomInfoSubscription.unsubscribe(); + } + if (!!this.userInfoListSubscription) { + this.userInfoListSubscription.unsubscribe(); + } + if (!!this.eventListSubscription) { + this.eventListSubscription.unsubscribe(); + } + if (!!this.eventInfoStatusSubscription) { + this.eventInfoStatusSubscription.unsubscribe(); + } + if (!!this.eventRemainedSubscription) { + this.eventRemainedSubscription.unsubscribe(); + } + if (!!this.searchEventListProcessingSubscription) { + this.searchEventListProcessingSubscription.unsubscribe(); + } + + if (!!this.interval) { + clearInterval(this.interval); + } + } + + ngAfterViewInit(): void { + // this.readyToReply(); + } + + /** + * 채팅방의 여러 팝업들을 닫아준다. + */ + clearView() { + // Right Drawer closed.. + this.closeRightDrawer.emit(); + + // Sticker Selector Clear.. + this.isShowStickerSelector = false; + this.selectedSticker = undefined; + + // Chat Search Clear.. + this.onCloseSearchArea(); + + // 번역기능 비활성화 2020-02-07 + // Translate Clear.. + // this.isTranslationProcess = false; + // this.isShowTranslation = false; + // this.translationSimpleview = false; + // this.translationPreview = false; + // this.destLocale = 'en'; // default English :: en + // this.translationPreviewInfo = null; + + // Read here Clear.. + this.firstCheckReadHere = true; + + // Chat Formfield Clear.. + if (!!this.chatForm) { + this.chatForm.replyInput.nativeElement.value = ''; + } + } + + get _roomUserInfos() { + if (this.roomInfoSubject.value.roomType === RoomType.Single) { + return this.userInfoListSubject.value.filter(roomUserInfo => { + return this.loginResSubject.value.userSeq !== roomUserInfo.seq; + }); + } else { + return this.userInfoListSubject.value + .filter(roomUserInfo => { + return ( + this.loginResSubject.value.userSeq !== roomUserInfo.seq && + roomUserInfo.isJoinRoom + ); + }) + .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + } + } + + getRoomNameByRoomUser(roomUserInfos: (UserInfo | UserInfoShort)[]) { + let roomName = new TranslatePipe( + this.uiTranslateService, + this.changeDetectorRef + ).transform(roomUserInfos, 'name', ','); + + if (!roomName || roomName.trim().length === 0) { + roomName = this.translateService.instant('chat.noRoomUser'); + } + return roomName; + } + + /** 대화전송 가능한 방인지 판단 */ + getEnableSend() { + if (!this.roomInfoSubject.value) { + return false; + } + + if ( + [ + RoomType.Bot, + RoomType.Allim, + RoomType.Link, + RoomType.Allim_Elephant, + RoomType.Allim_TMS + ].some(v => v === this.roomInfoSubject.value.roomType) + ) { + return false; + } + + return true; + } + + getConvertTimer(timerInterval: number, unit: number = 1) { + if (timerInterval >= 0 && timerInterval < 60 * unit) { + return Math.floor((timerInterval / 1) * unit) + ' 초'; + } else if (timerInterval >= 60 * unit && timerInterval < 3600 * unit) { + return Math.floor(((timerInterval / 1) * unit) / 60) + ' 분'; + } else if (timerInterval >= 3600 * unit && timerInterval <= 86400 * unit) { + return Math.floor(((timerInterval / 1) * unit) / 60 / 60) + ' 시간'; + } else { + return ''; + } + } + + getShowContextMenu(menuType: string) { + if ( + ['OPEN_ROOM_USER', 'ADD_MEMBER', 'ADD_GROUP', 'EDIT_ROOM'].some( + v => v === menuType + ) + ) { + if ( + !this.roomInfoSubject.value || + !this.roomInfoSubject.value.roomType || + [ + RoomType.Mytalk, + RoomType.Allim, + RoomType.Bot, + RoomType.Link, + RoomType.Allim_Elephant, + RoomType.Allim_TMS + ].some(v => v === this.roomInfoSubject.value.roomType) + ) { + return false; + } + } + + return true; + } + + getShowUnreadCount(): boolean { + if ( + !this.roomInfoSubject.value || + this.roomInfoSubject.value === undefined + ) { + return true; + } + if ( + [ + RoomType.Mytalk, + RoomType.Allim, + RoomType.Bot, + RoomType.Link, + RoomType.Allim_Elephant, + RoomType.Allim_TMS + ].some(v => v === this.roomInfoSubject.value.roomType) + ) { + return false; + } + + return true; + } + + readyToReply(): void { + setTimeout(() => { + this.focusReplyInput(); + }); + } + + focusReplyInput(): void { + setTimeout(() => { + if (!!this.chatForm) { + this.chatForm.focus(); + } + }); + } + + onScrollupMessages(event: any) {} + onYReachStartMessages(event: any) { + // 자동 스크롤이 아닌 버튼 방식으로 변경. + // this.onMoreEvent(this.baseEventSeq); + } + onYReachEndMessages(event: any) { + this.chatMessages.initEventMore(); + if (!!this.snackBarPreviewEvent) { + this.snackBarPreviewEvent.dismiss(); + } + + // // clear readHere object.. 정책상 클리어 하지 않도록 함. + // if (!this.firstCheckReadHere) { + // this.clearReadHere = true; + // } + } + + /** More Event */ + onMoreEvent(seq: number) { + this.store.dispatch( + EventStore.info({ + roomSeq: this.roomInfoSubject.value.roomSeq, + baseSeq: seq, + requestCount: + environment.productConfig.CommonSetting.eventRequestDefaultCount + }) + ); + } + + /** Send Event */ + async onSendMessage(message: string) { + this.chatMessages.initEventMore(); + + if (!this.selectedSticker) { + if (!message || message.trim().length === 0) { + const result = await this.dialogService.open< + AlertDialogComponent, + AlertDialogData, + AlertDialogResult + >(AlertDialogComponent, { + width: '360px', + data: { + title: this.translateService.instant('chat.errors.label'), + message: this.translateService.instant( + 'chat.errors.inputChatMessage' + ) + } + }); + return; + } + } + + // 번역기능 비활성화 2020-02-07 + // if (!!this.isShowTranslation && this.destLocale.trim().length > 0) { + // /** CASE : Translation */ + // // 번역할 대화 없이 스티커만 전송할 경우. + // if (!message || message.trim().length === 0) { + // this.sendMessageOfSticker(message); + // } else { + // this.sendMessageOfTranslate(message); + // } + // } else + + if (!!this.selectedSticker) { + /** CASE : Sticker */ + this.sendMessageOfSticker(message); + } else if ( + message.trim().length > + environment.productConfig.CommonSetting.masstextLength + ) { + /** CASE : MASS TEXT */ + this.sendMessageOfMassText(message); + } else { + /** CASE : Normal Text */ + this.sendMessageOfNormal(message); + } + } + + /** Send Normal message */ + sendMessageOfNormal(message: string) { + this.store.dispatch( + EventStore.send({ + senderSeq: this.loginResSubject.value.userSeq, + req: { + roomSeq: this.roomInfoSubject.value.roomSeq, + eventType: EventType.Character, + sentMessage: StringUtil.escapeHtml(message) + } + }) + ); + } + /** Send Sticker message */ + async sendMessageOfSticker(message: string, isCheck: boolean = true) { + if ( + !!isCheck && + !!message && + message.trim().length > + environment.productConfig.CommonSetting.masstextLength + ) { + const result = await this.dialogService.open< + AlertDialogComponent, + AlertDialogData, + AlertDialogResult + >(AlertDialogComponent, { + width: '360px', + data: { + title: this.translateService.instant('chat.errors.label'), + message: this.translateService.instant( + 'chat.errors.maxLengthOfMassText', + { + maxLength: environment.productConfig.CommonSetting.masstextLength + } + ) + } + }); + return; + } + + const stickerJson: StickerEventJson = { + name: '스티커', + file: this.selectedSticker.index, + chat: !!message ? StringUtil.escapeHtml(message.trim()) : '' + }; + this.store.dispatch( + EventStore.send({ + senderSeq: this.loginResSubject.value.userSeq, + req: { + roomSeq: this.roomInfoSubject.value.roomSeq, + eventType: EventType.Sticker, + sentMessage: JSON.stringify(stickerJson) + } + }) + ); + this.isShowStickerSelector = false; + this.setStickerHistory(this.selectedSticker); + this.selectedSticker = null; + } + /** Send Masstext message */ + sendMessageOfMassText(message: string) { + this.store.dispatch( + EventStore.sendMass({ + senderSeq: this.loginResSubject.value.userSeq, + req: { + roomSeq: this.roomInfoSubject.value.roomSeq, + eventType: EventType.MassText, + // sentMessage: message.replace(/\n/gi, '\r\n') + sentMessage: StringUtil.escapeHtml(message) + } + }) + ); + } + /** + * 번역기능 비활성화 2020-02-07 + * Send Translation message + **/ + // sendMessageOfTranslate(message: string) { + // const destLocale = this.destLocale; + // const original = StringUtil.escapeHtml(message); + // const roomSeq = this.roomInfoSubject.value.roomSeq; + + // if (!!this.isTranslationProcess) { + // return; + // } + + // this.isTranslationProcess = true; + // this.commonApiService + // .translationSave({ + // userSeq: this.loginResSubject.value.userSeq, + // deviceType: this.environmentsInfo.deviceType, + // token: this.loginResSubject.value.tokenString, + // roomSeq, + // original, + // srcLocale: '', + // destLocale + // } as TranslationSaveRequest) + // .pipe( + // take(1), + // map(res => { + // if (res.statusCode === StatusCode.Success) { + // let sentMessage = ''; + // let eventType = EventType.Translation; + // let previewObject: TranslationEventJson | MassTranslationEventJson; + + // if (res.translationSeq > 0) { + // // Mass Text Translation + // previewObject = res; + // sentMessage = res.returnJson; + // eventType = EventType.MassTranslation; + // } else { + // // Normal Text Translation + // previewObject = { + // locale: destLocale, + // original, + // translation: res.translation, + // stickername: '', + // stickerfile: !!this.selectedSticker + // ? this.selectedSticker.index + // : '' + // }; + + // sentMessage = JSON.stringify(previewObject); + // eventType = EventType.Translation; + // } + + // if (!!this.translationPreview) { + // // preview + // this.translationPreviewInfo = { + // previewInfo: res, + // translationType: eventType + // }; + // } else { + // // direct send + // this.store.dispatch( + // EventStore.send({ + // senderSeq: this.loginResSubject.value.userSeq, + // req: { + // roomSeq, + // eventType, + // sentMessage + // } + // }) + // ); + + // if (!!this.translationPreviewInfo) { + // this.translationPreviewInfo = null; + // } + // } + + // if (!!this.selectedSticker) { + // this.isShowStickerSelector = false; + // this.setStickerHistory(this.selectedSticker); + // this.selectedSticker = null; + // } + // } else { + // this.isTranslationProcess = false; + // this.dialogService.open< + // AlertDialogComponent, + // AlertDialogData, + // AlertDialogResult + // >(AlertDialogComponent, { + // width: '360px', + // data: { + // title: '', + // message: this.translateService.instant( + // 'chat.error.translateServerError' + // ) + // } + // }); + // this.logger.error('res', res); + // } + // }), + // catchError(error => { + // this.isTranslationProcess = false; + // this.dialogService.open< + // AlertDialogComponent, + // AlertDialogData, + // AlertDialogResult + // >(AlertDialogComponent, { + // width: '360px', + // data: { + // title: '', + // message: this.translateService.instant( + // 'chat.error.translateServerError' + // ) + // } + // }); + // return of(this.logger.error('error', error)); + // }) + // ) + // .subscribe(() => { + // this.isTranslationProcess = false; + // }); + // } + + onClickReceiveAlarm() { + this.store.dispatch( + RoomStore.updateOnlyAlarm({ roomInfo: this.roomInfoSubject.value }) + ); + } + + /** MassText Detail View */ + onMassDetail(value: number) { + this.store.dispatch( + ChatStore.selectedMassDetail({ + massEventSeq: value + }) + ); + } + + onMassTranslationDetail(params: { + message: Info; + contentsType: string; + }) { + this.commonApiService + .transMassTalkDownload({ + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + eventTransSeq: params.message.sentMessageJson.translationSeq.toString() + }) + .pipe(take(1)) + .subscribe(res => { + let contents = ''; + if (res.statusCode === StatusCode.Success) { + contents = + params.contentsType === 'T' ? res.translation : res.original; + } else { + contents = + params.contentsType === 'T' + ? params.message.sentMessageJson.translation + : params.message.sentMessageJson.original; + } + + this.dialogService.open( + MassDetailComponent, + { + disableClose: false, + width: '550px', + data: { + title: this.translateService.instant('chat.detailView'), + contents + } + } + ); + }); + } + + async onFileViewer(fileInfo: FileEventJson) { + const result = await this.dialogService.open< + FileViewerDialogComponent, + FileViewerDialogData, + FileViewerDialogResult + >(FileViewerDialogComponent, { + position: { + top: '50px' + }, + maxWidth: '100vw', + maxHeight: '100vh', + height: 'calc(100% - 50px)', + width: '100%', + hasBackdrop: false, + panelClass: 'app-dialog-full', + data: { + fileInfo, + downloadUrl: this.sessionVerInfo.downloadUrl, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + userSeq: this.loginResSubject.value.userSeq + } + }); + } + + /** File Save, Save As */ + onSave(value: { + fileInfo: FileEventJson; + fileDownloadItem: FileDownloadItem; + type: string; + }) { + this.logger.debug('fileSave', value); + if (value.type === 'saveAs') { + this.nativeService + .selectSaveFilePath(value.fileInfo.fileName) + .then(result => { + if (!result) { + return; + } + + if (result.canceled) { + // this.snackBarService.open( + // this.translateService.instant('common.file.results.canceled'), + // this.translateService.instant('common.file.errors.label'), + // { + // duration: 1000 + // } + // ); + } else { + this.saveFile(value, result.filePath); + } + }) + .catch(reason => { + this.snackBarService.open( + this.translateService.instant( + 'common.file.errors.failToSpecifyPath' + ), + this.translateService.instant('common.file.errors.label') + ); + }); + } else { + this.saveFile(value); + } + } + + onFileDragEnter(items: DataTransferItemList) { + this.clearView(); + this.logger.debug('onFileDragEnter', items); + } + + onFileDragOver() { + this.logger.debug('onFileDragOver'); + } + + onFileDragLeave() { + this.logger.debug('onFileDragLeave'); + } + + onExistNewMessage(info: Info) { + let message = ''; + + const contents = StringUtil.convertFinalEventMessage( + info.type, + info.sentMessageJson || info.sentMessage + ); + + if (!!contents) { + const senderUser = this.userInfoListSubject.value.filter( + user => user.seq === info.senderSeq + ); + if (!!senderUser && senderUser.length > 0) { + message += `${senderUser[0].name} : `; + } + message += contents; + + this.snackBarPreviewEvent = this.snackBarService.open( + message, + this.translateService.instant('common.messages.confirm'), + { + // duration: 3000, + verticalPosition: 'bottom', + horizontalPosition: 'center', + panelClass: ['chat-snackbar-class'] + } + ); + this.snackBarPreviewEvent.onAction().subscribe(() => { + this.chatMessages.initEventMore(); + this.chatMessages.scrollToBottom(); + this.snackBarPreviewEvent.dismiss(); + }); + } + } + + saveFile( + value: { + fileInfo: FileEventJson; + fileDownloadItem: FileDownloadItem; + type: string; + }, + savePath?: string + ) { + this.appFileService.fileTalkDownlod({ + req: { + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + attachmentsSeq: value.fileInfo.attachmentSeq, + fileDownloadItem: value.fileDownloadItem + }, + fileName: value.fileInfo.fileName, + savePath + }); + } + + async onFileSelected(fileUploadItems: FileUploadItem[]) { + this.logger.debug('onFileSelected', fileUploadItems); + this.clearView(); + + const info = { + senderSeq: this.loginResSubject.value.userSeq, + roomSeq: this.roomInfoSubject.value.roomSeq + }; + + const allObservables: Observable[] = []; + + const fileAllowSize = + !!this.sessionVerInfo && this.sessionVerInfo.fileAllowSize + ? this.sessionVerInfo.fileAllowSize + : environment.productConfig.CommonSetting.defaultFileAllowSize; + + if (fileAllowSize > 0) { + if ( + fileUploadItems.filter( + fui => fui.file.size > fileAllowSize * 1024 * 1024 + ).length + ) { + this.snackBarService.open( + this.translateService.instant('common.file.errors.oversize', { + maxSize: fileAllowSize + }), + '', + { + duration: 1000, + verticalPosition: 'bottom', + horizontalPosition: 'center' + } + ); + + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onUploadComplete(); + } + return; + } + } + + const checkExt = this.commonApiService.acceptableExtensionForFileTalk( + fileUploadItems.map(fui => FileUtil.getExtension(fui.file.name)) + ); + if (!checkExt.accept) { + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onUploadComplete(); + } + + this.snackBarService.open( + this.translateService.instant('common.file.errors.notSupporedType', { + supporedType: + checkExt.reject.length > 0 ? checkExt.reject.join(',') : '' + }), + '', + { + duration: 1000, + verticalPosition: 'bottom', + horizontalPosition: 'center' + } + ); + + return; + } + // 멀티 파일 업로드 + const isFileTalkSaveMulti = this.getSaveFileMulti(fileUploadItems); + + if (!!isFileTalkSaveMulti) { + this.fileTalkSaveMulti(fileUploadItems); + return; + } + // 일반 업로드 + for (const fileUploadItem of fileUploadItems) { + let thumbnail: File; + if ( + -1 !== + [ + '3gp', + 'avi', + 'm4v', + 'mkv', + 'mov', + 'mp4', + 'mpeg', + 'mpg', + 'rv', + 'ts', + 'webm', + 'wmv' + ].indexOf(FileUtil.getExtension(fileUploadItem.file.name)) + ) { + thumbnail = await FileUtil.thumbnail(fileUploadItem.file); + this.logger.debug('thumbnail', thumbnail); + } + + const req: FileTalkSaveRequest = { + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + roomSeq: this.roomInfoSubject.value.roomSeq, + file: fileUploadItem.file, + fileName: fileUploadItem.file.name, + thumb: thumbnail, + fileUploadItem + }; + + allObservables.push( + this.commonApiService.fileTalkSave(req, null).pipe( + map(res => { + if (!res) { + return; + } + if (StatusCode.Success === res.statusCode) { + return res; + } else { + throw res; + } + }) + ) + ); + } + + forkJoin(allObservables) + .pipe(take(1)) + .subscribe( + resList => { + for (const res of resList) { + this.store.dispatch( + EventStore.send({ + senderSeq: info.senderSeq, + req: { + roomSeq: info.roomSeq, + eventType: EventType.File, + sentMessage: res.returnJson + } + }) + ); + } + }, + error => { + this.logger.debug('onFileSelected error', error); + this.snackBarService.open( + this.translateService.instant('common.file.errors.failToUpload'), + this.translateService.instant('common.file.errors.label') + ); + + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onUploadComplete(); + } + }, + () => { + this.fileUploadQueue.onUploadComplete(); + } + ); + } + + private fileTalkSaveMulti(fileUploadItemList: FileUploadItem[]) { + const fileList: File[] = []; + const info = { + senderSeq: this.loginResSubject.value.userSeq, + roomSeq: this.roomInfoSubject.value.roomSeq + }; + for (const fileUploadItem of fileUploadItemList) { + fileList.push(fileUploadItem.file); + } + const req: FileTalkSaveMultiRequest = { + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + roomSeq: this.roomInfoSubject.value.roomSeq, + fileUploadItems: fileUploadItemList, + files: fileList, + type: 'b' + }; + + this.commonApiService + .fileTalkSaveMulti(req, null) + .pipe( + map(res => { + if (!res) { + return; + } + if (StatusCode.Success === res.statusCode) { + return res; + } else { + throw res; + } + }) + ) + .subscribe( + res => { + this.logger.debug('fileTalkSaveMulti', res); + + this.store.dispatch( + EventStore.send({ + senderSeq: info.senderSeq, + req: { + roomSeq: info.roomSeq, + eventType: EventType.BundleImage, + sentMessage: res.returnJson + } + }) + ); + }, + error => { + this.logger.debug('onFileSelected error', error); + this.snackBarService.open( + this.translateService.instant('common.file.errors.failToUpload'), + this.translateService.instant('common.file.errors.label') + ); + + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onUploadComplete(); + } + }, + () => { + this.fileUploadQueue.onUploadComplete(); + } + ); + } + + private getSaveFileMulti(fileUploadItems: FileUploadItem[]): boolean { + for (const fileUploadItem of fileUploadItems) { + if ( + fileUploadItems.length <= 1 || + -1 === + ['jpg', 'jpeg', 'jpe', 'png', 'gif'].indexOf( + FileUtil.getExtension(fileUploadItem.file.name) + ) + ) { + return false; + } + } + + return true; + } + + onContextMenuMessage(params: { + event: MouseEvent; + message: Info; + type?: string; + }) { + params.event.preventDefault(); + params.event.stopPropagation(); + + this.messageContextMenuPosition.x = params.event.clientX + 'px'; + this.messageContextMenuPosition.y = params.event.clientY + 'px'; + this.messageContextMenuTrigger.menu.focusFirstItem('mouse'); + this.messageContextMenuTrigger.menuData = { + message: params.message, + loginRes: this.loginResSubject.value, + clicktype: params.type + }; + this.messageContextMenuTrigger.openMenu(); + } + + async onClickMessageContextMenu( + menuType: string, + message: Info, + clicktype?: string + ) { + switch (menuType) { + case 'COPY': + { + switch (message.type) { + case EventType.Character: + { + if ( + this.clipboardService.copyFromContent( + (message as Info).sentMessage + ) + ) { + this.snackBarService.open( + this.translateService.instant( + 'common.clipboard.results.copied' + ), + '', + { + duration: 3000, + verticalPosition: 'top', + horizontalPosition: 'center' + } + ); + } + } + break; + case EventType.MassText: + { + this.commonApiService + .massTalkDownload({ + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + eventMassSeq: message.seq + }) + .pipe(take(1)) + .subscribe(res => { + if (this.clipboardService.copyFromContent(res.content)) { + this.snackBarService.open( + this.translateService.instant( + 'common.clipboard.results.copied' + ), + '', + { + duration: 3000, + verticalPosition: 'top', + horizontalPosition: 'center' + } + ); + } + }); + } + break; + case EventType.Translation: + { + let trgtStr = ''; + if (clicktype === 'translation') { + // translation + trgtStr = (message.sentMessageJson as TranslationEventJson) + .translation; + } else { + // original + trgtStr = (message.sentMessageJson as TranslationEventJson) + .original; + } + + if (this.clipboardService.copyFromContent(trgtStr)) { + this.snackBarService.open( + this.translateService.instant( + 'common.clipboard.results.copied' + ), + '', + { + duration: 3000, + verticalPosition: 'top', + horizontalPosition: 'center' + } + ); + } + } + break; + case EventType.MassTranslation: + { + const sentMessageJson: MassTranslationEventJson = message.sentMessageJson as MassTranslationEventJson; + this.commonApiService + .transMassTalkDownload({ + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + eventTransSeq: sentMessageJson.translationSeq.toString() + }) + .pipe(take(1)) + .subscribe(res => { + let contents = ''; + if (res.statusCode === StatusCode.Success) { + contents = + clicktype === 'translation' + ? res.translation + : res.original; + } else { + contents = + clicktype === 'translation' + ? sentMessageJson.translation + : sentMessageJson.original; + } + + if (this.clipboardService.copyFromContent(contents)) { + this.snackBarService.open( + this.translateService.instant( + 'common.clipboard.results.copied' + ), + '', + { + duration: 3000, + verticalPosition: 'top', + horizontalPosition: 'center' + } + ); + } + }); + } + break; + default: + break; + } + } + break; + case 'FORWARD': + { + const result = await this.dialogService.open< + CreateChatDialogComponent, + CreateChatDialogData, + CreateChatDialogResult + >(CreateChatDialogComponent, { + width: '600px', + data: { + type: UserSelectDialogType.MessageForward, + title: this.translateService.instant('chat.forwardEventTo'), + ignoreRoom: [this.roomInfoSubject.value] + } + }); + + if (!!result && !!result.choice && result.choice) { + const userSeqs: number[] = []; + let roomSeq = ''; + if ( + !!result.selectedUserList && + result.selectedUserList.length > 0 + ) { + result.selectedUserList.map(user => userSeqs.push(user.seq)); + } + + if (!!result.selectedRoom) { + roomSeq = result.selectedRoom.roomSeq; + } + + if (userSeqs.length > 0 || roomSeq.trim().length > 0) { + this.store.dispatch( + EventStore.forward({ + senderSeq: this.loginResSubject.value.userSeq, + req: { + roomSeq: '-999', + eventType: message.type, + sentMessage: message.sentMessage + }, + trgtUserSeqs: userSeqs, + trgtRoomSeq: roomSeq + }) + ); + } + } + } + break; + case 'FORWARD_TO_ME': + { + if (this.loginResSubject.value.talkWithMeBotSeq > -1) { + this.store.dispatch( + EventStore.forward({ + senderSeq: this.loginResSubject.value.userSeq, + req: { + roomSeq: '-999', + eventType: message.type, + sentMessage: message.sentMessage + }, + trgtUserSeqs: [this.loginResSubject.value.talkWithMeBotSeq] + }) + ); + } + } + break; + case 'DELETE': + { + const result = await this.dialogService.open< + ConfirmDialogComponent, + ConfirmDialogData, + ConfirmDialogResult + >(ConfirmDialogComponent, { + width: '400px', + data: { + title: this.translateService.instant('chat.removeEvent'), + html: this.translateService.instant('chat.confirmRemoveEvent') + } + }); + + if (!!result && !!result.choice && result.choice) { + this.store.dispatch( + EventStore.del({ + roomSeq: this.roomInfoSubject.value.roomSeq, + eventSeq: message.seq + }) + ); + } + } + break; + case 'RECALL': + { + const result = await this.dialogService.open< + ConfirmDialogComponent, + ConfirmDialogData, + ConfirmDialogResult + >(ConfirmDialogComponent, { + width: '400px', + data: { + title: this.translateService.instant('chat.recallEvent'), + html: this.translateService.instant('chat.confirmRecallEvent') + } + }); + + if (!!result && !!result.choice && result.choice) { + this.store.dispatch( + EventStore.cancel({ + roomSeq: this.roomInfoSubject.value.roomSeq, + eventSeq: message.seq, + deviceType: this.environmentsInfo.deviceType + }) + ); + } + } + break; + default: + break; + } + } + + async onClickContextMenu(menuType: string) { + switch (menuType) { + case 'OPEN_ALBUM_LIST': + { + this.store.dispatch( + ChatStore.selectedRightDrawer({ + req: RightDrawer.AlbumBox + }) + ); + } + break; + case 'OPEN_FILE_LIST': + { + this.store.dispatch( + ChatStore.selectedRightDrawer({ + req: RightDrawer.FileBox + }) + ); + } + break; + case 'CHAT_SEARCH': + { + this.onShowToggleSearchArea(); + } + break; + case 'OPEN_ROOM_USER': + { + this.store.dispatch( + ChatStore.selectedRightDrawer({ + req: RightDrawer.RoomUser + }) + ); + } + break; + case 'ADD_MEMBER': + { + const curRoomUser = this.userInfoListSubject.value.filter( + user => + user.seq !== this.loginResSubject.value.userSeq && user.isJoinRoom + ); + + const result = await this.dialogService.open< + CreateChatDialogComponent, + CreateChatDialogData, + CreateChatDialogResult + >(CreateChatDialogComponent, { + width: '600px', + data: { + type: UserSelectDialogType.EditChatMember, + title: this.translateService.instant('chat.modifyRoomMember'), + curRoomUser + } + }); + + if (!!result && !!result.choice && result.choice) { + if ( + !!result.selectedUserList && + result.selectedUserList.length > 0 && + curRoomUser + .map(user => user.seq) + .sort() + .join('|') === + result.selectedUserList + .map(user => user.seq) + .sort() + .join('|') + ) { + // 변경된 것이 없다면 중지. + return; + } + + // include me here.. + const userSeqs: number[] = this.userInfoListSubject.value + .filter(userInfo => userInfo.isJoinRoom) + .map(userInfo => userInfo.seq); + + if ( + !!result.selectedUserList && + result.selectedUserList.length > 0 + ) { + result.selectedUserList.forEach(user => { + if (userSeqs.indexOf(user.seq) < 0) { + userSeqs.push(user.seq); + } + }); + } + + if (userSeqs.length > 0) { + // include me + const myUserSeq = this.loginResSubject.value.userSeq; + if (!!myUserSeq && userSeqs.indexOf(myUserSeq) < 0) { + userSeqs.push(myUserSeq); + } + + this.store.dispatch( + RoomStore.inviteOrOpen({ + req: { + divCd: 'Invite', + userSeqs + } + }) + ); + } + } + } + break; + case 'ADD_GROUP': + { + const result = await this.dialogService.open< + SelectGroupDialogComponent, + SelectGroupDialogData, + SelectGroupDialogResult + >(SelectGroupDialogComponent, { + width: '600px', + data: { + title: this.translateService.instant('chat.addMemberToGroup') + } + }); + + if (!!result && !!result.choice && result.choice) { + if (!!result.group) { + const oldGroup: GroupDetailData = result.group; + const trgtUserSeq: number[] = []; + result.group.userSeqs.map(seq => trgtUserSeq.push(seq)); + this.userInfoListSubject.value + .filter(v => v.isJoinRoom) + .filter(v => result.group.userSeqs.indexOf(v.seq) < 0) + .forEach(user => { + trgtUserSeq.push(user.seq); + }); + + this.store.dispatch( + SyncStore.updateGroupMember({ + oldGroup, + trgtUserSeq + }) + ); + } + } + } + break; + case 'EDIT_ROOM': + { + const result = await this.dialogService.open< + EditChatRoomDialogComponent, + EditChatRoomDialogData, + EditChatRoomDialogResult + >(EditChatRoomDialogComponent, { + width: '600px', + data: { + title: this.translateService.instant('chat.settingsOfRoom'), + roomInfo: this.roomInfoSubject.value + } + }); + + if (!!result && !!result.choice && result.choice) { + const roomName: string = result.roomName; + const roomNameChangeTarget: string = result.roomNameChangeTarget; + const timeRoomInterval: number = result.timeRoomInterval; + const roomInfo: RoomInfo = result.roomInfo; + + // 방제목 업데이트. + this.store.dispatch( + RoomStore.update({ + req: { + roomSeq: roomInfo.roomSeq, + roomName, + receiveAlarm: roomInfo.receiveAlarm, + syncAll: + roomNameChangeTarget.toUpperCase() === 'ALL' ? true : false + } + }) + ); + + if ( + roomInfo.isTimeRoom && + timeRoomInterval > 0 && + roomInfo.timeRoomInterval !== timeRoomInterval + ) { + this.store.dispatch( + RoomStore.updateTimeRoomInterval({ + roomSeq: roomInfo.roomSeq, + timerInterval: timeRoomInterval + }) + ); + } + } + } + break; + case 'CLOSE_ROOM': + { + this.store.dispatch(ChatStore.clearSelectedRoom()); + } + break; + default: + break; + } + } + + onClickOpenProfile(userSeq: number) { + const roomType = this.roomInfoSubject.value.roomType; + if ( + roomType !== RoomType.Allim && + roomType !== RoomType.Bot && + roomType !== RoomType.Link && + roomType !== RoomType.Allim_Elephant && + roomType !== RoomType.Allim_TMS + ) { + this.openProfile.emit({ userSeq }); + } + } + + /** About Sticker */ + onShowToggleStickerSelector() { + this.isShowStickerSelector = !this.isShowStickerSelector; + if (!this.isShowStickerSelector) { + this.selectedSticker = null; + } + } + onSelectedSticker(stickerInfo: StickerFilesInfo) { + this.selectedSticker = stickerInfo; + } + setStickerHistory(sticker: StickerFilesInfo) { + const history = this.localStorageService.get(KEY_STICKER_HISTORY); + + if (!!history && history.length > 0) { + const stickers: string[] = []; + [sticker.index, ...history.filter(hist => hist !== sticker.index)].map( + (s, i) => { + if (i < 10) { + stickers.push(s); + } + } + ); + + this.localStorageService.set(KEY_STICKER_HISTORY, stickers); + } else { + this.localStorageService.set(KEY_STICKER_HISTORY, [ + sticker.index + ]); + } + } + getStickerHistory(): string[] { + return this.localStorageService.get(KEY_STICKER_HISTORY); + } + + /** About Chat Search */ + onShowToggleSearchArea() { + this.isShowSearchArea = !this.isShowSearchArea; + + if (!this.isShowSearchArea) { + this.searchedListSubject.next([]); + this.searchedFocusEvent = null; + this.searchText = ''; + } + } + onCloseSearchArea() { + this.isShowSearchArea = false; + this.searchedListSubject.next([]); + this.searchedFocusEvent = null; + this.searchText = ''; + + this.moreSearchProcessing = false; + this.searchTotalCount = 0; + this.searchCurrentIndex = 0; + + this.store.dispatch(EventStore.infoForSearchEnd({})); + } + onSearchChat(searchText: string, baseSeq?: number) { + this.searchText = searchText; + + // CASE :: searching text after retrieve All event Infos. + this.store.dispatch( + EventStore.infoAll({ + req: { + roomSeq: this.roomInfoSubject.value.roomSeq, + baseSeq: this.eventListSubject.value[0].seq, + requestCount: + environment.productConfig.CommonSetting.eventRequestDefaultCount * 2 + }, + infoList: undefined + }) + ); + + // this.doSearchTextInEvent(searchText); + } + doSearchTextInEvent(searchText: string, baseSeq?: number): void { + this.searchedListSubject.next( + this.eventListSubject.value.filter(event => { + let contents = ''; + if (event.type === EventType.Character) { + contents = event.sentMessage; + } else if ( + event.type === EventType.Sticker && + !!event.sentMessageJson + ) { + contents = (event.sentMessageJson as StickerEventJson).chat; + } else if (event.type === EventType.File && !!event.sentMessageJson) { + contents = (event.sentMessageJson as FileEventJson).fileName; + } else if ( + event.type === EventType.MassText && + !!event.sentMessageJson + ) { + contents = (event.sentMessageJson as MassTextEventJson).content; + } else if ( + (event.type === EventType.Translation || + event.type === EventType.MassTranslation) && + !!event.sentMessageJson + ) { + contents = (event.sentMessageJson as TranslationEventJson).original; + contents += (event.sentMessageJson as TranslationEventJson) + .translation; + } + + return contents.indexOf(searchText) > -1; + }) + ); + + if ( + !!this.searchedListSubject.value && + this.searchedListSubject.value.length > 0 + ) { + this.searchTotalCount = this.searchedListSubject.value.length; + + if (!!baseSeq && baseSeq > 0) { + this.searchedListSubject.value.forEach((searched, index) => { + if (searched.seq <= baseSeq) { + this.searchCurrentIndex = index + 1; + this.searchedFocusEvent = searched; + } + }); + } else { + this.searchCurrentIndex = this.searchedListSubject.value.length; + this.searchedFocusEvent = this.searchedListSubject.value[ + this.searchedListSubject.value.length - 1 + ]; + } + + this.store.dispatch(EventStore.infoForSearchEnd({})); + + // this.chatMessages.gotoPosition(this.searchedFocusEvent.seq); + this.chatMessages.initEventMore(this.searchedFocusEvent.seq); + } else { + this.searchTotalCount = 0; + this.searchCurrentIndex = 0; + + this.searchedFocusEvent = null; + } + } + onPrevSearch() { + this.searchedListSubject.value.forEach((event, index) => { + if (event.seq === this.searchedFocusEvent.seq && index > 0) { + this.searchCurrentIndex = this.searchCurrentIndex - 1; + this.searchedFocusEvent = this.searchedListSubject.value[index - 1]; + this.chatMessages.gotoPosition(this.searchedFocusEvent.seq); + } + }); + } + onNextSearch() { + // let exist = false; + // this.searchedList.forEach((event, index) => { + // if ( + // event.seq === this.searchedFocusEvent.seq && + // index < this.searchedList.length - 2 && + // !exist + // ) { + // exist = true; + // this.searchCurrentIndex = this.searchCurrentIndex + 1; + // this.searchedFocusEvent = this.searchedList[index + 1]; + // this.goSearchPosition(this.searchedFocusEvent.seq); + // } + // }); + + if (this.searchCurrentIndex < this.searchedListSubject.value.length) { + this.searchedFocusEvent = this.searchedListSubject.value[ + this.searchCurrentIndex + ]; + this.searchCurrentIndex = this.searchCurrentIndex + 1; + this.chatMessages.gotoPosition(this.searchedFocusEvent.seq); + } + } + onSearchAndPrev() { + if ( + !!this.searchText && + this.searchText.trim().length > 0 && + this.eventRemainedSubject.value + ) { + this.moreSearchProcessing = true; + this.chatMessages.storeScrollPosition(); + + // Case :: retrieve event infos step by step until include searchtext in event.. + this.store.dispatch( + EventStore.infoForSearch({ + req: { + roomSeq: this.roomInfoSubject.value.roomSeq, + baseSeq: this.eventListSubject.value[0].seq, + requestCount: + environment.productConfig.CommonSetting.eventRequestDefaultCount + }, + searchText: this.searchText + }) + ); + } + } + + /** + * 번역기능 비활성화 2020-02-07 + * About Translation */ + // onShowToggleTranslation() { + // this.isShowTranslation = !this.isShowTranslation; + // if (!this.isShowTranslation) { + // } + // } + // onChangeTranslationSimpleView(value: boolean) { + // this.translationSimpleview = value; + // } + // onChangeTranslationPreView(value: boolean) { + // this.translationPreview = value; + // } + // onChangeDestLocale(destLocale: string) { + // this.destLocale = destLocale; + // } + // onCancelTranslation() { + // this.isTranslationProcess = false; + // this.translationPreviewInfo = null; + // } + + // onSendTranslationMessage(params: { + // previewInfo: TranslationSaveResponse | null; + // translationType: EventType.Translation | EventType.MassTranslation; + // }) { + // let sentMessage = ''; + // if (params.translationType === EventType.MassTranslation) { + // // Mass Text Translation + // sentMessage = params.previewInfo.returnJson; + // } else { + // // Normal Text Translation + // sentMessage = JSON.stringify({ + // locale: params.previewInfo.destLocale, + // original: params.previewInfo.original, + // translation: params.previewInfo.translation, + // stickername: '', + // stickerfile: !!this.selectedSticker ? this.selectedSticker.index : '' + // }); + // } + + // this.store.dispatch( + // EventStore.send({ + // senderSeq: this.loginResSubject.value.userSeq, + // req: { + // roomSeq: this.roomInfoSubject.value.roomSeq, + // eventType: params.translationType, + // sentMessage + // } + // }) + // ); + + // this.isTranslationProcess = false; + // this.translationPreviewInfo = null; + // } + + onClipboardPaste(event: ClipboardEvent) { + this.nativeService.readFromClipboard().then(async data => { + if (!!data.image && !!data.text) { + const result = await this.dialogService.open< + ClipboardDialogComponent, + ClipboardDialogData, + ClipboardDialogResult + >(ClipboardDialogComponent, { + width: '800px', + maxWidth: '800px', + height: '800px', + minHeight: '800px', + disableClose: false, + data: { + content: data + } + }); + + if (result.selected.text) { + this.onSendMessage(data.text.replace(/\t/g, ' ')); + } + + if (result.selected.image) { + const fileUploadItems = FileUploadItem.fromDataUrls( + 'clipboard', + data.imageDataUrl + ); + + this.onFileSelected(fileUploadItems); + } + } else if (!!data.image && !data.text) { + const fileUploadItems = FileUploadItem.fromDataUrls( + 'clipboard', + data.imageDataUrl + ); + + this.onFileSelected(fileUploadItems); + } else { + const v = this.chatForm.replyInput.nativeElement.value; + const selectionStart = this.chatForm.replyInput.nativeElement + .selectionStart; + const selectionEnd = this.chatForm.replyInput.nativeElement + .selectionEnd; + + const start = v.substr(0, selectionStart); + const end = v.substr(selectionEnd); + + this.chatForm.replyInput.nativeElement.value = `${start}${data.text}${end}`; + + event.preventDefault(); + } + }); + } +} diff --git a/documents/업무/2월/2째주/묶음파일변경사항.txt b/documents/업무/2월/2째주/묶음파일변경사항.txt new file mode 100644 index 0000000..71ae742 --- /dev/null +++ b/documents/업무/2월/2째주/묶음파일변경사항.txt @@ -0,0 +1 @@ +c:\projects\work\next-ucap-messenger\projects\ucap-webmessenger-api-common\src\lib\services\common-api.service.ts \ No newline at end of file diff --git a/weekly-report/주간보고_박병은_2020.0110.pptx b/weekly-report/1월/주간보고_박병은_2020.0110.pptx similarity index 100% rename from weekly-report/주간보고_박병은_2020.0110.pptx rename to weekly-report/1월/주간보고_박병은_2020.0110.pptx diff --git a/weekly-report/주간보고_박병은_2020.0117.pptx b/weekly-report/1월/주간보고_박병은_2020.0117.pptx similarity index 100% rename from weekly-report/주간보고_박병은_2020.0117.pptx rename to weekly-report/1월/주간보고_박병은_2020.0117.pptx diff --git a/weekly-report/주간보고_박병은_2020.0123.pptx b/weekly-report/1월/주간보고_박병은_2020.0123.pptx similarity index 100% rename from weekly-report/주간보고_박병은_2020.0123.pptx rename to weekly-report/1월/주간보고_박병은_2020.0123.pptx diff --git a/weekly-report/주간보고_박병은_2020.0131.pptx b/weekly-report/1월/주간보고_박병은_2020.0131.pptx similarity index 100% rename from weekly-report/주간보고_박병은_2020.0131.pptx rename to weekly-report/1월/주간보고_박병은_2020.0131.pptx diff --git a/weekly-report/주간보고_박병은_2020.0207.pptx b/weekly-report/2월/주간보고_박병은_2020.0207.pptx similarity index 100% rename from weekly-report/주간보고_박병은_2020.0207.pptx rename to weekly-report/2월/주간보고_박병은_2020.0207.pptx diff --git a/weekly-report/2월/주간보고_박병은_2020.0214.pptx b/weekly-report/2월/주간보고_박병은_2020.0214.pptx new file mode 100644 index 0000000000000000000000000000000000000000..99572b96b3a4a11d4ad9c202dce10e10ff372bb3 GIT binary patch literal 26458 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 literal 0 HcmV?d00001