쇼핑몰 프로젝트를 진행하면서 주문이 들어오거나 특정 갯수 이하로 재고가 떨어지거나 재고가 0개가 되는 경우 텔레그램을 통해 쇼핑몰 관리자에게 메시지를 전송하는 기능이 필요하다.
이를 위해서 telegram api 를 사용하는데 설계 순서는
- Telegram 메시지 전송 테스트
동적으로 데이터가 변화되었을 때 메시지를 전송하는 것이 아닌 하드 코딩 된 값을 전송해보는 telegram api 테스트- db에 트리거를 생성하여 주문이 들어왔을 때 관련 정보를 telegram_msg 테이블에 저장
- 하드 코딩 된 값이 아니라 동적으로 값을 가져올 수 있도록 spring boot 코드 작성
이 과정으로 진행하였다.
// MyTelegramBot.java
@Component
public abstract class MyTelegramBot {
protected final String prefix;
protected final String botToken;
protected final String chatId;
protected final String url;
protected final String payload;
private static final String method = "POST";
private static final Map<String, String> headers = new HashMap<>();
static {
headers.put("Content-Type", "application/x-www-form-urlencoded");
}
@Autowired
public MyTelegramBot(@Value("${telegram.bot.prefix}") String prefix,
@Value("${telegram.bot.token}") String botToken,
@Value("${telegram.chat.id}") String chatId) {
this.prefix = prefix;
this.botToken = botToken;
this.chatId = chatId;
this.url = "https://api.telegram.org/bot" + botToken + "/sendMessage";
this.payload = "chat_id=" + chatId + "&parse_mode=html&text=";
}
protected void send(String key, JSONArray contents) {
int size = contents.size();
JSONObject content = null;
Map<String, String> map = null;
for(int i = 0 ; i < size ; i++) {
map = new HashMap<String,String>();
content = (JSONObject)contents.get(i);
Set<String> set = content.keySet();
Iterator<String> iter = set.iterator();
String _key = null;
String _value = null;
while(iter.hasNext()) {
_key = iter.next().toString();
_value = content.get(_key).toString();
map.put(_key, _value);
}
send(key,map);
}
post(key,contents);
}
private void send(String key, Map<String,String> content) {
String _payload = getLinkWithContent(key,content);
try {
String response = Util.send(method, url, headers, _payload);
// 여기서 response를 처리할 수 있습니다. 예를 들어, 로깅 등
System.out.println("Telegram API Response: " + response);
} catch (Exception e) {
e.printStackTrace();
}
}
protected String getLinkWithContent(String key, Map<String,String> content) {
String templete = getTemplate(key);
//String _payload = "";
String link = getATag(key, content);
if(link.startsWith("<a") || link.startsWith("<A")) {
link = payload + link+ replace(templete,content) + "</a>";;
//link = _payload;
}else {
link = payload + replace(templete,content);;
}
return link;
}
protected abstract String getATag(String key, Map<String,String> content);
protected abstract void post(String key, JSONArray contents);
/**
* get message template by key
* write templete for purpose
* replacement block %key_block%
* ex :
* [주문필요] vendor: %vendor% , item_name : %item_name% , qty: %qty%
*
* @param key templete caller
* @return message templete
*/
protected abstract String getTemplate(String key);
/**
* get replacement block should be matched json key
*
* _map = new JSONObject();
* _map.put("vendor", "5S Distributor");
* _map.put("item_name", "shin ramen");
* _map.put("qty", "3");
*
* @param key templete caller
* @return
*/
protected abstract JSONArray getContents(String key);
protected String replace(String template,Map<String,String> map) {
Set<String> set = map.keySet();
Iterator<String> iter = set.iterator();
String key = null;
String value = null;
while(iter.hasNext()) {
key = iter.next().toString();
value = map.get(key);
template = template.replaceAll("%"+key+"%", value);
}
//return "<a href='http://youtube.com'>"+template+"</a>";
return template;
}
protected Map<String, String> getHeaders() {
return headers;
}
}
// Util.java
public class Util {
public static String send(String method, String url, Map<String, String> headers, String content) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders httpHeaders = new HttpHeaders();
// Set headers
headers.forEach(httpHeaders::set);
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// Set HTTP method
HttpMethod httpMethod = HttpMethod.valueOf(method);
// Create HttpEntity with headers and body
HttpEntity<String> entity = new HttpEntity<>(content, httpHeaders);
// Send request and get response
ResponseEntity<String> response = restTemplate.exchange(url, httpMethod, entity, String.class);
// Return response body
return response.getBody();
}
}
// Telegram4Mart
@Component
public class Telegram4Mart extends MyTelegramBot {
@Autowired
public Telegram4CityMart(@Value("${telegram.bot.prefix}") String prefix,
@Value("${telegram.bot.token}") String botToken,
@Value("${telegram.chat.id}") String chatId) {
super(prefix, botToken, chatId);
}
protected String getTemplate(String key) {
String template = "";
if(key.equalsIgnoreCase("outofstock")){
//template = "[재고없음] vendor: %vendor% , item_name : %item_name%";
template = "[Out Of Stock] vendor: %vendor% , item_name : %item_name%";
//template = reader.getProperty(prefix + "_outofstock_template");
// gaza_outofstock_template=[재고없음] vendor: %vendor% , item_name : %item_name%
}
else if(key.equalsIgnoreCase("safetystock")){
//template = "[물품주문필요] amount : %amount% php , ordered by: %orderer%";
template = "[Need to order Item] amount : %amount% php , ordered by: %orderer%";
}
else if(key.equalsIgnoreCase("ordered")){
//template = "[주문접수] amount : %amount% php , ordered by: %orderer%";
template = "[Order received] amount : %amount% php , ordered by: %orderer%";
}
// 배송출발
// 배송도착
// 정산개시
// 정산종료
return template;
}
@Override
protected void post(String key, JSONArray contents) {
// TODO Auto-generated method stub
if(key.equalsIgnoreCase("outofstock")){
// update query
// remote.updateUpdate(~)
// 언제 몇 건이 통보되었는지 출력
System.out.println("updated for out of stock");
}
else if(key.equalsIgnoreCase("safetystock")){
// update query
// remote.updateUpdate(~)
// 언제 몇 건이 통보되었는지 출력
Date date = new Date();
//System.out.printf("%tA, %tB %tY %n", date, date, date);
//System.out.printf("updated for safestock at hours: %tH, minutes: %tM, seconds: %tS %n", date, date, date);
}
else if(key.equalsIgnoreCase("ordered")){
// update query
// remote.updateUpdate(~)
// 언제 몇 건이 통보되었는지 출력
Date date = new Date();
System.out.printf("updated for safestock at hours: %tH, minutes: %tM, seconds: %tS %n", date, date, date);
}
}
@Override
protected String getATag(String key, Map<String, String> content) {
String aTag = "";
if(key.equalsIgnoreCase("outofstock")){
aTag="<a href='http://citymart.joshi.co.kr/shop/item.php?it_id=" + content.get("it_id") + "'>";
}
else if(key.equalsIgnoreCase("safetystock")){
aTag="<a href='http://citymart.joshi.co.kr/shop/item.php?it_id=" + content.get("it_id") + "'>";
}
else if(key.equalsIgnoreCase("ordered")){
aTag = "<a href='http://citymart.joshi.co.kr/adm/shop_admin/orderform.php?od_id=" + content.get("od_id") + "'>";
}
return aTag;
}
}
위의 코드들이 telegram api와 통신할 부분이고 아래는 Spring Batch에서 위에서 정의한 메서드들을 활용해 job에 넣고 @Scheduled 어노테이션을 사용하여 1초에 한번씩 테이블을 확인해서 전송해야할 메시지가 있는지 체크하는 코드이다.
//TelegramJob.java
@Configuration
@RequiredArgsConstructor
public class TelegramJob {
private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
private final Telegram4CityMart telegramBot;
private final TelegramMsgRepository telegramMsgRepository;
private final JSONParser jsonParser = new JSONParser();
@Bean(name = "myTelegramJob")
public Job myTelegramJob() {
return new JobBuilder("telegramJob", jobRepository)
.incrementer(new RunIdIncrementer())
.start(sendPendingMessagesStep())
.build();
}
@Bean
public Step sendPendingMessagesStep() {
return new StepBuilder("sendPendingMessagesStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
List<TelegramMsg> pendingMessages = telegramMsgRepository.findByIsUpdatedFalse();
for (TelegramMsg msg : pendingMessages) {
try {
JSONObject jsonObject = (JSONObject) jsonParser.parse(msg.getMsgInfo());
JSONArray jsonArray = new JSONArray();
jsonArray.add(jsonObject);
telegramBot.send(msg.getKey(), jsonArray);
msg.setIsUpdated(true);
telegramMsgRepository.save(msg);
} catch (ParseException e) {
// JSON 파싱 에러 처리
e.printStackTrace();
}
}
return RepeatStatus.FINISHED;
}, transactionManager)
.build();
}
}
// TelegramJobScheduler
@Component
@Slf4j
public class TelegramJobScheduler {
private final JobLauncher jobLauncher;
private final Job telegramJob;
public TelegramJobScheduler(
JobLauncher jobLauncher,
@Qualifier("myTelegramJob") Job telegramJob) {
this.jobLauncher = jobLauncher;
this.telegramJob = telegramJob;
}
@Scheduled(fixedDelay = 1000) // 1초마다 실행
public void runTelegramJob() {
runJob(telegramJob, "telegram");
}
private void runJob(Job job, String jobName) {
try {
JobParameters jobParameters = new JobParametersBuilder()
.addLong("time", System.currentTimeMillis())
.toJobParameters();
jobLauncher.run(job, jobParameters);
} catch (Exception e) {
log.error("Error occurred while running {} job: ", jobName, e);
}
}
}