디스코드 봇 만들기#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단계 - 자동완성 콜백의 정해진 모양
자동완성은 콜백 함수 하나를 만들고, 명령어 파라미터에 연결해주면 끝이에요. 그런데 이 콜백은 모양이 정해져 있습니다.
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 로 걸러야 해요.
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자 자르기를 한 군데서만 하면 실수가 없어요
검색은 이렇게 합니다.
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 로 먼저 고른 값 읽기
자동완성의 진짜 재미는 여기예요. 앞에서 고른 옵션에 따라 뒤 후보가 달라지게 만들 수 있습니다.
/금지어추가 분류:광고 단어:... 처럼 쓸 때, 분류 에서 광고를 골랐으면 단어 후보도 광고 관련만 떠야 자연스럽죠.
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 방식 그대로 갑니다.
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군데만 고칩니다.
# 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
# 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 최종 프로젝트 "커뮤니티 통합 관리 봇"의 설정 관리 모듈이 됩니다. 자동완성 패턴은 나중에 역할 선택·채널 선택에도 똑같이 재활용돼요!
📚 디스코드 봇 만들기 시리즈
- #1 ~ #12 — 봇 만들기 기초부터 쿨다운·권한까지 [링크 자리]
- #13 역할·채널 자동화 [링크 자리]
- #14 데이터 저장하기 (JSON vs SQLite) [링크 자리]
- #15 슬래시 명령어 그룹(Group) [링크 자리]
- #16 리마인더·알림 기능 [링크 자리]
- #17 RAG로 문서 기반 답변봇 만들기
- #18 투표(Poll) 기능 만들기
- #19 Cog로 코드 쪼개기
- #20 자동 환영 메시지·온보딩
- #21 키워드 자동응답·금지어 필터
- #22 슬래시 명령어 자동완성(Autocomplete) ← 지금 이 글!
- #23 JSON에서 SQLite(aiosqlite)로 갈아타기 (예정)
연재 · 32편 중 22번째
디스코드 봇 만들기
- #1디스코드 봇 만들기 - Slash Command 완전 정복!
- #2Slash Command에 옵션(Arguments) 추가하기!
- #3Slash Command에 서브 커맨드(Subcommand) 넣기!
- #4Slash Command로 임베드 전송하기!
- #5버튼(Button) 상호작용 구현하기!
- #6드롭다운(Select Menu) 메뉴 구현하기!
- #7모달(Modal)로 입력 받기 구현하기 (파이썬 예제)
- #8임베드(Embed) 이미지·필드로 꾸미기 (파이썬 예제)
- #9AI(Claude·ChatGPT API) 연동하기! (파이썬 예제)
- #10다국어(i18n) 지원하기! (파이썬 예제)
- #11플라스크(Flask)로 웹 대시보드 만들기! (파이썬 예제)
- #12명령어 쿨다운(Cooldown)·권한(Permission) 관리하기! (파이썬 예제)
- #13역할·채널 자동화로 서버 관리하기! (파이썬 예제)
- #14데이터 저장하기! JSON vs SQLite (파이썬 예제)
- #15슬래시 명령어 그룹(Group)으로 명령어 정리하기! (파이썬 예제)
- #16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제)
- #17RAG로 문서 기반 답변봇 만들기! 우리 서버 전용 FAQ 봇 (파이썬 예제)
- #18투표(Poll) 기능 만들기! 다수결로 정하는 서버 규칙 (파이썬 예제)
- #19Cog로 코드 쪼개기! 1000줄 봇도 안 더러워지는 구조 (파이썬 예제)
- #20자동 환영 메시지·온보딩 만들기! 규칙 동의 버튼으로 자동 역할 부여 (파이썬 예제)
- #21키워드 자동응답·금지어 필터 만들기! 경고 누적으로 자동 제재까지 (파이썬 예제)
- #22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제)지금 읽는 글
- #23aiosqlite로 비동기 DB 제대로 설계하기! JSON 탈출 (파이썬 예제)
- #24채널 난독화(Channel Obfuscation) 대응하기! 11/16부터 안 보이는 채널이 생겨요 (파이썬 예제)
- #25봇이 죽지 않게! 에러 핸들링·로깅 체계 잡기 (파이썬 예제)
- #26티켓(Ticket) 문의 시스템 만들기! 버튼 하나로 1:1 상담 채널 자동 생성 (파이썬 예제)
- #27티켓 담당자 배정·우선순위 만들기! 스태프가 여러 명이어도 안 꼬이게 (파이썬 예제)
- #28레벨·경험치(XP) 시스템 만들기! 채팅할수록 레벨업, 랭킹판까지 (파이썬 예제)
- #29화폐·상점(Economy) 시스템 만들기! 코인 벌어서 아이템 사기 (파이썬 예제)
- #30투표·설문(Poll) 봇 만들기! 네이티브 투표 + 버튼으로 실시간 집계까지 (파이썬 예제) 🗳️
- #32서버 로그·감사(Audit Log) 채널 만들기! 메시지 삭제·수정·입퇴장·추방까지 자동 기록 (파이썬 예제) 🕵️
- #33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉
