πŸ€– Admin Main Dashboard μŠ¬λž™ 봇 μ œμž‘ κ³Όμ • 정리

κ±μ€μ„œΒ·2022λ…„ 2μ›” 7일
2
post-thumbnail

πŸ’‘ 2021λ…„ 1μ›”, 사내 λ©”μ‹ μ €λ‘œ μ‚¬μš©ν•˜λŠ” μŠ¬λž™(Slack)의 봇(Bot)을 μ‚¬μš©ν•˜μ—¬ μ‹ μ„  λ§ˆμΌ“ μ‹œμŠ€ν…œκ³Ό μ—°λ™λ˜λŠ” κΈ°λŠ₯을 μΆ”κ°€ν•˜λŠ” 것을 싱크 νƒ€μž„ 과제둜 λ°›κ²Œ λ˜μ–΄ 과정에 λŒ€ν•΄ μ •λ¦¬ν–ˆμŠ΅λ‹ˆλ‹€.

아이디어 λ„μΆœ 및 μ„ μ •


κΈ°μ‘΄μ—λŠ” λ”°λ‘œ 봇을 μ‚¬μš©ν•˜μ§€ μ•Šκ³  github, notion λ“±κ³Ό μ—°λ™ν•˜μ—¬ μŠ¬λž™μ„ μ‚¬μš©ν•˜κ³  μžˆμ—ˆκΈ° λ•Œλ¬Έμ—, λ‹€λ₯Έ μ‚¬λžŒλ“€μ€ μŠ¬λž™λ΄‡μ„ μ–΄λ–»κ²Œ ν™œμš©ν•˜λŠ”μ§€ μ°Έκ³ ν•˜λ €κ³  λ¦¬μ„œμΉ˜λΆ€ν„° ν•΄λ³΄μ•˜λ‹€. 개인이 μž‘μ„±ν•œ 글이 λŒ€λΆ€λΆ„μ΄μ—ˆλŠ”λ° λ‹€ν–‰νžˆ λ§ˆμΌ“μ»¬λ¦¬μ™€ μŠ€ν¬μΉ΄μ—μ„œ κΈ°μ—… λ‚΄μ—μ„œ μ‚¬μš©ν•˜λŠ” μŠ¬λž™ 봇에 λŒ€ν•œ 글을 기술 λΈ”λ‘œκ·Έμ— μ μ–΄λ‘¬μ„œ κ·Έ 뢀뢄을 많이 μ°Έκ³ ν–ˆλ‹€.

λ§ˆμΌ“μ»¬λ¦¬μ—μ„œλŠ” μ‹ μ„ μ‹ν’ˆ λ°°λ‹¬μ΄λΌλŠ” λ™μΌν•œ ν‚€μ›Œλ“œλ₯Ό 가진 νšŒμ‚¬λΌ 글에 μž‘μ„±λœ 터미널 μž…κ³  μŠ€μΊ” 였λ₯˜ μ•Œλ¦Ό λ΄‡μ΄λΌλŠ” 아이디어가 인상 κΉŠμ—ˆμœΌλ‚˜, μžμ‚¬μ—μ„œλŠ” DCμ—μ„œ μŠ€μΊ” μ΄μŠˆκ°€ μžˆμ„ λ•Œ μ–΄λ–€ λ°©μ‹μœΌλ‘œ μ²˜λ¦¬ν•˜λŠ”μ§€μ— λŒ€ν•œ ν”„λ‘œμ„ΈμŠ€λ₯Ό μ •ν™•νžˆ νŒŒμ•…ν•˜μ§€ λͺ»ν–ˆλ˜ λ•ŒλΌμ„œ νŒ¨μŠ€ν–ˆλ‹€.

결둠적으둜 μ–΄λ“œλ―Ό λ©”μΈνŽ˜μ΄μ§€ λŒ€μ‹œλ³΄λ“œμ˜ 데이터λ₯Ό 좜λ ₯ν•˜λŠ” μŠ¬λž™λ΄‡μ„ λ§Œλ“€μ–΄λ³΄κΈ°λ‘œ κ²°μ •ν–ˆλ‹€.

μŠ¬λž™ μ•±(봇) λ§Œλ“€κΈ°


κΈ°μ‘΄ μŠ¬λž™μ€ 채널에 μ‚¬μš©μžμ™€ λ™λ“±ν•œ μœ„μΉ˜λ‘œ 봇이 μΆ”κ°€λ˜λŠ” λ°©μ‹μ΄μ—ˆμœΌλ‚˜, μ§€κΈˆμ˜ μŠ¬λž™μ€ 채널에 μ‚¬μš©μžμ™€ 앱을 λΆ„λ¦¬ν•˜μ—¬ 채널에 앱을 μ„€μΉ˜ν•˜λŠ” λ°©μ‹μœΌλ‘œ λ°”λ€Œμ—ˆλ‹€. λ”°λΌμ„œ (봇 = μ•±)μ΄λΌλŠ” 점을 μ°Έκ³ ν•΄μ•Ό ν•œλ‹€.

μ›Ή μ‚¬μ΄νŠΈ μ„€μ •

  1. https://api.slack.com/apps μ ‘μ†ν•΄μ„œ Create New App 클릭

  2. App Nameκ³Ό 앱을 μ–΄λ–€ Workspace에 좔가할지 μ •ν•˜κ³  Create New App 클릭

  3. Settings > Basic Information > Display Information
    μŠ¬λž™μ—μ„œ λ³΄μ—¬μ§ˆ μ•± 이름, μ•„μ΄μ½˜ 이미지(512px x 512px ~ 2000px x 2000px), μ•±μ˜ λ°°κ²½ 색을 μž…λ ₯

  4. Settings > Basic Information > Add features and functionality
    ν•„μš” μ‹œ μ‚¬μš©μžμ˜ ν˜ΈμΆœμ„ ν†΅ν•΄μ„œ λŒ€μ‹œλ³΄λ“œμ˜ 데이터λ₯Ό λ‚˜νƒ€λ‚΄λŠ” 게 이 μ•±μ˜ λͺ©μ μ΄μ—ˆκΈ° λ•Œλ¬Έμ—, Slash Commandsλ₯Ό ν†΅ν•΄μ„œ 봇을 κ΅¬ν˜„ν•˜κΈ°λ‘œ κ²°μ •ν–ˆλ‹€.

    Incoming Webhooks μ™ΈλΆ€ μ†ŒμŠ€μ—μ„œ μŠ¬λž™μœΌλ‘œ μš”μ²­ 전솑(봇 μœ μ € μΆ”κ°€ ν•„μˆ˜)
    Interactive Components λ²„νŠΌ, 메뉴 λ“±μ˜ μš”μ†Œλ₯Ό μΆ”κ°€ν•΄μ„œ μŠ¬λž™κ³Ό μƒν˜Έμž‘μš©
    Slash Commands [/ + λͺ…λ Ήμ–΄]λ₯Ό μ‚¬μš©ν•˜μ—¬ μŠ¬λž™κ³Ό μƒν˜Έμž‘μš©(봇 μœ μ € μΆ”κ°€ ν•„μˆ˜)
    Event Subscriptions μŠ¬λž™μ—μ„œ νŠΉμ • ν™œλ™μ΄ 일어났을 λ•Œ μ•±μœΌλ‘œ μ•Œλ¦Ό 전솑
    Bots μ‚¬μš©μžκ°€ 채널과 λŒ€ν™”λ₯Ό 톡해 μ•±κ³Ό μƒν˜Έμž‘μš©
    Permissions 앱이 μŠ¬λž™ API와 μƒν˜Έμž‘μš© ν•  수 μžˆλ„λ‘ κΆŒν•œ ꡬ성

  5. Features > Slash Commands
    Create New Command둜 λͺ…령을 μΆ”κ°€ν•œλ‹€.
    β†’ 사진에 λ‚˜μ˜¨ Request URL은 ν•˜λ‹¨μ˜ [API μ œμž‘] κ³Όμ • μ°Έκ³ 

API μ œμž‘

  1. 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에 μž…λ ₯ν•΄μ£Όμ—ˆλ‹€.

  2. λŒ€μ‹œλ³΄λ“œμ˜ λ°μ΄ν„°λŠ” ν˜„μž¬ λ£¨λΉ„μ—μ„œ κ°€κ³΅ν•˜μ—¬ ν‘œμ‹œν•΄μ£Όκ³  있기 λ•Œλ¬Έμ— λ£¨λΉ„μ—μ„œ 데이터 쑰회 apiλ₯Ό μΆ”κ°€ν•œ ν›„ μžλ°”μ—μ„œ ν˜ΈμΆœν•˜μ—¬ μŠ¬λž™κ³Ό μ—°λ™ν•˜κΈ°λ‘œ ν–ˆλ‹€. λ‹¨μˆœνžˆ 컨트둀러λ₯Ό μΆ”κ°€ν•΄μ£Όκ³  routes.rb에 μΆ”κ°€ν•˜λ©΄ λ˜λŠ” μž‘μ—…μΈλ° μ–΄μ©Œλ‹€ λ³΄λ‹ˆ λ£¨λΉ„κ΄€λ¦¬μžμΈ rvmκ³Ό rbenvκ°€ κΌ¬μ—¬λ²„λ €μ„œ EGλ‹˜μ΄ 해결에 도움을 μ£Όμ…¨λ‹€. πŸ™‡β€β™€οΈ

  • Ruby
    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
  • Java
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λ…„ μ „ μž‘μ„±ν•΄λ‘” κΈ€μ΄λΌμ„œ μœ νš¨ν•˜μ§€ μ•Šμ€ 뢀뢄이 μžˆμ„ 수 있고, μ½”λ“œλ„ λ”λŸ½μ§€λ§Œ... 기둝이라고 μƒκ°ν•˜λ‹ˆ λΏŒλ“―ν•˜λ‹€. μ•žμœΌλ‘œ 더 λ°œμ „λœ λͺ¨μŠ΅μ„ 기둝으둜 남길 수 μžˆλ„λ‘ λ…Έλ ₯해야지 (ΰΈ‡ β€’Μ€_‒́)ΰΈ‡βœ§

참고자료


profile
끄적끄적

0개의 λŒ“κΈ€