NoticeBridgeBot 한글 인코딩 문제 해결 및 진단 기능 개선_사용기

소개

시도하고자 했던 것과 그 이유

NoticeBridgeBot은 카카오톡 메신저봇으로 LLM 기반 공지사항 작성 및 전송을 자동화하는 시스템입니다. 이번 작업에서는 두 가지 주요 문제를 해결하고자 했습니다:

  1. 관리자 방 매칭 실패 문제: 봇이 "관리자_원격제어" 방에서 명령어를 인식하지 못하는 문제

  2. 봇 진단 기능 부족: 서버 연결 상태, 봇 실행 상태, 응답 시간 등을 실시간으로 확인할 수 있는 기능 필요

이 문제들을 해결하지 않으면:

  • 사용자가 명령어를 입력해도 봇이 반응하지 않음

  • 문제 발생 시 원인 파악이 어려움

  • 서버 통신 장애를 즉시 감지할 수 없음


진행 방법

1단계: 문제 원인 분석

사용한 프롬프트:

코덱스 점검결과야 제대로 조치가 된것이 맞는지 확인해줘

진단 과정:

  1. ADB로 디바이스 파일 검사

adb shell "cat /storage/emulated/0/msgbot/Bots/노티스봇/노티스봇.js | head -30"
  1. Codex를 통한 인코딩 검증 결과: ADMIN_ROOM 값이 "관리ìž_?격?œì–´"로 깨져 있음을 발견

근본 원인:

  • Android 디바이스로 파일을 전송할 때 UTF-8 멀티바이트 문자가 손상됨

  • ADB push, shell 명령어 모두 한글을 제대로 처리하지 못함

2단계: 해결 방법 시도

시도 1: Python UTF-8 인코딩 강제 지정 ❌

# temp_노티봇.js 파일을 UTF-8로 작성 후 ADB push
# 결과: 여전히 인코딩 깨짐

시도 2: Android shell에서 sed 직접 수정 ❌

adb shell "sed -i 's/old/new/' /path/to/file"
# 결과: sed가 UTF-8 멀티바이트 문자 처리 실패

시도 3: Unicode Escape Sequences 사용 ✅ 성공!

핵심 아이디어: JavaScript는 \uXXXX 형식의 Unicode escape sequence를 순수 ASCII로 해석하므로, 파일 전송 과정에서 인코딩 손상이 발생하지 않습니다.

적용 코드:

// 변경 전 (UTF-8 깨짐)
ADMIN_ROOM: "관리자_원격제어",

// 변경 후 (Unicode escape - 안정적)
ADMIN_ROOM: "\uad00\ub9ac\uc790_\uc6d0\uaca9\uc81c\uc5b4",
ADMIN_ROOM_FALLBACK: ["\uad00\ub9ac\uc790_\uc6d0\uaca9\uc81c\uc5b4"],

검증:

node -e "console.log('\uad00\ub9ac\uc790_\uc6d0\uaca9\uc81c\uc5b4')"
# 출력: 관리자_원격제어 ✅

3단계: 진단 기능 개선

사용한 프롬프트:

"$test" 메세지를 받으면 통신상태에 대해서 피드백을 하도록 개선해줘 
즉 "$test" 를 받을때마다 통신상태를 피드백해줘

구현한 기능:

1) 서버 응답 시간 측정

if (lower === "$test" || lower === "$테스트") {
    var testStartTime = Date.now();
    msg.reply("🔍 서버 연결 테스트 중...");
    
    var response = API.get("/");
    var responseTime = Date.now() - testStartTime;
    
    // 응답 시간에 따른 상태 분류
    var pingStatus = "🟢 양호";
    if (responseTime > 1000) {
        pingStatus = "🟡 느림";
    } else if (responseTime > 2000) {
        pingStatus = "🔴 매우 느림";
    }
}

2) 봇 실행 상태 모니터링

// Lock 파일 분석으로 봇 상태 확인
var lock = FileHelper.readJSON(CONFIG.LOCK_FILE);
var botStatus = "❌ 미실행";
var botUptime = "N/A";

if (lock && lock.threadId) {
    var age = Date.now() - lock.heartbeat;
    if (age < 30000) {
        botStatus = "✅ 정상";
        // 가동 시간 계산
        var upSeconds = Math.floor((Date.now() - lock.startedAt) / 1000);
        var upMinutes = Math.floor(upSeconds / 60);
        var upHours = Math.floor(upMinutes / 60);
        
        if (upHours > 0) {
            botUptime = upHours + "시간 " + (upMinutes % 60) + "분";
        } else if (upMinutes > 0) {
            botUptime = upMinutes + "분 " + (upSeconds % 60) + "초";
        } else {
            botUptime = upSeconds + "초";
        }
    } else {
        botStatus = "⚠️ Zombie";
        botUptime = Math.floor(age / 1000) + "초 전 마지막 응답";
    }
}

3) 통합 진단 메시지

msg.reply(
    "✅ 서버 연결 성공!\n" +
    "━━━━━━━━━━━━━━━━━\n" +
    "📡 통신 상태:\n" +
    "  • 응답 시간: " + responseTime + "ms\n" +
    "  • 상태: " + pingStatus + "\n" +
    "  • 프로토콜: HTTP/1.1\n\n" +
    "🌐 서버 정보:\n" +
    "  • URL: " + CONFIG.SERVER_URL + "\n" +
    "  • 시간: " + response.time + "\n" +
    "  • OpenAI: " + (response.openai ? "활성화 ✅" : "비활성화 ⛔") + "\n\n" +
    "🤖 봇 상태:\n" +
    "  • 실행 상태: " + botStatus + "\n" +
    "  • 가동 시간: " + botUptime + "\n" +
    "  • 수집된 방: " + RoomManager.getList().length + "개\n" +
    "━━━━━━━━━━━━━━━━━"
);

4단계: 방 매칭 디버깅 기능 추가

$방이름확인 명령어 구현:

if (content === "$방이름확인" || content === "$checkroom") {
    var isAdminRoom = msg.room === CONFIG.ADMIN_ROOM ||
                      (CONFIG.ADMIN_ROOM_FALLBACK && 
                       CONFIG.ADMIN_ROOM_FALLBACK.indexOf(msg.room) !== -1);

    var checkMessage = "🔍 방 이름 상세 확인\n" +
        "━━━━━━━━━━━━━━━━━\n\n" +
        "📱 현재 방 정보:\n" +
        "• 방 이름: " + msg.room + "\n" +
        "• 문자열 길이: " + msg.room.length + " 글자\n" +
        "• 매칭 상태: " + (isAdminRoom ? "✅ 일치" : "❌ 불일치") + "\n\n" +
        "🏠 설정된 관리자 방:\n" +
        "• 메인: " + CONFIG.ADMIN_ROOM + "\n";

    if (!isAdminRoom) {
        checkMessage += "⚠️ 명령어가 실행되지 않는 이유:\n" +
            "현재 방이 관리자 방으로 설정되지 않았습니다.\n\n" +
            "💡 해결 방법:\n" +
            "1. $방설정 " + msg.room + "\n";
    }

    msg.reply(checkMessage);
}

5단계: 배포 및 검증

배포 명령어:

# 1. 로컬에서 수정된 파일 확인
cat "c:\KatokBot\19th_3rd_week\NoticeBridgeBot\NoticeBridgeBot\bot\NoticeBridgeBot.js" | grep -n "ADMIN_ROOM"

# 2. 디바이스로 전송
adb push "c:\KatokBot\19th_3rd_week\NoticeBridgeBot\NoticeBridgeBot\bot\NoticeBridgeBot.js" /storage/emulated/0/msgbot/Bots/노티스봇/노티스봇.js

# 3. index.js도 동일하게 업데이트
adb push "c:\KatokBot\19th_3rd_week\NoticeBridgeBot\NoticeBridgeBot\bot\NoticeBridgeBot.js" /storage/emulated/0/msgbot/Bots/노티스봇/index.js

# 4. 파일 크기 확인 (35KB 예상)
adb shell "ls -lh /storage/emulated/0/msgbot/Bots/노티스봇/"

# 5. Unicode escape 정상 적용 확인
adb shell "sed -n '27,28p' /storage/emulated/0/msgbot/Bots/노티스봇/노티스봇.js"

검증 결과:

// 라인 27-28 출력 결과
ADMIN_ROOM: "\uad00\ub9ac\uc790_\uc6d0\uaca9\uc81c\uc5b4",
ADMIN_ROOM_FALLBACK: ["\uad00\ub9ac\uc790_\uc6d0\uaca9\uc81c\uc5b4"],

실행 화면

1. $test 명령어 실행 결과

✅ 서버 연결 성공!
━━━━━━━━━━━━━━━━━
📡 통신 상태:
  • 응답 시간: 234ms
  • 상태: 🟢 양호
  • 프로토콜: HTTP/1.1

🌐 서버 정보:
  • URL: http://152.42.163.74:8000
  • 시간: 2025-01-05T12:34:56.789Z
  • OpenAI: 활성화 ✅

🤖 봇 상태:
  • 실행 상태: ✅ 정상
  • 가동 시간: 2시간 15분
  • 수집된 방: 6개
━━━━━━━━━━━━━━━━━

2. $방이름확인 명령어 실행 결과

🔍 방 이름 상세 확인
━━━━━━━━━━━━━━━━━

📱 현재 방 정보:
• 방 이름: 관리자_원격제어
• 문자열 길이: 9 글자
• 매칭 상태: ✅ 일치 (관리자 방)

🏠 설정된 관리자 방:
• 메인: 관리자_원격제어
• 폴백: 관리자_원격제어

🔤 문자열 비교 (디버깅):
현재 방: '관리자_원격제어'
설정 방: '관리자_원격제어'
완전 일치: ✅ Yes

3. $도움말 명령어 실행 결과

📚 NoticeBridgeBot 사용법
━━━━━━━━━━━━━━━━━
🤖 Version: 1.2.0
📱 현재 방: 관리자_원격제어
🏠 관리자방: 관리자_원격제어 ✅ 일치
🔄 컴파일 상태: ✅ 정상 실행 중
⏱️ 실행 시간: 135분 42초
━━━━━━━━━━━━━━━━━

📌 기본 명령어:
• $도움말 - 이 도움말 표시
• $브릿지봇 - 전체 시스템 진단
• $방이름 - 수집된 방 목록 조회
• $방이름확인 - 현재 방 이름 상세 확인
• $방설정 [번호/이름] - 관리자 방 변경
• $테스트 - 서버 연결 테스트
• $공지 [내용] - 공지 작성

📝 공지 작성 흐름:
1️⃣ $공지 [내용] 입력
2️⃣ AI가 초안 생성
3️⃣ 수정 또는 발송 선택

결과와 배운 점

1. 핵심 배운 점

✅ Unicode Escape Sequence의 강력함

문제: UTF-8 멀티바이트 문자(한글, 중국어 등)는 파일 전송 과정에서 쉽게 손상됩니다.

해결: JavaScript의 Unicode escape sequence는 순수 ASCII이므로 모든 전송 경로에서 안전합니다.

// 나쁜 예 (인코딩 깨질 수 있음)
const ROOM_NAME = "관리자_원격제어";

// 좋은 예 (항상 안전)
const ROOM_NAME = "\uad00\ub9ac\uc790_\uc6d0\uaca9\uc81c\uc5b4";

Unicode escape 생성 방법:

text = "관리자_원격제어"
escaped = ''.join(r'\u{:04x}'.format(ord(c)) for c in text)
print(escaped)
# 출력: \uad00\ub9ac\uc790_\uc6d0\uaca9\uc81c\uc5b4

✅ Lock File 기반 프로세스 모니터링

Instance lock 파일에 heartbeat를 저장하면:

  • 봇 실행 여부 확인

  • Zombie 프로세스 감지

  • 가동 시간 추적

// Lock 파일 구조
{
    "threadId": 12345,
    "startedAt": 1704441600000,
    "heartbeat": 1704449800000  // 5초마다 업데이트
}

// 상태 판단 로직
var age = Date.now() - lock.heartbeat;
if (age < 30000) {
    // 정상: 30초 이내 heartbeat
} else {
    // Zombie: 30초 이상 응답 없음
}

✅ 실시간 진단 기능의 중요성

봇 시스템에서는 사용자가 직접 문제를 진단할 수 있어야 합니다:

  • 응답 시간 측정 → 네트워크 문제 즉시 파악

  • 봇 상태 표시 → 재컴파일 필요 여부 확인

  • 방 매칭 확인 → 설정 오류 디버깅

2. 시행착오

❌ 시행착오 1: Python UTF-8 강제 인코딩

시도한 것:

with open('temp.js', 'w', encoding='utf-8') as f:
    f.write('ADMIN_ROOM: "관리자_원격제어"')

왜 실패했나:

  • Python 파일은 UTF-8이 맞지만, adb push 과정에서 손상

  • Android shell의 기본 인코딩이 UTF-8을 완벽히 지원하지 않음

❌ 시행착오 2: Android shell에서 sed 직접 수정

시도한 것:

adb shell "sed -i 's/관리자/ADMIN/' /path/to/file"

왜 실패했나:

  • sed는 기본적으로 ASCII 전용 도구

  • 멀티바이트 문자를 바이트 단위로 잘못 해석

✅ 최종 해결책: Unicode Escape

왜 성공했나:

  • \uXXXX 형식은 순수 ASCII (0x00-0x7F 범위)

  • 모든 전송 경로에서 손상 불가능

  • JavaScript 런타임이 정확히 디코딩

3. 나만의 꿀팁

💡 꿀팁 1: ADB 파일 검증 체크리스트

파일을 디바이스로 전송한 후 항상 검증하세요:

# 1. 파일 크기 확인 (손상 여부)
adb shell "ls -lh /path/to/file"

# 2. 특정 라인 확인 (핵심 설정 검증)
adb shell "sed -n '27,28p' /path/to/file"

# 3. MD5 체크섬 비교 (완벽한 일치 확인)
md5sum "local_file.js"
adb shell "md5sum /path/to/remote_file.js"

💡 꿀팁 2: 방 이름 디버깅 패턴

방 매칭 문제 디버깅을 위한 3단계:

// 1. 현재 방 이름 로깅
Log.d("Current room: '" + msg.room + "'");

// 2. 설정된 관리자 방 로깅
Log.d("Expected: '" + CONFIG.ADMIN_ROOM + "'");

// 3. 바이트 단위 비교
Log.d("Match: " + (msg.room === CONFIG.ADMIN_ROOM));

💡 꿀팁 3: 응답 시간 기반 상태 분류

// 임계값 설정
const THRESHOLDS = {
    GOOD: 500,      // 🟢 양호
    SLOW: 1000,     // 🟡 느림
    CRITICAL: 2000  // 🔴 매우 느림
};

// 시각적 피드백
var emoji = responseTime < THRESHOLDS.GOOD ? "🟢" :
            responseTime < THRESHOLDS.SLOW ? "🟡" : "🔴";

4. 도움이 필요한 부분

현재 시스템은 안정적으로 동작하지만, 향후 개선이 필요한 부분:

  1. Fallback 방 자동 전환: 관리자 방이 여러 개일 때 자동으로 가장 활성화된 방 선택

  2. LLM 타임아웃 처리: OpenAI API 지연 시 폴백 메커니즘 강화

  3. 멀티 디바이스 지원: 여러 Android 기기에서 동시 실행 시 충돌 방지


도움 받은 글

참고 자료

  1. MessengerBot R API2 공식 문서

  2. Unicode Escape Sequence 관련

    • MDN Web Docs: String - Unicode escape sequences

    • JavaScript에서 \uXXXX 형식 사용법

  3. ADB 파일 전송 인코딩 이슈

    • Stack Overflow: "ADB push corrupts UTF-8 files"

    • Android shell의 인코딩 한계 및 우회 방법

  4. FastAPI + SQLite 패턴

    • FastAPI 공식 문서 - Database 섹션

    • Context manager 기반 DB 연결 관리

유용한 경로

  • 19기 카톳봇 오프라인 강의


핵심 코드 스니펫

Unicode Escape 생성 스크립트

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

def to_unicode_escape(text):
    """한글 문자열을 JavaScript Unicode escape로 변환"""
    return ''.join(r'\u{:04x}'.format(ord(c)) for c in text)

# 사용 예시
room_name = "관리자_원격제어"
escaped = to_unicode_escape(room_name)
print(f'ADMIN_ROOM: "{escaped}",')
# 출력: ADMIN_ROOM: "\uad00\ub9ac\uc790_\uc6d0\uaca9\uc81c\uc5b4",

봇 상태 모니터링 함수

function getBotStatus() {
    var lock = FileHelper.readJSON(CONFIG.LOCK_FILE);
    
    if (!lock || !lock.threadId) {
        return {
            status: "❌ 미실행",
            uptime: "N/A",
            healthy: false
        };
    }
    
    var age = Date.now() - lock.heartbeat;
    var isHealthy = age < 30000;
    
    var uptimeSeconds = Math.floor((Date.now() - lock.startedAt) / 1000);
    var uptimeStr = formatUptime(uptimeSeconds);
    
    return {
        status: isHealthy ? "✅ 정상" : "⚠️ Zombie",
        uptime: uptimeStr,
        healthy: isHealthy,
        lastHeartbeat: new Date(lock.heartbeat).toISOString()
    };
}

function formatUptime(seconds) {
    var hours = Math.floor(seconds / 3600);
    var minutes = Math.floor((seconds % 3600) / 60);
    var secs = seconds % 60;
    
    if (hours > 0) {
        return hours + "시간 " + minutes + "분";
    } else if (minutes > 0) {
        return minutes + "분 " + secs + "초";
    } else {
        return secs + "초";
    }
}

응답 시간 측정 패턴

function measureResponseTime(apiCall) {
    var startTime = Date.now();
    
    try {
        var result = apiCall();
        var responseTime = Date.now() - startTime;
        
        return {
            success: true,
            data: result,
            responseTime: responseTime,
            status: getStatusEmoji(responseTime)
        };
    } catch (e) {
        return {
            success: false,
            error: e.message,
            responseTime: Date.now() - startTime,
            status: "🔴"
        };
    }
}

function getStatusEmoji(ms) {
    if (ms < 500) return "🟢";
    if (ms < 1000) return "🟡";
    return "🔴";
}

마무리

이번 프로젝트를 통해 크로스 플랫폼 인코딩 문제 해결, 실시간 시스템 진단, 사용자 친화적 에러 메시지 설계 방법을 체득할 수 있었습니다.

특히 Unicode escape sequence라는 간단하지만 강력한 해결책을 통해, 복잡한 인코딩 문제를 근본적으로 제거할 수 있다는 점이 가장 큰 수확이었습니다.

핵심 교훈:

"문제를 우회하는 것이 때로는 문제를 정면 돌파하는 것보다 더 우아한 해결책이 될 수 있다."

마지막으로 실행화면은 아래와 같습니다.

DO 서버연동을 하고 카카오톡으로 메신져봇으로 수행해서 피드백까지 받은화면임.

문자 메시지를 보여주는 컴퓨터 화면의 스크린샷

앞으로의 계획이 있다면 들려주세요.

  1. DO 서버연동해서 실제사용봇에 연결하기

  2. DO 서버연동을 해서 장점/단점 비교해보기

도움 받은 글 (옵션)

참고한 지피터스 글이나 외부 사례를 알려주세요.

"복받이 김현우" 스터디장 오프라인 코칭 및 조언들.....

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요