π‘ 2021λ 1μ, μ¬λ΄ λ©μ μ λ‘ μ¬μ©νλ μ¬λ(Slack)μ λ΄(Bot)μ μ¬μ©νμ¬ μ μ λ§μΌ μμ€ν κ³Ό μ°λλλ κΈ°λ₯μ μΆκ°νλ κ²μ μ±ν¬ νμ κ³Όμ λ‘ λ°κ² λμ΄ κ³Όμ μ λν΄ μ 리νμ΅λλ€.
κΈ°μ‘΄μλ λ°λ‘ λ΄μ μ¬μ©νμ§ μκ³ github, notion λ±κ³Ό μ°λνμ¬ μ¬λμ μ¬μ©νκ³ μμκΈ° λλ¬Έμ, λ€λ₯Έ μ¬λλ€μ μ¬λλ΄μ μ΄λ»κ² νμ©νλμ§ μ°Έκ³ νλ €κ³ λ¦¬μμΉλΆν° ν΄λ³΄μλ€. κ°μΈμ΄ μμ±ν κΈμ΄ λλΆλΆμ΄μλλ° λ€νν λ§μΌμ»¬λ¦¬μ μ€ν¬μΉ΄μμ κΈ°μ λ΄μμ μ¬μ©νλ μ¬λ λ΄μ λν κΈμ κΈ°μ λΈλ‘κ·Έμ μ μ΄λ¬μ κ·Έ λΆλΆμ λ§μ΄ μ°Έκ³ νλ€.
λ§μΌμ»¬λ¦¬μμλ μ μ μν λ°°λ¬μ΄λΌλ λμΌν ν€μλλ₯Ό κ°μ§ νμ¬λΌ κΈμ μμ±λ ν°λ―Έλ μ κ³ μ€μΊ μ€λ₯ μλ¦Ό λ΄μ΄λΌλ μμ΄λμ΄κ° μΈμ κΉμμΌλ, μμ¬μμλ DCμμ μ€μΊ μ΄μκ° μμ λ μ΄λ€ λ°©μμΌλ‘ μ²λ¦¬νλμ§μ λν νλ‘μΈμ€λ₯Ό μ νν νμ νμ§ λͺ»νλ λλΌμ ν¨μ€νλ€.
κ²°λ‘ μ μΌλ‘ μ΄λλ―Ό λ©μΈνμ΄μ§ λμ보λμ λ°μ΄ν°λ₯Ό μΆλ ₯νλ μ¬λλ΄μ λ§λ€μ΄λ³΄κΈ°λ‘ κ²°μ νλ€.
κΈ°μ‘΄ μ¬λμ μ±λμ μ¬μ©μμ λλ±ν μμΉλ‘ λ΄μ΄ μΆκ°λλ λ°©μμ΄μμΌλ, μ§κΈμ μ¬λμ μ±λμ μ¬μ©μμ μ±μ λΆλ¦¬νμ¬ μ±λμ μ±μ μ€μΉνλ λ°©μμΌλ‘ λ°λμλ€. λ°λΌμ (λ΄ = μ±)μ΄λΌλ μ μ μ°Έκ³ ν΄μΌ νλ€.
https://api.slack.com/apps μ μν΄μ Create New App
ν΄λ¦
App Nameκ³Ό μ±μ μ΄λ€ Workspaceμ μΆκ°ν μ§ μ νκ³ Create New App
ν΄λ¦
Settings > Basic Information > Display Information
μ¬λμμ 보μ¬μ§ μ± μ΄λ¦, μμ΄μ½ μ΄λ―Έμ§(512px x 512px ~ 2000px x 2000px), μ±μ λ°°κ²½ μμ μ
λ ₯
Settings > Basic Information > Add features and functionality
νμ μ μ¬μ©μμ νΈμΆμ ν΅ν΄μ λμ보λμ λ°μ΄ν°λ₯Ό λνλ΄λ κ² μ΄ μ±μ λͺ©μ μ΄μκΈ° λλ¬Έμ, Slash Commandsλ₯Ό ν΅ν΄μ λ΄μ ꡬννκΈ°λ‘ κ²°μ νλ€.
Incoming Webhooks
μΈλΆ μμ€μμ μ¬λμΌλ‘ μμ² μ μ‘(λ΄ μ μ μΆκ° νμ)
Interactive Components
λ²νΌ, λ©λ΄ λ±μ μμλ₯Ό μΆκ°ν΄μ μ¬λκ³Ό μνΈμμ©
Slash Commands
[/ + λͺ
λ Ήμ΄]λ₯Ό μ¬μ©νμ¬ μ¬λκ³Ό μνΈμμ©(λ΄ μ μ μΆκ° νμ)
Event Subscriptions
μ¬λμμ νΉμ νλμ΄ μΌμ΄λ¬μ λ μ±μΌλ‘ μλ¦Ό μ μ‘
Bots
μ¬μ©μκ° μ±λκ³Ό λνλ₯Ό ν΅ν΄ μ±κ³Ό μνΈμμ©
Permissions
μ±μ΄ μ¬λ APIμ μνΈμμ© ν μ μλλ‘ κΆν ꡬμ±
Features > Slash Commands
Create New Command
λ‘ λͺ
λ Ήμ μΆκ°νλ€.
β μ¬μ§μ λμ¨ Request URLμ νλ¨μ [API μ μ] κ³Όμ μ°Έκ³
Slash λͺ λ Ήμ μΆκ°ν λ Request URLμ μ λ ₯ν΄μΌ νκΈ° λλ¬Έμ μλ° apiλΆν° μΆκ°νλ€.
μ¬λμ λͺ λ Ήμ΄κ° νΈμΆλλ©΄ μ¬λμ μ§μ ν μμ² URLλ‘ HTTP POSTλ₯Ό 보λΈλ€. μ΄ μμ²μλ λͺ λ Ήκ³Ό μ΄λ₯Ό νΈμΆν μ¬λμ μ€λͺ νλ λ°μ΄ν° νμ΄λ‘λκ° ν¬ν¨λλ€. (μ°Έκ³ λ§ν¬)
μ΄ λ μ μ‘λ ν€λμ Contet-Typeμ x-www-form-urlencodedμ΄κΈ° λλ¬Έμ 컨νΈλ‘€λ¬μμ @PostMapping μ΄λ Έν μ΄μ μ consumes μμ±(μμ νκ³ μ νλ λ°μ΄ν° ν¬λ§·μ μ μ)μ μΆκ°ν΄μ£Όμλ€. consumes μμ±μ μΆκ°ν¨μΌλ‘μ¨ μ§μ ν λ―Έλμ΄ νμ κ³Ό μΌμΉν λλ§ μμ²μ΄ λ§€μΉνλ€.
@PostMapping(λ€λ₯Έ μμ±, consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
κ°λ¨ν λ©μμ§λ₯Ό μ μ‘ν΄ apiκ° μ μμ μΌλ‘ μλνλμ§ νμΈνκΈ° μν΄ μλμ λ°μ΄ν°λ₯Ό json νμμΌλ‘ 리ν΄νλ μ½λλ₯Ό μμ±νλ€.
slackApiResponse.setResponse_type("in_channel"); // μ±λ μ 체 곡μ
slackApiResponse.setText("μλ
νμΈμ! μ΄λλ―Ό λ©μΈ λμ보λ λ°μ΄ν°λ₯Ό 보μ¬μ£Όλ λ΄ μ
λλ€.");
κ·Έλ¦¬κ³ λ‘컬μμ μ€νμ€μΈ μλ²λ₯Ό μΈλΆμμ μ κ·Όν μ μλλ‘ ngrokμ μ΄μ©ν΄ 1319ν¬νΈλ₯Ό μ΄κ³ , Request URLμ μ
λ ₯ν΄μ£Όμλ€.
λμ보λμ λ°μ΄ν°λ νμ¬ λ£¨λΉμμ κ°κ³΅νμ¬ νμν΄μ£Όκ³ μκΈ° λλ¬Έμ 루λΉμμ λ°μ΄ν° μ‘°ν apiλ₯Ό μΆκ°ν ν μλ°μμ νΈμΆνμ¬ μ¬λκ³Ό μ°λνκΈ°λ‘ νλ€. λ¨μν 컨νΈλ‘€λ¬λ₯Ό μΆκ°ν΄μ£Όκ³ routes.rbμ μΆκ°νλ©΄ λλ μμ μΈλ° μ΄μ©λ€ 보λ 루λΉκ΄λ¦¬μμΈ rvmκ³Ό rbenvκ° κΌ¬μ¬λ²λ €μ EGλμ΄ ν΄κ²°μ λμμ μ£Όμ ¨λ€. πββοΈ
class ApiSlackController < ApplicationController
def show
dashboard_data = LastMileDashboardService.work(Date.current)
@inventory_dashboard_data = InventoryDashboardService.work(Date.current)
@sold_out_dashboard_data = InventoryDashboardService.get_sold_out_summary(Date.current)
@a_day_ago = dashboard_data[:past]
@today = dashboard_data[:present]
@a_day_later = dashboard_data[:future]
@now = Time.zone.now
@dashboards_data = {
'inventory_dashboard_data' => @inventory_dashboard_data,
'sold_out_dashboard_data' => @sold_out_dashboard_data,
'a_day_ago' => @a_day_ago,
'today' => @today,
'a_day_later' => @a_day_later,
'now' => @now
}
render json: @dashboards_data
end
end
package com
import com
...
@Slf4j
@RestController
@RequestMapping("[μλ΅]")
public class SlackBotApiController {
@Autowired
SlackApiService slackApiService;
@PostMapping(value = "/dashboard", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public SlackApiResponse sendMessage(
@ModelAttribute SlackApiRequest slackApiRequest
) {
return slackApiService.getMessage(slackApiRequest);
}
}
package com
import com
...
import java.util.Map;
public interface SlackApiService {
DashboardsDto getDashboardData();
SlackApiResponse getMessage(SlackApiRequest slackApiRequest);
Map<String, String> getTextAndTitle(String inputText, DashboardsDto dto);
}
package com
import com
...
@Slf4j
@Transactional
@Service
public class SlackApiServiceImpl implements SlackApiService {
// λ£¨λΉ api
private static final String HOST_URL = "http://localhost:3000/...[μλ΅]";
// λ£¨λΉ api μ‘°ν
@Override
public DashboardsDto getDashboardData() {
HttpURLConnection connection;
ObjectMapper mapper = new ObjectMapper();
try {
URL url = new URL(HOST_URL);
connection = (HttpURLConnection)url.openConnection();
connection.setRequestMethod("GET");
int statusCode = connection.getResponseCode();
if (statusCode == HttpURLConnection.HTTP_OK) {
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder sb = new StringBuilder();
String line = "";
Map<String, Object> data = new HashMap<>();
while ((line = br.readLine()) != null) {
sb.append(line);
}
return mapper.readValue(sb.toString(), DashboardsDto.class);
} else if (statusCode == HttpURLConnection.HTTP_NOT_FOUND) {
System.out.println(statusCode + " Error");
} else {
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// μ¬λμ μ μ‘λ attachments μΆκ°ν΄μ controllerμ λ°ν
@Override
public SlackApiResponse getMessage(SlackApiRequest slackApiRequest) {
SlackApiResponse slackApiResponse = new SlackApiResponse();
if (slackApiRequest.getToken() == null) {
slackApiResponse.setText("INVALID TOKEN");
return slackApiResponse;
}
try {
Map<String, String> textAndTitle = getTextAndTitle(slackApiRequest.getText(), getDashboardData());
ArrayList<Map> fieldArrayList = new ArrayList<Map>();
Map<String, Object> field = new HashMap<>();
field.put("title", textAndTitle.get("title"));
field.put("value", textAndTitle.get("text"));
fieldArrayList.add(field);
ArrayList<Map> attachmentArrayList = new ArrayList<Map>();
Map<String, Object> attachment = new HashMap<>();
attachment.put("fields", fieldArrayList.stream().collect(Collectors.toList()));
attachment.put("color", "#aed1ff");
attachment.put("title", "μ΄λλ―Ό λ©μΈ λμ보λ");
attachment.put("title_link", "https://admin.onul-hoi.com/admin");
attachmentArrayList.add(attachment);
slackApiResponse.setResponse_type("in_channel"); // μ±λ μ 체 곡μ
slackApiResponse.setText("μ΄λλ―Ό λ©μΈ λμ보λ λ°μ΄ν°");
slackApiResponse.setAttachments(attachmentArrayList);
} catch (Exception e) {
e.printStackTrace();
}
return slackApiResponse;
}
// μ¬λμΌλ‘ μ μ‘λ λ©μμ§ ν
μ€νΈμ νμ΄ν μ€μ
public Map<String, String> getTextAndTitle(String inputText, DashboardsDto dto) {
String title="", text = "";
DecimalFormat decimalFormat = new DecimalFormat("###,###");
Map<String, String> textAndTitle = new HashMap<String, String>();
if(inputText.contains("λ§€μΆ")) {
InventoryDashboardDataDto.CurrentMonthSalesAmountBean currentMonthSalesAmount = dto.getInventoryDashboardDataDto().getCurrentMonthSalesAmountBean();
LocalTime cacheDate = ZonedDateTime.parse(currentMonthSalesAmount.getCacheDate()).toLocalTime();
title = "μ΄λ²λ¬ λ§€μΆ";
text = "λ°μ΄ν° μ§κ³:...[μλ΅]";
} else if(inputText.contains("λ°°μ‘")) {
DashboardsDto.DayBean today = dto.getTodayDto();
String lastDeliveredTime = today.getLastDeliveredTime() == null ? "-" : today.getLastDeliveredTime().toString();
title = today.getDateStr() + " λ°°μ‘νν©";
text = "λ°°μ‘건μ:...[μλ΅]";
} else if(inputText.contains("νμ ")) {
InventoryDashboardDataInInventoryDashboardDataDto inventoryData = dto.getInventoryDashboardDataDto().getInventoryDashboardDataInInventoryDashboardDataDto();
InventoryDashboardDataInInventoryDashboardDataDto.currentDataAndLastWeekDataBean currentInventoryData = inventoryData.getCurrentDataDto();
SoldOutDashboardDataDto soldOutDashboardData = dto.getSoldOutDashboardDataDto();
String totalLossRate = soldOutDashboardData.getTotalLossRate() == null ? "0.0" : dto.getSoldOutDashboardDataDto().getTotalLossRate().toString();
LocalDate targetDate = LocalDate.parse(soldOutDashboardData.getTargetDate());
title = "νμ νν©";
text = "λ°μ£Ό λ μ§...[μλ΅]"
} else {
text = "/dashboard λͺ
λ Ήμ΄μ ν¨κ» [λ§€μΆ, λ°°μ‘, νμ ] μ€ νλλ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.";
}
textAndTitle.put("title", title);
textAndTitle.put("text", text);
return textAndTitle;
}
}
1λ
μ μμ±ν΄λ κΈμ΄λΌμ μ ν¨νμ§ μμ λΆλΆμ΄ μμ μ μκ³ , μ½λλ λλ½μ§λ§... κΈ°λ‘μ΄λΌκ³ μκ°νλ λΏλ―νλ€. μμΌλ‘ λ λ°μ λ λͺ¨μ΅μ κΈ°λ‘μΌλ‘ λ¨κΈΈ μ μλλ‘ λ
Έλ ₯ν΄μΌμ§ (ΰΈ β’Μ_β’Μ)ΰΈβ§
μ¬λ λ΄ κ΄λ ¨ 리μμΉ μλ£
μ¬λλ΄, μ΄λκΉμ§ λ§λ€μ΄λ΄€λ?
Enabling interactivity with Slash Commands
Reference: Secondary message attachments
Formatting text for app surfaces
κΈ°ν 리μμΉ μλ£