구글 스크립트를 이용한 문자보내기 문제 #24
-
사용 중인 프로그래밍 언어 및 버전구글 스크립트 SDK 버전No response 운영 환경기타 (설명란에 자세히 기재해주세요) 질문/문제 설명솔라피에서는 공식적으로 구글 스크립트를 지원하지 않는다고 알고 있습니다 API 오류 (400): child "date" fails because ["date" must be a valid ISO 8601 date] 상세 정보: {"errorCode":"ValidationError","errorMessage":"child "date" fails because ["date" must be a valid ISO 8601 date]"} 실행하면 위와 같은 오류가 발생되고 있습니다 코드 예시// 스프레드시트가 열릴 때 실행되어 사용자 지정 메뉴를 추가하는 함수는 DispatchSheetCreator.gs로 이동했습니다.
// function onOpen() 함수 삭제
/**
* 솔라피 API 관련 상수 및 설정
*/
const SOLAPI_CONFIG = {
apiKey: "", // 솔라피 API 키
apiSecret: "", // 여기에 솔라피에서 발급받은 API Secret을 입력하세요 (필수)
from: "", // 솔라피에 등록된 발신번호
baseUrl: "https://api.solapi.com", // 솔라피 API 기본 URL
sendUrl: "/messages/v4/send", // 단일 메시지 발송 엔드포인트
sendManyUrl: "/messages/v4/send-many" // 여러 건 메시지 발송 엔드포인트
};
function showTemplateSelectionDialog() {
Logger.log('showTemplateSelectionDialog 함수 시작');
const ss = SpreadsheetApp.getActiveSpreadsheet();
const activeSheet = ss.getActiveSheet();
const activeSheetName = activeSheet.getName();
Logger.log(`현재 시트 이름: ${activeSheetName}`);
const templateSheet = ss.getSheetByName('문자 템플릿');
if (!templateSheet) {
SpreadsheetApp.getUi().alert('"문자 템플릿" 시트를 찾을 수 없습니다.');
Logger.log('오류: "문자 템플릿" 시트를 찾을 수 없습니다.');
return;
}
Logger.log('"문자 템플릿" 시트 찾음');
const allowedSheetNames = [
String(templateSheet.getRange('I2').getValue()).trim(),
String(templateSheet.getRange('I3').getValue()).trim(),
].filter(String); // 빈 문자열 제거
Logger.log(`허용된 시트 이름: ${allowedSheetNames}`);
// 허용된 시트 이름 목록에 현재 시트 이름이 있는지 확인
if (!allowedSheetNames.includes(activeSheetName)) {
SpreadsheetApp.getUi().alert(`현재 시트("${activeSheetName}")에서는 문자 보내기 기능을 사용할 수 없습니다.`);
Logger.log(`알림: 현재 시트("${activeSheetName}")에서는 문자 보내기 기능을 사용할 수 없습니다.`);
return;
}
Logger.log('현재 시트 이름 유효성 검사 통과');
const selectedCell = activeSheet.getActiveCell();
const phoneNumberColumnIndex = 9; // I열 (연락처)
const customerNameColumnIndex = 2; // B열 (화주명) - 실제 화주명 컬럼 인덱스로 변경
const phoneNumber = selectedCell.getValue();
Logger.log(`선택된 전화번호: ${phoneNumber}`);
if (selectedCell.getColumn() !== phoneNumberColumnIndex) {
// 컬럼 문자 가져오는 부분 수정 - getColumnLetter() 대신 열 번호를 문자로 변환하는 함수 사용
const columnLetter = columnToLetter(phoneNumberColumnIndex);
SpreadsheetApp.getUi().alert(`연락처 컬럼 (${columnLetter}열)의 셀을 선택해주세요.`);
Logger.log(`알림: 잘못된 컬럼 선택 (${selectedCell.getColumn()})`);
return;
}
Logger.log('전화번호 컬럼 확인');
const customerName = activeSheet.getRange(selectedCell.getRow(), customerNameColumnIndex).getValue();
Logger.log(`현재 행의 화주명: ${customerName}`);
const templates = templateSheet.getDataRange().getValues();
Logger.log(`"문자 템플릿" 시트 전체 데이터: ${JSON.stringify(templates)}`);
const templateHeaders = templates[0];
const customerNameColumnInTemplate = templateHeaders.indexOf('거래처상호'); // A열
const templateLocationColumn = templateHeaders.indexOf('상하차지'); // B열
const templateInfoColumn = templateHeaders.indexOf('배차정보'); // C열
const templateEtcColumn = templateHeaders.indexOf('기타전달사항'); // D열
Logger.log(`템플릿 헤더 정보: 거래처상호(${customerNameColumnInTemplate}), 상하차지(${templateLocationColumn}), 배차정보(${templateInfoColumn}), 기타전달사항(${templateEtcColumn})`);
if (customerNameColumnInTemplate === -1 || templateLocationColumn === -1 || templateInfoColumn === -1 || templateEtcColumn === -1) {
SpreadsheetApp.getUi().alert('"문자 템플릿" 시트에 필요한 헤더가 없습니다.');
Logger.log('오류: "문자 템플릿" 시트에 필요한 헤더가 없습니다.');
return;
}
const filteredTemplates = templates
.slice(1) // 헤더 제외
.filter(row => String(row[customerNameColumnInTemplate]).trim() === String(customerName).trim());
Logger.log(`필터링된 템플릿 데이터: ${JSON.stringify(filteredTemplates)}`);
if (filteredTemplates.length === 0) {
SpreadsheetApp.getUi().alert(`"${customerName}" 화주에 대한 문자 템플릿이 없습니다.`);
Logger.log(`알림: "${customerName}" 화주에 대한 문자 템플릿이 없습니다.`);
return;
}
// HTML UI로 전달할 템플릿 목록 생성
const templateList = filteredTemplates.map(row => ({
title: `${row[templateLocationColumn]} - ${row[templateInfoColumn]}`,
content: `${row[templateLocationColumn]}\n${row[templateInfoColumn]}${row[templateEtcColumn] ? '\n' + row[templateEtcColumn] : ''}`,
}));
Logger.log(`생성된 템플릿 목록: ${JSON.stringify(templateList)}`);
// 템플릿 HTML 가져오기
const htmlTemplate = HtmlService.createTemplateFromFile('TemplateSelection');
// 템플릿에 데이터 바인딩
htmlTemplate.phoneNumber = phoneNumber;
htmlTemplate.templates = templateList;
// HTML 출력 생성
const htmlOutput = htmlTemplate.evaluate()
.setWidth(400)
.setHeight(300);
SpreadsheetApp.getUi().showModalDialog(htmlOutput, '템플릿 선택');
Logger.log('템플릿 선택 대화상자 표시 시도');
}
function processSelectedTemplate(selectedTemplate, phoneNumber) {
Logger.log('processSelectedTemplate 함수 시작');
Logger.log(`선택된 템플릿: ${JSON.stringify(selectedTemplate)}, 전화번호: ${phoneNumber}`);
// 메시지 내용 확인 및 발송 대화상자
const htmlTemplate = HtmlService.createTemplate(`
<style>
body { font-family: Arial, sans-serif; margin: 15px; }
.message-box { margin-bottom: 15px; }
textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.options { margin: 15px 0; }
.buttons { margin-top: 20px; }
button { padding: 8px 15px; margin-right: 10px; background-color: #4285f4; color: white;
border: none; border-radius: 4px; cursor: pointer; }
button:hover { background-color: #3367d6; }
button.cancel { background-color: #f1f1f1; color: #333; }
.alt-button { background-color: #0f9d58; }
.help-button { background-color: #f4b400; }
</style>
<div>
<h3>문자 메시지 전송</h3>
<div class="message-box">
<p><b>받는 번호:</b> <?= phoneNumber ?></p>
<p><b>문자 내용:</b></p>
<textarea id="message" rows="6"><?= content ?></textarea>
</div>
<div class="buttons">
<button onclick="sendMessage()">보내기</button>
<button class="alt-button" onclick="sendAlternative()">대체 방식으로 보내기</button>
<button class="help-button" onclick="getDebugInfo()">디버그 정보</button>
<button class="cancel" onclick="google.script.host.close()">취소</button>
</div>
</div>
<script>
function sendMessage() {
const message = document.getElementById('message').value;
if (!message.trim()) {
alert("메시지 내용을 입력해주세요.");
return;
}
google.script.run
.withSuccessHandler(function(result) {
if (result.success) {
alert("메시지가 발송되었습니다.");
google.script.host.close();
} else {
alert("메시지 발송 실패: " + result.error);
}
})
.withFailureHandler(function(error) {
alert("오류가 발생했습니다: " + error);
})
.sendMessageToNumber('<?= phoneNumber ?>', message);
}
function sendAlternative() {
const message = document.getElementById('message').value;
if (!message.trim()) {
alert("메시지 내용을 입력해주세요.");
return;
}
google.script.run
.withSuccessHandler(function(result) {
if (result) {
alert("대체 방식으로 메시지가 발송되었습니다.");
google.script.host.close();
}
})
.withFailureHandler(function(error) {
alert("대체 방식 오류: " + error);
})
.sendMessageAlternative('<?= phoneNumber ?>', message);
}
function getDebugInfo() {
google.script.run
.collectSolapiDebugInfo();
}
</script>
`);
// 템플릿에 데이터 바인딩
htmlTemplate.phoneNumber = phoneNumber;
htmlTemplate.content = selectedTemplate.content;
// HTML 출력 생성
const htmlOutput = htmlTemplate.evaluate()
.setWidth(600)
.setHeight(400);
SpreadsheetApp.getUi().showModalDialog(htmlOutput, '문자 내용 확인 및 발송');
Logger.log('문자 내용 확인 및 발송 대화상자 표시 시도');
}
/**
* 솔라피 API를 이용한 실제 문자 발송 함수
* @param {string} phoneNumber - 수신번호
* @param {string} message - 발송할 메시지
* @returns {object} 발송 결과
*/
function sendMessageToNumber(phoneNumber, message) {
Logger.log(`sendMessageToNumber 함수 시작 - 전화번호: ${phoneNumber}, 내용: ${message}`);
try {
// 전화번호 포맷 정리 (하이픈 제거)
phoneNumber = phoneNumber.toString().replace(/-/g, '');
// 메시지 타입 결정 (SMS/LMS)
const messageType = getMessageByteLength(message) > 90 ? "LMS" : "SMS";
Logger.log(`메시지 타입: ${messageType}, 바이트 길이: ${getMessageByteLength(message)}`);
// API Secret 확인
if (!SOLAPI_CONFIG.apiSecret) {
SpreadsheetApp.getUi().alert("API Secret이 설정되지 않았습니다. 솔라피에서 발급받은 API Secret을 SOLAPI_CONFIG.apiSecret에 설정해주세요.");
return {
success: false,
error: "API Secret이 설정되지 않았습니다."
};
}
// 인증 관련 값 생성 (밀리초 타임스탬프)
const timestamp = Date.now().toString();
const salt = getRandomString(20); // 랜덤 문자열 생성
const signature = generateSignature(SOLAPI_CONFIG.apiKey, SOLAPI_CONFIG.apiSecret, timestamp, salt);
// 현재 시간을 정확한 ISO 8601 형식으로 변환 (YYYY-MM-DDTHH:mm:ss.sssZ)
// UTC 시간으로 설정하여 타임존 문제 방지
const now = new Date();
const isoDate = now.toISOString(); // 예: 2023-09-22T15:30:45.123Z
Logger.log(`ISO 8601 날짜 (toISOString): ${isoDate}`);
// 공식 API 문서 기반 메시지 요청 본문
const payload = {
messages: [{
to: phoneNumber,
from: SOLAPI_CONFIG.from,
text: message,
type: messageType
}],
date: isoDate, // ISO 8601 형식의 날짜
agent: {
sdkVersion: "appscript/1.0",
osPlatform: "Google Apps Script"
}
};
// HMAC-SHA256 인증 헤더 구성 (정확한 형식으로)
const headers = {
"Content-Type": "application/json",
"Authorization": `HMAC-SHA256 apiKey=${SOLAPI_CONFIG.apiKey}, date=${timestamp}, salt=${salt}, signature=${signature}`
};
// 요청 옵션 구성
const options = {
method: "post",
contentType: "application/json",
headers: headers,
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
// 전체 요청 정보 로깅
Logger.log(`솔라피 API 요청 정보:`);
Logger.log(`URL: ${SOLAPI_CONFIG.baseUrl}${SOLAPI_CONFIG.sendManyUrl}`);
Logger.log(`Headers: ${JSON.stringify(headers)}`);
Logger.log(`Payload: ${JSON.stringify(payload)}`);
Logger.log(`HTTP Method: ${options.method}`);
// 솔라피 여러 건 메시지 전송 API 엔드포인트
const apiUrl = `${SOLAPI_CONFIG.baseUrl}${SOLAPI_CONFIG.sendManyUrl}`;
const response = UrlFetchApp.fetch(apiUrl, options);
const responseCode = response.getResponseCode();
// 응답 디버깅을 위한 추가 로그
Logger.log(`API 응답 코드: ${responseCode}`);
Logger.log(`API 응답 헤더: ${JSON.stringify(response.getAllHeaders())}`);
// 응답 데이터를 텍스트로 먼저 기록
const responseText = response.getContentText();
Logger.log(`API 응답 원본 텍스트: ${responseText}`);
// 응답 파싱 시도
let result;
try {
result = JSON.parse(responseText);
Logger.log(`API 응답 파싱 성공: ${JSON.stringify(result)}`);
} catch (parseError) {
Logger.log(`API 응답 파싱 실패: ${parseError}`);
// 응답 내용을 있는 그대로 기록
SpreadsheetApp.getUi().alert(`API 응답 파싱 실패. 원본 응답: ${responseText}`);
return {
success: false,
error: "API 응답 파싱 오류: " + parseError.toString(),
rawResponse: responseText
};
}
if (responseCode === 200 || responseCode === 201 || responseCode === 202) {
// 성공 시 모든 응답 데이터를 포함하여 반환
SpreadsheetApp.getUi().alert(`문자 발송 성공!\n수신번호: ${phoneNumber}\n메시지 유형: ${messageType}`);
return {
success: true,
messageId: result.messageId || "",
to: phoneNumber,
type: messageType,
completeResponse: result
};
} else {
// 오류 시 자세한 정보 포함
const errorMsg = result.message || result.errorMessage || "API 오류";
const errorDetails = JSON.stringify(result);
SpreadsheetApp.getUi().alert(`API 오류 (${responseCode}): ${errorMsg}\n\n상세 정보: ${errorDetails}`);
return {
success: false,
error: errorMsg,
errorCode: result.code || result.errorCode || responseCode,
rawError: responseText
};
}
} catch (e) {
Logger.log(`문자 발송 중 오류 발생: ${e.toString()}`);
SpreadsheetApp.getUi().alert(`문자 발송 중 오류가 발생했습니다: ${e.toString()}`);
return {
success: false,
error: e.toString()
};
}
}
/**
* 솔라피 공식 SDK 방식으로 문자 발송 시도 (대체 방법)
* @param {string} phoneNumber - 수신번호
* @param {string} message - 발송할 메시지
*/
function sendMessageAlternative(phoneNumber, message) {
Logger.log(`sendMessageAlternative 시작 - 전화번호: ${phoneNumber}`);
try {
// 전화번호 포맷 정리 (하이픈 제거)
phoneNumber = phoneNumber.toString().replace(/-/g, '');
// 메시지 타입 결정 (SMS/LMS)
const messageType = getMessageByteLength(message) > 90 ? "LMS" : "SMS";
// API Secret 확인
if (!SOLAPI_CONFIG.apiSecret) {
SpreadsheetApp.getUi().alert("API Secret이 설정되지 않았습니다. 솔라피에서 발급받은 API Secret을 SOLAPI_CONFIG.apiSecret에 설정해주세요.");
return false;
}
// 인증 관련 값 생성 (밀리초 타임스탬프)
const timestamp = Date.now().toString();
const salt = getRandomString(20); // 랜덤 문자열 생성
const signature = generateSignature(SOLAPI_CONFIG.apiKey, SOLAPI_CONFIG.apiSecret, timestamp, salt);
// 현재 시간을 정확한 ISO 8601 형식으로 변환 (UTC 시간으로)
const now = new Date();
const isoDate = now.toISOString(); // 예: 2023-09-22T15:30:45.123Z
Logger.log(`ISO 8601 날짜 (대체 방식): ${isoDate}`);
// 단일 메시지 발송 API 요청 본문 - 다른 형식으로 시도
const payload = {
message: {
to: phoneNumber,
from: SOLAPI_CONFIG.from,
text: message,
type: messageType
},
date: isoDate, // ISO 8601 형식의 날짜
agent: {
sdkVersion: "appscript/1.0",
osPlatform: "Google Apps Script"
}
};
// HMAC-SHA256 인증 헤더 구성 (정확한 형식으로)
const headers = {
"Content-Type": "application/json",
"Authorization": `HMAC-SHA256 apiKey=${SOLAPI_CONFIG.apiKey}, date=${timestamp}, salt=${salt}, signature=${signature}`
};
// 요청 옵션 구성
const options = {
method: "post",
contentType: "application/json",
headers: headers,
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
// 전체 요청 정보 로깅
Logger.log(`솔라피 API 요청 정보 (대체 방식):`);
Logger.log(`URL: ${SOLAPI_CONFIG.baseUrl}${SOLAPI_CONFIG.sendUrl}`);
Logger.log(`Headers: ${JSON.stringify(headers)}`);
Logger.log(`Payload: ${JSON.stringify(payload)}`);
Logger.log(`HTTP Method: ${options.method}`);
// 솔라피 단일 메시지 전송 API 엔드포인트
const apiUrl = `${SOLAPI_CONFIG.baseUrl}${SOLAPI_CONFIG.sendUrl}`;
const response = UrlFetchApp.fetch(apiUrl, options);
const responseCode = response.getResponseCode();
Logger.log(`API 응답 코드 (대체 방식): ${responseCode}`);
const responseText = response.getContentText();
Logger.log(`API 응답 원본 (대체 방식): ${responseText}`);
Logger.log(`API 응답 헤더 (대체 방식): ${JSON.stringify(response.getAllHeaders())}`);
try {
const result = JSON.parse(responseText);
Logger.log(`대체 방식 응답 파싱 성공: ${JSON.stringify(result)}`);
if (responseCode === 200 || responseCode === 201 || responseCode === 202) {
SpreadsheetApp.getUi().alert(`대체 방식으로 문자 발송 성공!\n수신번호: ${phoneNumber}`);
return true;
} else {
const errorMsg = result.message || result.errorMessage || "API 오류";
SpreadsheetApp.getUi().alert(`대체 방식 API 오류: ${errorMsg}\n\n상세 정보: ${JSON.stringify(result)}`);
return false;
}
} catch (parseError) {
Logger.log(`대체 방식 응답 파싱 실패: ${parseError}`);
SpreadsheetApp.getUi().alert(`응답 파싱 실패: ${responseText}`);
return false;
}
} catch (error) {
Logger.log(`대체 방식 오류: ${error.toString()}`);
SpreadsheetApp.getUi().alert(`대체 방식 오류: ${error.toString()}`);
return false;
}
}
/**
* 랜덤 문자열 생성 함수 (salt용)
* @param {number} length - 생성할 문자열 길이
* @returns {string} 랜덤 문자열
*/
function getRandomString(length) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
/**
* HMAC-SHA256 서명 생성 함수
* @param {string} apiKey - API 키
* @param {string} apiSecret - API 시크릿
* @param {string} timestamp - 타임스탬프
* @param {string} salt - 솔트 값
* @returns {string} Base64로 인코딩된 서명
*/
function generateSignature(apiKey, apiSecret, timestamp, salt) {
const message = timestamp + salt;
const signature = Utilities.computeHmacSha256Signature(message, apiSecret);
return Utilities.base64Encode(signature);
}
/**
* 솔라피 고객센터에 문의할 수 있는 정보를 수집하는 함수
*/
function collectSolapiDebugInfo() {
const debugInfo = {
apiKey: SOLAPI_CONFIG.apiKey.substring(0, 4) + "..." + SOLAPI_CONFIG.apiKey.substring(SOLAPI_CONFIG.apiKey.length - 4),
fromNumber: SOLAPI_CONFIG.from,
scriptVersion: "1.0",
timestamp: new Date().toISOString(),
sendUrl: SOLAPI_CONFIG.baseUrl + SOLAPI_CONFIG.sendUrl,
sendManyUrl: SOLAPI_CONFIG.baseUrl + SOLAPI_CONFIG.sendManyUrl,
hasApiSecret: SOLAPI_CONFIG.apiSecret ? "설정됨" : "설정되지 않음"
};
const debugText = `=== 솔라피 디버그 정보 ===\n` +
`API 키: ${debugInfo.apiKey}\n` +
`API Secret: ${debugInfo.hasApiSecret}\n` +
`발신번호: ${debugInfo.fromNumber}\n` +
`스크립트 버전: ${debugInfo.scriptVersion}\n` +
`시간: ${debugInfo.timestamp}\n` +
`단일 발송 URL: ${debugInfo.sendUrl}\n` +
`여러 건 발송 URL: ${debugInfo.sendManyUrl}\n` +
`\n이 정보를 솔라피 고객센터에 문의하실 때 함께 보내주세요.`;
SpreadsheetApp.getUi().alert(debugText);
return debugInfo;
}
/**
* 숫자 열 인덱스를 문자 열 이름으로 변환 (예: 1 -> A, 2 -> B, 27 -> AA)
* 구글 스프레드시트 API에 getColumnLetter 함수가 없으므로 직접 구현
* @param {number} column - 변환할 열 인덱스 (1부터 시작)
* @return {string} 열 문자 이름
*/
function columnToLetter(column) {
let temp, letter = '';
while (column > 0) {
temp = (column - 1) % 26;
letter = String.fromCharCode(temp + 65) + letter;
column = (column - (temp + 1)) / 26;
}
return letter;
}
/**
* 문자 메시지 바이트 길이 계산 (SMS/LMS 구분용)
* @param {string} message - 메시지 내용
* @returns {number} 바이트 길이
*/
function getMessageByteLength(message) {
// 텍스트를 바이트 배열로 변환
const blob = Utilities.newBlob(message);
return blob.getBytes().length;
}시도한 해결 방법ISO 8601 형식에 맞추어 기대하는 결과성공적인 메시지 전송 확인사항
|
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 3 replies
-
|
안녕하세요, 솔라피 기술지원팀입니다. date 포맷은 |
Beta Was this translation helpful? Give feedback.
회원님이 보내주신 데이터중 헤더의 date를 보시면 안내드린 포맷과 다른 부분을 확인하실 수 있습니다. 해당 부분 참고하셔서 date= 부분을 iso 8601 포맷에 맞게 변경하여 재시도 해주시면 감사하겠습니다.
이후에도 동일한 현상 발생하시면 지금처럼 데이터 보내주시면 확인 후 안내 도와드리겠습니다.