DEV/other things

Zendesk 기능 메모

석봉 2022. 10. 29. 13:59
const userId = 1234;

// 지정한 티켓 번호로 이동.
client.invoke('routeTo', 'user', userId);

사용하던 Zendesk 기능 메모.

 

* 주의점!

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

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

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

 


일반적인 Zendesk API 요청 axios / fetch 예시

// 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;
  }
}





// fetch
const url = '/api/v2/users/search.json?query=is_verified:false';
const options = {
    method: 'GET',
    headers: {
        'Content-type': 'application/json',
    }
};

const res = await fetch(url, options);
await res.json();

 

 

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를 이용하여 검색

https://support.zendesk.com/hc/ko/articles/4408883318554-%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B7%B8%EB%A3%B9-%EB%B0%8F-%EC%A1%B0%EC%A7%81-%EA%B2%80%EC%83%89%ED%95%98%EA%B8%B0

 

사용자, 그룹 및 조직 검색하기

version="1.0" encoding="UTF-8"? 사용 중인 플랜 최종 사용자, 팀원, 그룹 및 조직 개체의 데이터는 각각의 페이지에서 검색할 수 있습니다. 이 문서에서 다루는 주제는 다음과 같습니다. 검색하기 정보 u

support.zendesk.com

/**
 * - 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(); // 활용하기

 

Dynamically changeing app icons (탑바 앱 가변 아이콘 샘플 코드)
https://github.com/Seokhyeon-Park/icon-counter.git