ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Zendesk 기능 메모
    DEV/other things 2022. 10. 29. 13:59
    const userId = 1234;
    
    // 지정한 티켓 번호로 이동.
    client.invoke('routeTo', 'user', userId);

    사용하던 Zendesk 기능 메모.

     

    * 주의점!

    1. Sample이라 상황에 맞게 수정하여 사용한다.

    2. HelpCenter는 최대한 API를 쓰지 않는것이 좋다!

    3. 각 API마다 권한별 결괏값이 다를 수 있다. 권한에 상관 없이 모두 표출되어야 하는 경우 관리자 계정 정보를 사용해야 한다.

     


    일반적인 Zendesk API 요청 axios 예시

    const callApi = async (zendesk_master_emailm zendesk_api_token) => {
      try {
        const auth = 'Basic ' + btoa(zendesk_master_email + '/token:' + zendesk_api_token);
        const headers = { 'Authorization': auth };
    
        const data = {
        	// POST 요청시 필요한 데이터 작성
        };
    
    	// GET 요청의 경우 data 제외
        const res = await axios.post('[Zendesk API URL 작성]', data, { headers });
    
        return user;
      } catch (error) {
        throw error;
      }
    }

     

    HelpCenter 

    // account, user 정보를 가지고 있는 Object
    // helpcenter에서 사용 가능함
    HelpCenter

     

    유저정보(id로 조회)

      const url = `/api/v2/users/${userId}`;
      
      const options = {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Basic ' + btoa(`${ZENDESK_INFO.email}/token:${ZENDESK_INFO.token}`)
        },
      };
    
      try {
        const response = await fetch(url, options);
        if(response?.status != 200) { return; }
        const data = await response.json();
        return data;
      } catch (error) {
        console.log(error);
      }
      
    // or client request 사용(zendesk app 내부)
      
    const { user } = await client.request('/api/v2/users/me.json');
    console.log(user.external_id);

     

    내 유저정보

    const getMyInfo = await fetch(`/api/v2/users/me.json`);
    
    try {
        const response = await getMyInfo.json();
        if(response?.status != 200) { return; }
        const data = await response.json();
        return data;
    } catch (error) {
        console.log(error);
    }

     

    HelpCenter Sections 조회

    // HelpCenter의 경우 다국어를 지원함
    // 해당 언어에 따라 url이 변경되므로 참고하자.
    const url = `/api/v2/help_center/${language}/sections`;
    
    const options = {
    method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Basic ' + btoa(`${ZENDESK_INFO.email}/token:${ZENDESK_INFO.token}`)
        },
    };
    
    try {
        const response = await fetch(url, options);
        if(response?.status != 200) { return; }
        const data = await response.json();
        return data;
    } catch (error) {
        console.log(error);
    }

     

    HelpCenter 특정 Section의 Articles 조회

    const url = `/api/v2/help_center/${language}/sections/${section_id}/articles`;
    
    const options = {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Basic ' + btoa(`${ZENDESK_INFO.email}/token:${ZENDESK_INFO.token}`)
        },
    };
    
    try {
        const response = await fetch(url, options);
        if(response?.status != 200) { return; }
        const data = await response.json();
        return data;
    } catch (error) {
        console.log(error);
    }

     

    HelpCenter 특정 Section의 Articles 조회 (100개씩)

    // 특정 Section
    const url = `/api/v2/help_center/${language}/sections/${section_id}/articles.json?per_page=100`;
    // 모든 Sections
    const all_section_url = `/api/v2/help_center/${language}/articles.json?per_page=100`;
    
    const options = {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Basic ' + btoa(`${ZENDESK_INFO.email}/token:${ZENDESK_INFO.token}`)
        },
    };
    
    try {
        const response = await fetch(url, options);
        if(response?.status != 200) { return; }
        const data = await response.json();
        return data;
    } catch (error) {
        console.log(error);
    }

     

    HelpCenter Community의 Posts 조회

    // 조회 URL
    const url = `/api/v2/community/posts`;
    
    const options = {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Basic ' + btoa(`${ZENDESK_INFO.email}/token:${ZENDESK_INFO.token}`)
        },
    };
    
    try {
        const response = await fetch(url, options);
        if(response?.status != 200) { return; }
        const data = await response.json();
        return data;
    } catch (error) {
        console.log(error);
    }

     

    HelpCenter Community의 Posts 조회(search query 사용)

    // 태그 query 검색
    // 참고로 젠데스크 search는 and 조건 불가함
    // 대신 검색 결과는 우선순위가 존재함 (직접 필터링해서 AND처럼 사용 가능)
    const url = `/api/v2/help_center/community_posts/search?query=${query}`;
    
    const options = {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Basic ' + btoa(`${ZENDESK_INFO.email}/token:${ZENDESK_INFO.token}`)
        },
    };
    
    try {
        const response = await fetch(url, options);
        if(response?.status != 200) { return; }
        const data = await response.json();
        return data;
    } catch (error) {
        console.log(error);
    }

     

    HelpCenter Post 이미지 첨부

    // 이미지가 첨부되면 유저 이미지가 젠데스크 서버로 업로드가 되며, <img src={업로드된 경로} /> 사용
    // API 계정 정보
    const ZD_CONFIG = {
        EMAIL: "",
        API_TOKEN: ""
    }
    
    const auth = 'Basic ' + btoa(ZD_CONFIG.EMAIL + '/token:' + ZD_CONFIG.API_TOKEN);
    
    /**
     * https://developer.zendesk.com/api-reference/help_center/help-center-api/user_images/
     * 사용자 이미지 API를 사용하여 최종 사용자가 헬프 센터 인스턴스에 이미지를 업로드하도록 할 수 있습니다.
     * 사용자 이미지 업로드는 3단계 프로세스입니다.
     * 1. 이미지 업로드 URL 및 토큰 생성을 요청합니다. 이미지 업로드 URL 및 토큰 생성을 참조하십시오 .
     * 2. 이미지 업로드 URL에 PUT 요청을 하여 이미지를 업로드합니다. 업로드 URL로 이미지 업로드를 참조하십시오 .
     * 3. 도움말 센터에서 이미지 경로 생성을 요청하세요. 이미지 경로 만들기 를 참조하십시오 .
     * 게시물 댓글 본문의 경로를 사용하여 이미지를 인라인으로 표시할 수 있습니다.
     */
    
    // 1. 이미지 업로드 URL 및 토큰 생성
    const createAnImageUploadURLAndToken = async (file) => {
        const response = await fetch('/api/v2/guide/user_images/uploads',
            {
                method: 'POST',
                headers: {
                    'Authorization': auth,
                },
                body: JSON.stringify({
                    "content_type": file.type,
                    "file_size": file.size
                })
            })
        try {
            if (!response.ok) throw response;
            else {
                const { upload } = await response.json();
                return upload;
            }
        } catch (error) {
            console.error(error);
            alert('이미지 첨부 중 오류가 발생했습니다.(1)')
        }
    }
    
    // 2. 업로드 URL로 이미지 업로드
    const uploadTheImage = async ({ headers, token, url }, fileArrBuf) => {
        const response = await fetch(url,
            {
                method: 'PUT',
                headers,
                body: fileArrBuf
            })
        try {
            if (response.status != 200) throw response;
            else return token;
        } catch (error) {
            console.error(error);
            alert('이미지 첨부 중 오류가 발생했습니다.(2)')
        }
    }
    
    // 3. 이미지 경로 생성
    const createImagePath = async (token) => {
        const response = await fetch('/api/v2/guide/user_images',
            {
                method: 'POST',
                headers: {
                    'Authorization': auth,
                },
                body: JSON.stringify({
                    "token": token,
                    "brand_id": ${brand_id},
                })
            })
    
        try {
            if (!response.ok) throw response;
            else {
                const { user_image } = await response.json();
                return user_image;
            }
        } catch (error) {
            console.error(error);
            alert('이미지 첨부 중 오류가 발생했습니다.(3)')
        }
    }
    
    // 업로드하려는 파일이 2mb가 넘는지 확인
    const check_file_size_over_2mb = (file) => {
        return file.size > 2097152 ? true : false;
    }
    
    // 파일 선택 이벤트
    $(document).on("change", "input.formFile", async (event) => {
        const currentFile = event.target.files[0];
        if (!check_file_size_over_2mb(currentFile)) {
            const resultCreateAnImageUploadURLAndToken = await createAnImageUploadURLAndToken(currentFile);
            if (resultCreateAnImageUploadURLAndToken) {
                const fileArrBuf = await currentFile.arrayBuffer();
                const token = await uploadTheImage(resultCreateAnImageUploadURLAndToken, fileArrBuf);
                if (token) {
                    const uploaded = createImagePath(token);
                    console.log('uploaded', uploaded)
                }
            }
        } else alert("파일 용량이 2MB를 초과합니다. 2MB 이하의 파일을 선택 부탁드립니다.");
    });

     

    ZAFClient

    // import
    const client = ZAFClient.init();

     

    앱 로드 시 발생

    /**
     * - 앱 로드 시 발생
     * - 에이전트가 앱을 다시 로드하는 경우에도 실행
     */
    client.on('app.registered', async function(context) {
    	// ...
    });

     

    티켓 제출 시 발생

    /**
     * - 제출 버튼을 클릭하는 경우 발생
     */
    client.on('ticket.save', async () => {
    	// ...
    });

     

    Instance 예시

    getInstanceClient(location) {
        const { instances } = await client.get('instances');
        for (const instanceGuid in instances) {
            if (instances[instanceGuid].location === location) {
                return client.instance(instanceGuid);
            }
        }
    }

     

    티켓 불러오기 예시 1

    const { ticket } = await client.get('ticket');

     

    티켓 불러오기 예시 2

    // ${ticket_id} : ticket의 id
    const ticket = await client.request(`/api/v2/tickets/${ticket_id}`);

     

    티켓 불러오기(여러개) 예시

    // 'ids=' 뒤 숫자는 티켓 번호 예시
    const tickets = await client.request(`/api/v2/tickets/show_many?ids=301075,301079`);

     

    티켓의 유저 정보 예시

    // 유저(요청자)정보
    client.get('ticket.requester');
    // 전화번호, 아이디 등등...
    client.get('ticket.requester.identities');

     

    Search를 이용하여 검색

    /**
     * - status : ticket의 상태를 의미하며 new, solved 등 존재
     * - requester_id : 요청자 id
     * - tags : ticket의 tag
     * 순서가 중요한듯함.
     * https://developer.zendesk.com/api-reference/ticketing/ticket-management/search/
     * https://developer.zendesk.com/documentation/ticketing/using-the-zendesk-api/searching-with-the-zendesk-api/
     */
    const _query = `/api/v2/search.json?query=type:ticket requester_id:${ticket.requester_id} status:new tags:${ticket.tag_name}`;
    const tickets = await client.request(_query);
    
    // sample 2 *
    const query = `/api/v2/search.json?query=type:ticket custom_field_5098259165071:true created>=2023-07-01 created<=2023-07-30`;
    const searchRes = await client.request(query);
    
    // sample 2
    const query = `/api/v2/search.json?query=type:ticket created>2023-08-01 created<2023-08-05`;
    const searchRes = await client.request(query);
    
    // sample 3
    const searchRes = await client.request('/api/v2/search.json?query=type:ticket&sort_by=updated_at&sort_order=desc');
    
    // user의 특정 user_fields(custom_fields)의 값으로 확인하기
    // 값이 null인 field를 찾고 싶으면 "field:none" 사용
    // 여집합 검색은 필드 키 값 앞에 - 사용 >> "-user_fields_key:value"
    query = `/api/v2/search.json?query=type:user user_fields_key:value`;
    const searchRes = await client.request(query);

     

    앱 내부에서 통신

    /**
     * - [발신단]
     * - 발신할 .js 내부에 정의, 필요한 이벤트 내에서 발동
     * - backgroundClient : background App instance
     * - trigger() : 발신측
     * - function_name : 수신측 (상황에 맞게 이름을 지정하여 사용)
     * - ticket.id : 전송할 인자 (예시로 ticket의 id를 발송)
     */
    // const backgroundClient = getInstance... (인스턴스 참고) 
    backgroundClient.trigger('function_name', ticket.id);
    
    
    /**
     * - [수신단]
     * - 수신한 .js 내부에 정의
     * - id : 예시로 ticket id를 수신하여 명칭을 id로 사용, 상황에 맞게 이름을 지정하여 사용
     */
     
     client.on('function_name', async (id) => {
     	// ...
     })

     

    앱 간 통신

    /**
     * - [발신단]
     * - 발신할 App의 .js 내부에 정의, 필요한 이벤트 내에서 발동
     * - appId : 대상(수신) App의 id
     * - event : 이벤트명
     * - body : data.body
     */
    const appId = client._metadata.settings.app_id_sample;
    const sampleData1 = 'This is sample data1';
    const sampleData2 = 'This is sample data2';
    
    // 데이터
    let data = {
        'app_id' : appId,
        'event' : 'event_name',
        'body'  : {
            data1 : sampleData1,
            data2 : sampleData2
        }
    }
    
    // 발신
    client.request({
        url: `/api/v2/apps/notify`,
        method: 'POST',
        contentType: "application/json",
        data: JSON.stringify(data),
    });
    
    
    /**
     * - [수신단]
     * - 수신할(타 App) .js 내부에 정의
     * - data : 예시로 data 수신하여 명칭을 data로 사용, 상황에 맞게 이름을 지정하여 사용
     */
     
    client.on('api_notification.event_name', async (data) => {
        console.log("data 1 : ", data.body.data1);
        console.log("data 2 : ", data.body.data2);
    });

     

    앱 사이즈 수정

    // width, height를 상황에 맞게 조절
    client.invoke('resize', { width: '1400px', height: '85vh' });

     

    티켓 오픈

    const ticketId = 1200;
    
    // 지정한 티켓 번호로 이동.
    client.invoke('routeTo', 'ticket', ticketId);

     

     

    앱 오픈

    client.on('pane.activated', function (data) {
    
    });

     

    탑바 닫기

    // 해당 탑바를 닫음 (탑바만 가능한 것으로 알고 있음)
    client.invoke('popover', 'hide');

     

    미리 로드

    client.invoke('preloadPane');

     

    티켓 모두 닫기 (Console)

    for(let ele of document.querySelectorAll('[data-test-id="close-button"]')){ ele.click(); }

     

    티켓 번호 수동으로 긁을때... (크롤링)

    for(const ele of document.querySelectorAll('[data-garden-id="tables.row"]')){
        console.log(ele.childNodes[3].innerText);
    }
    
    $('.StyledPageBase-sc-lw1w9j-0.StyledPage-sc-1k0een3-0.StyledNavigation-sc-184uuwe-0.idfrwS')[$('.StyledPageBase-sc-lw1w9j-0.StyledPage-sc-1k0een3-0.StyledNavigation-sc-184uuwe-0.idfrwS').length-1].click();

     

    수동으로 API 긁을때...

    // url 예시
    let urls = [
        '/api/v2/search.json?query=type:ticket%20external_id:1234',
        '/api/v2/tickets/1234',
    ]
    
    for(let url of urls) {
        let res = await client.request(url);
        console.log(res);
    }

     

    수동으로 API 긁을때... (with next_page)

    let url = '/api/v2/search.json?query=assignee:시스템%20created>2022-10-07%20tags:migration order_by:created_at sort:asc';
    
    for(let cnt = 0; cnt < 10; cnt++) {
        let data = await client.request(url);
        url = data.next_page;
    }

     

    manifest parameters 사용법

    client.metadata().then((metadata) => {
      console.log(metadata.settings);
    });

     

    zendesk alert

    /**
     * alert 종류
     * 'notice' : green
     * 'alert'  : yellow
     * 'error'  : red
     * 
     * duration 자리에 { sticky: true } 넣으면 닫기를 눌러야 alert 종료
     */
    
    // base
    client.invoke('notify', message[, kind, duration]);
    
    // sample ('notify', '내용', 'alert 종류', 3000 ms)
    client.invoke('notify', `다시 확인해주세요.`, 'alert', 3000);

     

    티켓 업데이트 / 내부메모 추가

    async appendZendeskComment() {
        // 현재 티켓번호 불러오기
        const ticketId = await ticket.getID();
    
        // 내부매모 등록 
        try {
            const commentHTML = `
            <p>==================================</p>
            // ...
            <p>==================================</p>`;
            await ticket.update(ticketId, { comment: { public: false, html_body: commentHTML } });
        } catch (error) {
            client.invoke('notify', `내부매모 등록에 오류가 발생하였습니다. (error code : ${error})`, 'error', { sticky: true });
            throw error;
        }
    },
    
    const ticket = {
        /**
         * [티켓 번호 가져오기]
         * !!! 사이드 바에서 다음과 같이 사용(앵간하면 이렇게 사용) !!!
         * ticketId = client['_context']['ticketId'];
         */
        async getID() {
            try {
                const res = await client.get('ticket.id');
                if (res['error']) {
                    throw new Error(res['error']);
                }
                console.log("[getId] current ticket id : ", res['ticket.id'])
                return res['ticket.id'];
            } catch (error) {
                throw error;
            }
        },
        /**
         * [티켓 업데이트]
         * @param {*} ticketID : 업데이트 티켓 번호
         * @param {*} param : 업데이트 티켓 데이터
         * @returns 
         */
        async update(ticketID, param) {
            try {
                const option = {
                    url: `/api/v2/tickets/${ticketID}`,
                    method: 'PUT',
                    contentType: 'application/json',
                    data: JSON.stringify({ ticket: param })
                }
                const res = await client.request(option);
                return res;
            } catch (error) {
                throw error;
            }
        }
    }
     
     

    루프 예시

    // 티켓의 comments 조회로 예시
    // 작업의 경우 보통 동일하게 작성 (생각해보면 당연함)
    // res에 대한 예외처리(실패) 추가하여 사용
    
    async function searchZendesk(ticket_ids) {
    	for(const ticket of tickets_ids) {
        	let target = `/api/v2/tickets/${ticket.id}/comments`;	// API 예시
            
            await zendeskApiRequest(target).then(async (res) => {
              	let next_page = res.next_page;
    
              	while (next_page !== null) {
                    const resMore = await zendeskApiRequest(next_page);
                    next_page = resMore.next_page;
                    
                    // 작업
    			}
                
                // 작업
        	}
        }
    }
    
    async function zendeskApiRequest(url) {
      try {
        const res = await client.request(url);
        return res;
      } catch (error) {
        return false;
      }
    }
     
     

    팝업창에서 사용

    const client = opener.ZAFClient.init(); // 활용하기

     

     
     
     
     
     
     

    댓글

Designed by Tistory.