카테고리: 디스코드 봇 만들기

디스코드 봇 만들기#22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제)

목차13개 항목

안녕하세요! Mojangcam입니다 😎

지난 #21편에서는 채팅을 지켜보는 cogs/automod.py를 만들었죠. 그런데 문제가 하나 있었어요. 금지어가 코드 안에 박혀 있었다는 것 😩

단어 하나 추가하려고 파일 열고, 리스트 고치고, 봇 껐다 켜고... 서버 관리자가 개발자가 아니면 아예 불가능하죠.

오늘은 그걸 명령어로 바꿉니다. /금지어추가 를 치면 추천 후보가 주르륵 뜨고, /금지어삭제 를 치면 이미 등록된 금지어만 후보로 떠서 골라 지울 수 있어요. 슬래시 명령어의 자동완성(Autocomplete) 기능이에요 ⌨️

이번 편에서 배울 것

  • choices 와 autocomplete 의 결정적인 차이 (25개의 벽!)
  • 자동완성 콜백의 정해진 시그니처 — (interaction, current)
  • 입력한 글자로 후보를 검색해서 걸러 보여주기
  • interaction.namespace 로 먼저 고른 값 읽기 (연동 자동완성)
  • 자동완성으로 금지어를 추가·삭제하고 #21 automod에 실제로 연결하기

자동완성은 한 번 만들어두면 "명령어 사용법을 아무도 안 물어보는" 마법 같은 기능이에요 ✨

🧠 choices 로는 왜 안 될까요?

app_commands.choices 는 이미 #15편에서 써봤어요. 그런데 오늘 만들 기능에는 못 씁니다. 이유가 명확해요.

  • choices 는 "고정 메뉴" 예요. 봇을 켤 때 디스코드 서버에 목록을 통째로 등록합니다. 그래서 목록이 바뀌면 명령어를 다시 동기화(sync) 해야 해요
  • autocomplete 는 "즉석 조리" 예요. 유저가 글자를 칠 때마다 디스코드가 우리 봇에게 물어보고, 봇이 그때그때 후보를 만들어 돌려줍니다

금지어 목록은 서버마다 다르고 수시로 바뀌잖아요. choices 로는 답이 없죠. 게다가 choices 는 등록 자체가 25개까지라, 금지어가 30개만 돼도 못 담습니다.

💡 자동완성도 화면에 보여줄 수 있는 후보는 25개예요. 하지만 이건 "저장할 수 있는 개수"가 아니라 "한 번에 보여줄 개수"라서, 금지어를 1000개 저장해두고 검색된 상위 25개만 보여주면 아무 문제가 없어요 🔍

📌 준비물

  • Python 3.8+
  • discord.py 2.x (2026년 8월 기준 최신은 2.7.1)
  • python-dotenv
  • #19편에서 만든 server-bot/ 폴더 — 오늘도 cogs/ 에 파일 하나 추가하고, #21의 automod.py 를 3줄만 고쳐요
  • 슬래시 명령어가 처음이라면 #19 Cog로 코드 쪼개기 편의 !sync 부분을 먼저 보고 오세요
터미널
pip install -U discord.py python-dotenv

버전 확인은 이렇게 해요.

터미널
python -c "import discord; print(discord.__version__)"

오늘은 .env 에 추가할 값이 없어요 🎉 금지어는 서버(길드)별로 JSON에 저장할 거라서, 설정 파일을 건드릴 필요가 없습니다.

📌 1단계 - 자동완성 콜백의 정해진 모양

자동완성은 콜백 함수 하나를 만들고, 명령어 파라미터에 연결해주면 끝이에요. 그런데 이 콜백은 모양이 정해져 있습니다.

Python
from typing import List

import discord
from discord import app_commands

async def fruit_autocomplete(
    interaction: discord.Interaction,
    current: str,
) -> List[app_commands.Choice[str]]:
    fruits = ["사과", "바나나", "포도", "수박"]
    return [
        app_commands.Choice(name=fruit, value=fruit)
        for fruit in fruits
    ]

@app_commands.command(name="과일")
@app_commands.autocomplete(과일=fruit_autocomplete)
async def pick_fruit(interaction: discord.Interaction, 과일: str):
    await interaction.response.send_message(f"{과일} 골랐네요!")

하나씩 뜯어볼게요 💡

  • async def : 반드시 코루틴이어야 해요. 일반 함수로 만들면 TypeError 가 납니다
  • 파라미터 2개 고정 : interaction 과 current 예요. 이름은 바꿔도 되지만 개수와 순서는 못 바꿔요. (Cog 안에 넣으면 self 가 앞에 붙어서 3개가 됩니다)
  • current : 유저가 지금 이 순간 치고 있는 글자예요. 아직 아무것도 안 쳤으면 빈 문자열 "" 입니다
  • 반환값 : Choice 객체의 리스트여야 해요. Choice(name=..., value=...) 에서 name 은 화면에 보이는 글자, value 는 실제로 명령어에 들어갈 값이에요. 둘을 다르게 줄 수도 있어요 (예: name="바보 (욕설)", value="바보")
  • @app_commands.autocomplete(과일=...) : 키워드 이름이 파라미터 이름과 똑같아야 해요. 오타 나면 TypeError: unknown parameter 가 바로 뜹니다

⚠️ 자동완성은 아무 타입에나 못 붙어요. discord.py 공식 문서 기준으로 str · int · float 만 지원합니다. discord.Member 나 discord.Role 같은 타입에 붙이면 TypeError 가 나요. 그런 건 디스코드가 알아서 멤버 목록을 띄워주니까 필요도 없고요 🙂

📌 2단계 - 검색해서 25개만 보여주기

위 코드는 후보를 전부 돌려주고 있어요. 실전에서는 이러면 안 됩니다. current 로 걸러야 해요.

Python
MAX_CHOICES = 25          # 디스코드가 보여주는 후보 최대 개수
MAX_NAME_LEN = 100        # Choice.name 글자 수 제한

def to_choices(words: List[str]) -> List[app_commands.Choice[str]]:
    return [
        app_commands.Choice(name=word[:MAX_NAME_LEN], value=word[:MAX_NAME_LEN])
        for word in words[:MAX_CHOICES]
    ]

하나씩 뜯어볼게요 💡

  • words[:MAX_CHOICES] : 25개를 넘기면 디스코드가 거부해서 자동완성이 통째로 안 뜹니다. 에러 메시지도 채팅창에 안 나오고 그냥 조용히 안 떠요. 그래서 제일 잡기 어려운 버그예요 😵 자를 때 확실히 잘라두세요
  • word[:MAX_NAME_LEN] : Choice.name 은 100자 제한이에요. 정규식 금지어는 길어질 수 있으니 미리 잘라둡니다
  • 함수로 빼둔 이유 : 자동완성 콜백을 여러 개 만들 건데, 25개·100자 자르기를 한 군데서만 하면 실수가 없어요

검색은 이렇게 합니다.

Python
matched = [w for w in words if current.lower() in w.lower()]
return to_choices(matched)
  • current.lower() in w.lower() : 대소문자 무시하고 포함되면 통과예요. "디스" 를 치면 "디스코드" 가 잡힙니다
  • 앞글자만 맞추고 싶으면 w.lower().startswith(current.lower()) 로 바꾸면 돼요

💬 3초 안에 답해야 해요. 자동완성 응답이 3초를 넘으면 디스코드가 그냥 버립니다. 그래서 콜백 안에서 무거운 DB 조회나 외부 API 호출은 피하는 게 좋아요. 오늘처럼 작은 JSON 파일을 읽는 정도는 전혀 문제없습니다 ⏱️

📌 3단계 - namespace 로 먼저 고른 값 읽기

자동완성의 진짜 재미는 여기예요. 앞에서 고른 옵션에 따라 뒤 후보가 달라지게 만들 수 있습니다.

/금지어추가 분류:광고 단어:... 처럼 쓸 때, 분류 에서 광고를 골랐으면 단어 후보도 광고 관련만 떠야 자연스럽죠.

Python
async def preset_autocomplete(self, interaction, current: str):
    category = interaction.namespace.분류      # ← 먼저 고른 값!
    pool = PRESETS.get(category, [])
    if not pool:                               # 아직 안 골랐으면 전체
        pool = [w for words in PRESETS.values() for w in words]

    already = set(load_words(interaction.guild_id or 0))
    matched = [w for w in pool if current.lower() in w.lower() and w not in already]
    return to_choices(matched)

하나씩 뜯어볼게요 💡

  • interaction.namespace : 지금 입력 중인 명령어의 다른 옵션 값들이 담긴 객체예요. namespace.분류 처럼 파라미터 이름 그대로 꺼내면 됩니다
  • 아직 안 골랐을 수도 있어요 : 유저가 단어 부터 채울 수도 있잖아요. 그때 namespace.분류 는 None 이라 PRESETS.get(None, []) 가 빈 리스트를 돌려줘요. 그래서 fallback으로 전체 목록을 넣어뒀습니다
  • w not in already : 이미 등록된 건 후보에서 빼요. 중복 추가를 애초에 막아주는 작은 배려예요 🙂
  • interaction.guild_id or 0 : DM에서 명령어를 쓰면 guild_id 가 None 이에요. 그대로 쓰면 TypeError 가 나니 기본값을 줍니다

💡 자동완성 콜백에는 명령어에 붙인 권한 체크(checks)가 적용되지 않아요. 권한 없는 사람도 후보 목록은 볼 수 있다는 뜻이에요. 진짜로 가려야 하는 정보라면 콜백 안에서 직접 권한을 확인하세요.

📌 4단계 - 금지어를 JSON에 저장하기

이제 후보를 어디서 가져올지 정할 차례예요. #14편에서 배운 JSON 방식 그대로 갑니다.

Python
WORD_FILE = Path("data/banned_words.json")

def load_all() -> Dict[str, List[str]]:
    if WORD_FILE.exists():
        with WORD_FILE.open(encoding="utf-8") as f:
            return json.load(f)
    return {}

def load_words(guild_id: int) -> List[str]:
    return load_all().get(str(guild_id), [])

def save_words(guild_id: int, words: List[str]) -> None:
    data = load_all()
    data[str(guild_id)] = words
    WORD_FILE.parent.mkdir(parents=True, exist_ok=True)
    with WORD_FILE.open("w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
  • 키가 str(guild_id) : JSON은 키를 문자열로만 저장할 수 있어요. json.dump 하면 숫자 키가 알아서 문자열로 바뀌는데, 읽을 때 숫자로 찾으면 못 찾습니다. 처음부터 문자열로 통일하는 게 안전해요
  • 서버별로 나눠 저장 : 봇을 여러 서버에 넣어도 금지어가 안 섞여요
  • ensure_ascii=False : 이거 없으면 한글이 안녕 처럼 저장돼서 파일을 열어봐도 못 읽어요

📌 5단계 - #21 automod 에 연결하기

마지막으로 #21에서 만든 automod.py 가 이 목록을 쓰게 만들어요. 딱 3군데만 고칩니다.

Python
# 1) import 추가
from cogs.wordadmin import load_words

# 2) COMPILED_BANNED 상수를 함수로 교체
def banned_patterns(guild_id: int):
    """기본 금지어 + /금지어추가 로 등록한 금지어"""
    words = BANNED_PATTERNS + load_words(guild_id)
    # re.compile 은 내부 캐시(512개)를 쓰기 때문에 매번 불러도 저렴해요
    return [re.compile(word, re.IGNORECASE) for word in words]

# 3) on_message 안에서 함수를 호출
for pattern in banned_patterns(message.guild.id):
    ...

"매번 컴파일하면 느려지지 않나요?" 하실 텐데요, re 모듈은 컴파일 결과를 512개까지 내부 캐시로 들고 있어요. 같은 패턴을 다시 컴파일하면 캐시에서 꺼내 씁니다. 실제로 재보면 20만 번 호출에 0.03초 수준이라 on_message 에서 써도 전혀 부담이 없어요 ⚡

⚠️ 금지어를 정규식으로 저장하니까, 유저가 ( 하나만 입력해도 re.error 가 날 수 있어요. 실서비스에서는 /금지어추가 에서 re.compile() 로 미리 검증하고 실패하면 안내 메시지를 주는 게 좋습니다. 아래 전체 코드는 학습용이라 이 검증은 생략했어요.

📌 전체 코드 (복붙 OK)

1) cogs/wordadmin.py

Python
# discord.py 2.x (2.7.1 기준) / Python 3.8+
# 디스코드 봇 만들기 #22 - 슬래시 명령어 자동완성(Autocomplete)

import json
from pathlib import Path
from typing import Dict, List

import discord
from discord import app_commands
from discord.ext import commands

WORD_FILE = Path("data/banned_words.json")
MAX_CHOICES = 25          # 디스코드가 보여주는 자동완성 후보 최대 개수
MAX_NAME_LEN = 100        # Choice.name 글자 수 제한

# /금지어추가 의 분류별 추천 프리셋
PRESETS: Dict[str, List[str]] = {
    "욕설": ["바보", "멍청이", "등신", "머저리"],
    "광고": [r"discord\.gg/\w+", r"https?://\S*(도박|토토)", r"카톡\s*문의", r"텔레\s*@\w+"],
    "도배": [r"(.)\1{9,}", r"[ㅋㅎ]{15,}"],
}

# ---------- 저장소 (#14 JSON 편 방식) ----------
def load_all() -> Dict[str, List[str]]:
    if WORD_FILE.exists():
        with WORD_FILE.open(encoding="utf-8") as f:
            return json.load(f)
    return {}

def load_words(guild_id: int) -> List[str]:
    """automod.py 에서도 이 함수를 가져다 씁니다."""
    return load_all().get(str(guild_id), [])

def save_words(guild_id: int, words: List[str]) -> None:
    data = load_all()
    data[str(guild_id)] = words
    WORD_FILE.parent.mkdir(parents=True, exist_ok=True)
    with WORD_FILE.open("w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

def to_choices(words: List[str]) -> List[app_commands.Choice[str]]:
    """문자열 목록을 Choice 목록으로. 25개·100자 제한을 여기서 한 번에 처리해요."""
    return [
        app_commands.Choice(name=word[:MAX_NAME_LEN], value=word[:MAX_NAME_LEN])
        for word in words[:MAX_CHOICES]
    ]

class WordAdmin(commands.Cog):
    """자동완성으로 금지어를 추가·삭제하는 관리 Cog"""

    def __init__(self, bot: commands.Bot):
        self.bot = bot

    # ---------- 자동완성 콜백 ----------
    async def preset_autocomplete(
        self,
        interaction: discord.Interaction,
        current: str,
    ) -> List[app_commands.Choice[str]]:
        """분류(namespace)에 따라 다른 후보를 돌려주는 연동 자동완성"""
        category = interaction.namespace.분류          # 먼저 고른 값 읽기
        pool = PRESETS.get(category, [])
        if not pool:                                   # 분류를 아직 안 골랐으면 전체
            pool = [word for words in PRESETS.values() for word in words]

        already = set(load_words(interaction.guild_id or 0))
        matched = [w for w in pool if current.lower() in w.lower() and w not in already]
        return to_choices(matched)

    async def registered_autocomplete(
        self,
        interaction: discord.Interaction,
        current: str,
    ) -> List[app_commands.Choice[str]]:
        """이미 등록된 금지어만 후보로 (삭제용)"""
        words = load_words(interaction.guild_id or 0)
        matched = [w for w in words if current.lower() in w.lower()]
        return to_choices(matched)

    # ---------- 명령어 ----------
    @app_commands.command(name="금지어추가", description="금지어를 추가해요 (자동완성 추천)")
    @app_commands.describe(분류="추천 목록 분류", 단어="추가할 단어 또는 정규식")
    @app_commands.choices(
        분류=[app_commands.Choice(name=key, value=key) for key in PRESETS]
    )
    @app_commands.autocomplete(단어=preset_autocomplete)
    @app_commands.checks.has_permissions(manage_messages=True)
    async def add_word(
        self,
        interaction: discord.Interaction,
        분류: app_commands.Choice[str],
        단어: str,
    ):
        단어 = 단어.strip()
        if not 단어:
            await interaction.response.send_message("빈 값은 추가할 수 없어요 🙅", ephemeral=True)
            return

        words = load_words(interaction.guild_id)
        if 단어 in words:
            await interaction.response.send_message(
                f"`{단어}` 는 이미 등록돼 있어요.", ephemeral=True
            )
            return

        words.append(단어)
        save_words(interaction.guild_id, words)
        await interaction.response.send_message(
            f"✅ `{단어}` 를 **{분류.value}** 금지어로 추가했어요. (총 {len(words)}개)",
            ephemeral=True,
        )

    @app_commands.command(name="금지어삭제", description="등록된 금지어를 골라서 삭제해요")
    @app_commands.describe(단어="삭제할 금지어 (등록된 것만 후보로 떠요)")
    @app_commands.autocomplete(단어=registered_autocomplete)
    @app_commands.checks.has_permissions(manage_messages=True)
    async def remove_word(self, interaction: discord.Interaction, 단어: str):
        words = load_words(interaction.guild_id)
        if 단어 not in words:
            await interaction.response.send_message(
                f"`{단어}` 는 등록되어 있지 않아요. `/금지어목록` 으로 확인해보세요.",
                ephemeral=True,
            )
            return

        words.remove(단어)
        save_words(interaction.guild_id, words)
        await interaction.response.send_message(
            f"🗑️ `{단어}` 를 삭제했어요. (남은 {len(words)}개)", ephemeral=True
        )

    @app_commands.command(name="금지어목록", description="등록된 금지어를 모두 보여줘요")
    async def list_words(self, interaction: discord.Interaction):
        words = load_words(interaction.guild_id)
        if not words:
            await interaction.response.send_message(
                "아직 등록된 금지어가 없어요. `/금지어추가` 로 넣어보세요!", ephemeral=True
            )
            return

        embed = discord.Embed(
            title="🛡️ 등록된 금지어",
            description="\n".join(f"`{word}`" for word in words[:50]),
            color=discord.Color.from_str("#5865F2"),
        )
        embed.set_footer(text=f"총 {len(words)}개")
        await interaction.response.send_message(embed=embed, ephemeral=True)

    # ---------- 권한 에러 처리 ----------
    @add_word.error
    @remove_word.error
    async def permission_error(self, interaction: discord.Interaction, error):
        if isinstance(error, app_commands.MissingPermissions):
            await interaction.response.send_message(
                "이 명령어는 **메시지 관리** 권한이 있어야 쓸 수 있어요 🔒", ephemeral=True
            )

async def setup(bot: commands.Bot):
    await bot.add_cog(WordAdmin(bot))

2) 폴더 구조

텍스트
server-bot/
├─ .env
├─ .gitignore
├─ bot.py
├─ data/
│  ├─ warns.json
│  └─ banned_words.json   # ← 오늘 자동 생성돼요
└─ cogs/
   ├─ admin.py
   ├─ poll.py             # #18 투표
   ├─ onboarding.py       # #20 환영·자동역할
   ├─ automod.py          # #21 (오늘 3줄 수정!)
   └─ wordadmin.py        # ← 오늘 추가!

💡 data/banned_words.json 도 .gitignore 에 넣어두세요. 서버 ID가 들어가는 파일이에요.

📌 실행 결과

python bot.py 로 실행하면 Cog가 하나 더 올라와요.

텍스트
✅ cogs.admin 불러옴
✅ cogs.automod 불러옴
✅ cogs.onboarding 불러옴
✅ cogs.poll 불러옴
✅ cogs.wordadmin 불러옴
MojangBot#1234 로그인 성공! (Cog 5개)

!sync 로 명령어를 등록한 뒤 /금지어추가 를 쳐보세요. 분류 를 고르면 단어 후보가 그 분류에 맞게 바뀝니다.

이번엔 /금지어삭제 예요. 여기서는 등록된 것만 뜹니다. 오타로 엉뚱한 걸 지울 일이 없어요.

추가한 금지어는 봇 재시작 없이 바로 #21의 automod에 반영돼요. /금지어추가 로 넣고 그 단어를 채팅에 쳐보면 즉시 삭제됩니다 🛡️

텍스트
@테스터 🚫 사용할 수 없는 표현이에요. 경고 1/3회 누적됐어요.

📌 자주 나는 에러

1. 자동완성이 아예 안 떠요 (에러도 없어요)

  • 원인 : 후보를 25개 넘게 돌려줬어요. 디스코드가 응답을 통째로 버립니다. 조용히 실패해서 제일 찾기 어려워요
  • 해결 : words[:25] 로 반드시 자르세요. 전체 코드의 to_choices() 처럼 함수 하나에서 처리하면 실수가 없어요

2. TypeError: unknown parameter / parameter name is not found

  • 원인 : @app_commands.autocomplete(단어=...) 의 키워드가 실제 파라미터 이름과 다릅니다
  • 해결 : 함수 정의부의 이름을 그대로 쓰세요. 한글 파라미터면 한글 그대로예요

3. TypeError: ... the parameter type was incorrect

  • 원인 : str · int · float 이 아닌 타입에 자동완성을 붙였어요 (discord.Member 등)
  • 해결 : 멤버·역할·채널은 디스코드가 알아서 목록을 띄워주니 자동완성이 필요 없어요. 굳이 쓰려면 파라미터를 str 로 받고 ID를 값으로 넘기세요

4. AttributeError: 'Namespace' object has no attribute '분류'

  • 원인 : interaction.namespace 에서 없는 이름을 꺼냈어요. 파라미터 이름을 바꿨는데 콜백은 안 고친 경우가 대부분입니다
  • 해결 : 이름을 맞추거나 getattr(interaction.namespace, "분류", None) 로 안전하게 꺼내세요

5. 자동완성은 뜨는데 명령어 실행이 AttributeError 로 죽어요

  • 원인 : @app_commands.choices 를 쓴 파라미터는 실행 시 Choice 객체로 들어와요. 문자열인 줄 알고 .strip() 같은 걸 부르면 터집니다
  • 해결 : 분류.value 로 값을 꺼내세요. 반대로 자동완성만 붙인 파라미터는 그냥 str 로 들어옵니다. 이 차이가 헷갈리기 제일 쉬워요 🤯

6. /금지어추가 는 되는데 automod가 안 잡아요

  • 원인 : automod.py 를 안 고쳤어요. #21 코드는 아직 COMPILED_BANNED 상수만 봅니다
  • 해결 : 5단계의 3줄 수정을 적용하세요. from cogs.wordadmin import load_words 가 들어갔는지 먼저 확인!

7. re.error: missing ), unterminated subpattern

  • 원인 : 금지어를 정규식으로 저장하는데 유저가 ( 같은 특수문자를 넣었어요
  • 해결 : 정규식으로 안 쓸 거면 re.escape(단어) 로 감싸서 저장하고, 쓸 거면 추가할 때 re.compile() 로 검증한 뒤 실패 시 안내하세요

8. 명령어가 아예 목록에 안 보여요

  • 원인 : 슬래시 명령어를 동기화 안 했어요
  • 해결 : #19편의 !sync 를 한 번 실행하세요. 전역 동기화는 반영에 시간이 걸리니, 테스트는 길드 동기화가 빨라요

📌 마무리

오늘은 슬래시 명령어에 자동완성을 붙여서 금지어를 명령어로 관리하게 만들었어요 🙌

오늘의 요약

  • choices 는 고정 메뉴, autocomplete 는 즉석 조리 — 바뀌는 목록엔 자동완성
  • 콜백은 async def (interaction, current) 모양 고정, Choice 리스트 반환
  • 25개를 넘기면 조용히 안 뜹니다 — 무조건 잘라서 보내기
  • interaction.namespace 로 먼저 고른 값을 읽으면 연동 자동완성이 돼요
  • 자동완성 콜백에는 명령어 권한 체크가 적용되지 않아요
  • choices 파라미터는 Choice 객체로, 자동완성 파라미터는 str 로 들어옵니다

다음 #23편에서는 JSON을 SQLite(aiosqlite)로 갈아엎습니다. 지금까지 만든 경고 기록·금지어 목록이 파일 하나에 다 들어 있는데, 서버가 커지면 이게 병목이 돼요. 비동기 DB로 옮기는 방법을 다뤄볼게요. 기대해주세요! 🙌

💬 오늘 만든 wordadmin.py 는 #27 최종 프로젝트 "커뮤니티 통합 관리 봇"의 설정 관리 모듈이 됩니다. 자동완성 패턴은 나중에 역할 선택·채널 선택에도 똑같이 재활용돼요!


📚 디스코드 봇 만들기 시리즈


연재 · 32편 중 22번째

디스코드 봇 만들기

  1. #1디스코드 봇 만들기 - Slash Command 완전 정복!
  2. #2Slash Command에 옵션(Arguments) 추가하기!
  3. #3Slash Command에 서브 커맨드(Subcommand) 넣기!
  4. #4Slash Command로 임베드 전송하기!
  5. #5버튼(Button) 상호작용 구현하기!
  6. #6드롭다운(Select Menu) 메뉴 구현하기!
  7. #7모달(Modal)로 입력 받기 구현하기 (파이썬 예제)
  8. #8임베드(Embed) 이미지·필드로 꾸미기 (파이썬 예제)
  9. #9AI(Claude·ChatGPT API) 연동하기! (파이썬 예제)
  10. #10다국어(i18n) 지원하기! (파이썬 예제)
  11. #11플라스크(Flask)로 웹 대시보드 만들기! (파이썬 예제)
  12. #12명령어 쿨다운(Cooldown)·권한(Permission) 관리하기! (파이썬 예제)
  13. #13역할·채널 자동화로 서버 관리하기! (파이썬 예제)
  14. #14데이터 저장하기! JSON vs SQLite (파이썬 예제)
  15. #15슬래시 명령어 그룹(Group)으로 명령어 정리하기! (파이썬 예제)
  16. #16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제)
  17. #17RAG로 문서 기반 답변봇 만들기! 우리 서버 전용 FAQ 봇 (파이썬 예제)
  18. #18투표(Poll) 기능 만들기! 다수결로 정하는 서버 규칙 (파이썬 예제)
  19. #19Cog로 코드 쪼개기! 1000줄 봇도 안 더러워지는 구조 (파이썬 예제)
  20. #20자동 환영 메시지·온보딩 만들기! 규칙 동의 버튼으로 자동 역할 부여 (파이썬 예제)
  21. #21키워드 자동응답·금지어 필터 만들기! 경고 누적으로 자동 제재까지 (파이썬 예제)
  22. #22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제)지금 읽는 글
  23. #23aiosqlite로 비동기 DB 제대로 설계하기! JSON 탈출 (파이썬 예제)
  24. #24채널 난독화(Channel Obfuscation) 대응하기! 11/16부터 안 보이는 채널이 생겨요 (파이썬 예제)
  25. #25봇이 죽지 않게! 에러 핸들링·로깅 체계 잡기 (파이썬 예제)
  26. #26티켓(Ticket) 문의 시스템 만들기! 버튼 하나로 1:1 상담 채널 자동 생성 (파이썬 예제)
  27. #27티켓 담당자 배정·우선순위 만들기! 스태프가 여러 명이어도 안 꼬이게 (파이썬 예제)
  28. #28레벨·경험치(XP) 시스템 만들기! 채팅할수록 레벨업, 랭킹판까지 (파이썬 예제)
  29. #29화폐·상점(Economy) 시스템 만들기! 코인 벌어서 아이템 사기 (파이썬 예제)
  30. #30투표·설문(Poll) 봇 만들기! 네이티브 투표 + 버튼으로 실시간 집계까지 (파이썬 예제) 🗳️
  31. #32서버 로그·감사(Audit Log) 채널 만들기! 메시지 삭제·수정·입퇴장·추방까지 자동 기록 (파이썬 예제) 🕵️
  32. #33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉
전체 32편 보기접기

글쓴이

Mojangcam

직접 코드를 짜고, 서버를 돌리고, 막힌 에러를 해결하며 배운 것들을 기록하는 개발·IT 실전 기록 블로그