Zendesk 기능 메모
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를 이용하여 검색
사용자, 그룹 및 조직 검색하기
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