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

디스코드 봇 만들기#32서버 로그·감사(Audit Log) 채널 만들기! 메시지 삭제·수정·입퇴장·추방까지 자동 기록 (파이썬 예제) 🕵️

한눈에 보기

디스코드 기본 감사 로그는 '누가 몇 개 지웠다'까지만 알려주고 지워진 내용은 안 보여줘요. raw 이벤트와 on_audit_log_entry_create를 조합해 삭제된 내용·삭제한 사람·입퇴장·역할 변경·추방까지 한 채널에 임베드로 쌓는 로그봇을 만듭니다.

  • Python 3.10.12 · discord.py 2.7.1 · python-dotenv 1.2.3 · 2026-09-25 직접 실행
  • 지워진 메시지 '내용'은 봇 캐시에 있을 때만 보여요 → max_messages를 넉넉히, Message Content·Server Members 인텐트 필수
  • 누가 지웠는지는 guild.audit_logs()에서 target·channel·시간(15초)으로 찾고, 본인 삭제는 감사 로그에 기록이 안 남아요
  • 추방·차단은 on_audit_log_entry_create(2.2+, Intents.moderation)로 실행자·사유까지 실시간으로 받습니다
목차13개 항목

안녕하세요! Mojangcam입니다 😎

지난 #31에서는 로비에 들어가면 자동으로 생기고 비면 사라지는 임시 음성채널(Temp Voice) 봇을 만들었어요. 이번 편은 서버 운영자라면 한 번쯤 꼭 찾게 되는 기능입니다. "이거 누가 지웠어?"

오늘 만들 로그봇은 이런 일을 해요.

  • 누군가 메시지를 지우면 지우기 전 내용이 그대로 로그 채널에 남아요
  • 관리자가 남의 메시지를 지웠다면 누가 지웠는지까지 찍혀요
  • 멤버가 들어오면 계정 생성일(부계정·봇 판별!), 나가면 갖고 있던 역할이 남고요
  • 추방·차단은 실행한 관리자와 사유까지 실시간으로 기록됩니다

왜 봇이 필요할까요? 🧠 디스코드에도 서버 설정 → 감사 로그 메뉴가 있긴 한데, 지워진 메시지의 내용은 안 보여줘요. "누가 몇 개 지웠다"까지만 나오죠. 분쟁이 생겼을 때 "아까 그 사람이 욕하고 지웠어요"를 증명하려면 봇이 미리 기록해두는 수밖에 없어요.

#서버-로그 채널에 메시지 삭제(빨강)·멤버 입장(초록) 임베드가 쌓인 화면과 이벤트별 색상 범례

이번 편의 로그봇은 #21 금지어 필터, #25 에러 핸들링과 함께 "커뮤니티 통합 관리 봇"의 운영 투명성 모듈 역할을 하게 됩니다.


📌 준비물

  • Python 3.9 이상 (3.10+ 권장) — dict[str, int] 표기를 써서 3.9부터 동작해요
  • discord.py 2.7.1 (2026-09-25 기준 PyPI 최신) — on_audit_log_entry_create가 2.2부터 생겼으니 구버전이면 꼭 올려주세요
  • python-dotenv 1.x
  • 디스코드 개발자 포털에서 만든 봇 + 서버 초대 완료 (권한: 감사 로그 보기, 메시지 보내기, 링크 첨부)
터미널
pip install -U discord.py python-dotenv

프로젝트 폴더에 .env 파일을 만들고 토큰을 넣어주세요. 토큰을 코드에 직접 쓰면 깃허브에 올리다 유출되기 딱 좋아요 😱

텍스트
token=여기에_봇_토큰

💡 .env는 꼭 .gitignore에 추가해두세요. (echo .env >> .gitignore)

⚠️ 이번 편은 특권 인텐트(Privileged Intents) 두 개가 필요해요. 개발자 포털 → Bot 탭에서 Message Content Intent(지워진 메시지 내용을 보려면 필수)와 Server Members Intent(입퇴장·역할 변경 감지)를 켜주세요. 이걸 안 켜면 삭제 로그에 내용이 텅 비어서 나오거나 입퇴장 이벤트가 아예 안 불려요.

🧩 로그봇이 기록할 것들

오늘 만들 로그는 6종류예요. 색으로 구분해서 스크롤만 해도 뭐가 일어났는지 보이게 할 거예요.

이벤트 기록 내용 임베드 색
메시지 삭제 작성자, 채널, 지워지기 전 내용, 첨부파일 이름, 지운 사람 빨강
메시지 수정 수정 전·후 내용, 메시지 바로가기 주황
입장·퇴장 계정 생성일(신규 계정 판별), 입장일, 갖고 있던 역할 초록 / 회색
닉네임·역할 변경 변경 전·후, 추가·제거된 역할 파랑
추방·차단·해제 실행한 관리자, 대상, 사유 짙은 빨강
채널 생성·삭제 채널 이름·종류 초록 / 빨강

📌 1단계 - 기본 세팅과 인텐트 3개

Python
# discord.py 2.7.1 / Python 3.9+ / python-dotenv 1.x
import os
import json
import asyncio

import discord
from discord import app_commands
from discord.ext import commands
from dotenv import load_dotenv

load_dotenv()
token = os.getenv("token")

CONFIG_FILE = "log_config.json"   # {서버ID(str): 로그채널ID(int)}
MAX_CONTENT = 1000                # 임베드 필드 최대 1024자 → 여유 있게 자르기

intents = discord.Intents.default()
intents.message_content = True   # 지워진 메시지 '내용'을 보려면 필수 (특권 인텐트)
intents.members = True           # 입퇴장·닉네임·역할 변경 감지 (특권 인텐트)
intents.moderation = True        # 추방·차단 + 감사 로그 이벤트

bot = commands.Bot(command_prefix="!", intents=intents, max_messages=5000)

log_channels: dict[str, int] = {}
lock = asyncio.Lock()

하나씩 뜯어볼게요 💡

  • intents.message_content : 다른 사람이 쓴 메시지의 내용을 봇이 볼 수 있게 해요. 이게 꺼져 있으면 삭제 로그에 content가 빈 문자열로 들어와요. 포털에서도 켜야 하는 특권 인텐트예요.
  • intents.members : on_member_join / on_member_remove / on_member_update가 이 인텐트에 묶여 있어요. 역시 특권 인텐트.
  • intents.moderation : on_member_ban / on_member_unban 그리고 오늘의 주인공 on_audit_log_entry_create가 여기 속해요. 이건 특권이 아니라서 코드에서만 켜면 됩니다. (예전 이름 intents.bans와 같은 거예요.)
  • max_messages=5000 : 봇이 기억해두는 메시지 개수예요. 기본값은 1000인데, 삭제된 메시지의 내용은 이 캐시에 있을 때만 볼 수 있어서 넉넉하게 올렸어요.

📌 2단계 - 로그 채널 저장과 임베드 공통 함수

서버마다 로그 채널이 다르니까 {서버ID: 채널ID}를 JSON에 저장해요. #31과 똑같은 패턴이에요.

Python
def save():
    with open(CONFIG_FILE, "w", encoding="utf-8") as f:
        json.dump(log_channels, f, ensure_ascii=False, indent=2)


def load():
    global log_channels
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, encoding="utf-8") as f:
            log_channels = json.load(f)


def get_log_channel(guild: discord.Guild):
    cid = log_channels.get(str(guild.id))
    return guild.get_channel(cid) if cid else None


def make_embed(title: str, color: discord.Color, user=None) -> discord.Embed:
    embed = discord.Embed(title=title, color=color, timestamp=discord.utils.utcnow())
    if user is not None:
        embed.set_author(name=f"{user} ({user.id})", icon_url=user.display_avatar.url)
    return embed


async def send_log(guild: discord.Guild, embed: discord.Embed):
    channel = get_log_channel(guild)
    if channel is None:
        return
    try:
        await channel.send(embed=embed)
    except discord.Forbidden:
        print(f"⚠️ {guild.name}: 로그 채널에 메시지를 보낼 권한이 없어요.")


def cut(text: str) -> str:
    text = text or "(내용 없음 — 임베드/첨부만 있거나 캐시에 없음)"
    return text if len(text) <= MAX_CONTENT else text[:MAX_CONTENT] + " …(생략)"
  • timestamp=discord.utils.utcnow() : 임베드 아래에 "오늘 오후 3:12" 같은 시간이 보는 사람의 시간대로 자동 표시돼요. 로그에서 제일 중요한 게 시간이잖아요!
  • set_author(name, icon_url) : 임베드 맨 위에 프로필 사진 + 이름 (ID)를 박아둬요. ID를 같이 남기는 이유는, 나중에 닉네임을 바꾸거나 서버를 나가도 ID는 안 변하기 때문이에요.
  • send_log의 try/except : 로그 채널 권한을 누가 바꿔버리면 봇이 Forbidden으로 죽을 수 있어요. #25에서 배운 대로 잡아서 콘솔에만 찍고 넘어갑니다.
  • cut() : 임베드 필드 값은 1024자 제한이 있어요. 넘기면 400 Bad Request가 나니까 미리 잘라요.

📌 3단계 - 핵심! 메시지 삭제·수정 잡기

여기서 raw 이벤트를 써요. on_message_delete는 캐시에 있는 메시지만 알려주는데, on_raw_message_delete는 캐시에 없어도 "어느 채널의 어떤 ID가 지워졌다"까지는 알려주거든요.

메시지 삭제 시 봇의 처리 흐름 - 삭제 발생, 캐시 조회, 감사 로그 조회 3단계와 상황별로 '삭제한 사람' 칸에 찍히는 값

Python
async def find_deleter(guild: discord.Guild, author_id: int, channel_id: int):
    """감사 로그에서 '방금 이 사람 메시지를 이 채널에서 지운 관리자'를 찾아요."""
    try:
        async for entry in guild.audit_logs(limit=5, action=discord.AuditLogAction.message_delete):
            same_target = entry.target is not None and entry.target.id == author_id
            same_channel = entry.extra.channel.id == channel_id
            recent = (discord.utils.utcnow() - entry.created_at).total_seconds() < 15
            if same_target and same_channel and recent:
                return entry.user
    except discord.Forbidden:
        return None
    return None


@bot.event
async def on_raw_message_delete(payload: discord.RawMessageDeleteEvent):
    if payload.guild_id is None:      # DM은 무시
        return
    guild = bot.get_guild(payload.guild_id)
    msg = payload.cached_message

    embed = make_embed("🗑️ 메시지 삭제", discord.Color.red(), msg.author if msg else None)
    embed.add_field(name="채널", value=f"<#{payload.channel_id}>", inline=True)

    if msg is None:
        embed.add_field(name="내용", value="캐시에 없는 메시지예요 (봇 시작 전 메시지)", inline=False)
        embed.set_footer(text=f"메시지 ID: {payload.message_id}")
    else:
        if msg.author.bot:
            return
        deleter = await find_deleter(guild, msg.author.id, payload.channel_id)
        who = deleter.mention if deleter else "본인 (또는 확인 불가)"
        embed.add_field(name="삭제한 사람", value=who, inline=True)
        embed.add_field(name="내용", value=cut(msg.content), inline=False)
        if msg.attachments:
            files = "\n".join(a.filename for a in msg.attachments)
            embed.add_field(name="첨부파일", value=files, inline=False)
        embed.set_footer(text=f"메시지 ID: {msg.id}")

    await send_log(guild, embed)


@bot.event
async def on_raw_message_edit(payload: discord.RawMessageUpdateEvent):
    if payload.guild_id is None:
        return
    before = payload.cached_message
    after = payload.message
    if before is None or after.author.bot:
        return
    if before.content == after.content:      # 임베드 미리보기 생성 등 '가짜 수정' 걸러내기
        return

    guild = bot.get_guild(payload.guild_id)
    embed = make_embed("✏️ 메시지 수정", discord.Color.orange(), after.author)
    embed.add_field(name="채널", value=after.channel.mention, inline=True)
    embed.add_field(name="바로가기", value=f"[메시지로 이동]({after.jump_url})", inline=True)
    embed.add_field(name="수정 전", value=cut(before.content), inline=False)
    embed.add_field(name="수정 후", value=cut(after.content), inline=False)
    await send_log(guild, embed)


@bot.event
async def on_raw_bulk_message_delete(payload: discord.RawBulkMessageDeleteEvent):
    if payload.guild_id is None:
        return
    guild = bot.get_guild(payload.guild_id)
    embed = make_embed("🧹 메시지 대량 삭제", discord.Color.dark_red())
    embed.add_field(name="채널", value=f"<#{payload.channel_id}>", inline=True)
    embed.add_field(name="개수", value=f"{len(payload.message_ids)}개", inline=True)
    await send_log(guild, embed)

하나씩 뜯어볼게요 💡

  • payload.cached_message : 지워진 메시지가 봇 캐시에 있었으면 Message 객체, 없으면 None이에요. 봇을 켜기 전에 올라온 메시지는 캐시에 없으니 "내용 확인 불가"로 남기되, 채널과 메시지 ID는 남겨요. 그래도 조사에 도움이 됩니다.
  • guild.audit_logs(limit=5, action=...) : 서버 감사 로그를 최신순으로 훑는 비동기 이터레이터예요. for가 아니라 async for 로 돌려야 해요. 봇에 감사 로그 보기 권한이 없으면 Forbidden이 나니까 try로 감쌌어요.
  • entry.target / entry.extra.channel / entry.created_at : message_delete 항목은 target이 메시지를 쓴 사람, extra.channel이 지워진 채널, extra.count가 지운 개수예요. 세 가지가 맞고 15초 안에 생긴 항목이면 그 entry.user(실행자)가 지운 사람이에요.
  • "본인 (또는 확인 불가)" : 자기 메시지를 스스로 지우면 감사 로그에 아예 기록이 안 남아요. 그래서 감사 로그에서 못 찾으면 본인 삭제로 봅니다. 다만 같은 관리자가 같은 사람 메시지를 연달아 지우면 디스코드가 기존 항목의 count만 올리고 새 항목을 안 만들어서, 15초 조건에 걸려 놓치는 경우가 있어요. 100% 정확한 건 아니라는 점만 기억해주세요.
  • payload.message (수정 이벤트) : 수정 후 메시지예요(2.5부터 추가). 수정 전은 cached_message에서 가져와요. 링크를 붙였을 때 디스코드가 미리보기 임베드를 만들면서 수정 이벤트가 한 번 더 오는데, content가 같으면 건너뛰게 해서 로그가 지저분해지는 걸 막았어요.
  • on_raw_bulk_message_delete : 관리자가 "메시지 100개 삭제" 같은 대량 삭제(purge)를 하면 개별 삭제 이벤트 대신 이게 한 번 와요. 내용을 전부 남기면 로그가 폭발하니 개수만 기록했어요.

📌 4단계 - 입장·퇴장·닉네임·역할 변경

Python
@bot.event
async def on_member_join(member: discord.Member):
    embed = make_embed("📥 멤버 입장", discord.Color.green(), member)
    embed.add_field(name="멤버", value=member.mention, inline=True)
    embed.add_field(name="계정 생성일", value=discord.utils.format_dt(member.created_at, "R"), inline=True)
    embed.add_field(name="현재 인원", value=f"{member.guild.member_count}명", inline=True)
    await send_log(member.guild, embed)


@bot.event
async def on_member_remove(member: discord.Member):
    embed = make_embed("📤 멤버 퇴장", discord.Color.light_grey(), member)
    embed.add_field(name="멤버", value=f"{member} ({member.mention})", inline=True)
    if member.joined_at:
        embed.add_field(name="입장일", value=discord.utils.format_dt(member.joined_at, "R"), inline=True)
    roles = [r.mention for r in member.roles if r != member.guild.default_role]
    if roles:
        embed.add_field(name="가지고 있던 역할", value=" ".join(roles), inline=False)
    await send_log(member.guild, embed)


@bot.event
async def on_member_update(before: discord.Member, after: discord.Member):
    if before.nick != after.nick:
        embed = make_embed("🏷️ 닉네임 변경", discord.Color.blue(), after)
        embed.add_field(name="변경 전", value=before.nick or "(없음)", inline=True)
        embed.add_field(name="변경 후", value=after.nick or "(없음)", inline=True)
        await send_log(after.guild, embed)

    added = [r for r in after.roles if r not in before.roles]
    removed = [r for r in before.roles if r not in after.roles]
    if added or removed:
        embed = make_embed("🎭 역할 변경", discord.Color.blurple(), after)
        if added:
            embed.add_field(name="➕ 추가", value=" ".join(r.mention for r in added), inline=False)
        if removed:
            embed.add_field(name="➖ 제거", value=" ".join(r.mention for r in removed), inline=False)
        await send_log(after.guild, embed)
  • format_dt(시각, "R") : <t:1790291186:R> 형식으로 바꿔줘서 디스코드가 "3일 전", "2년 전"처럼 상대 시간으로 보여줘요. 입장 로그에서 계정 생성일이 "12분 전"이면? 봇 계정이나 부계정일 확률이 높죠. 운영자들이 제일 좋아하는 필드예요 👀
  • on_member_remove : 스스로 나간 것도, 추방·차단당한 것도 전부 이 이벤트로 와요. 그래서 "왜 나갔는지"는 다음 단계의 감사 로그 이벤트로 따로 기록해요.
  • before.roles / after.roles 비교 : on_member_update는 닉네임·역할·타임아웃·서버 프로필 사진 등 뭐든 바뀌면 불려요. 그래서 안에서 "뭐가 바뀌었는지" 직접 비교해야 해요. 역할 리스트 차집합으로 추가·제거를 구분했어요.

📌 5단계 - 추방·차단은 감사 로그 이벤트로 (누가·왜)

Python
MOD_ACTIONS = {
    discord.AuditLogAction.kick: ("👢 추방(Kick)", discord.Color.dark_orange()),
    discord.AuditLogAction.ban: ("🔨 차단(Ban)", discord.Color.dark_red()),
    discord.AuditLogAction.unban: ("🕊️ 차단 해제(Unban)", discord.Color.teal()),
}


@bot.event
async def on_audit_log_entry_create(entry: discord.AuditLogEntry):
    if entry.action not in MOD_ACTIONS:
        return
    title, color = MOD_ACTIONS[entry.action]
    embed = make_embed(title, color, entry.user)
    target = entry.target
    embed.add_field(name="대상", value=f"{target} (<@{target.id}>)" if target else "(알 수 없음)", inline=True)
    embed.add_field(name="실행자", value=entry.user.mention if entry.user else "(알 수 없음)", inline=True)
    embed.add_field(name="사유", value=entry.reason or "(사유 없음)", inline=False)
    await send_log(entry.guild, embed)
  • on_audit_log_entry_create : 서버 감사 로그에 새 항목이 생기는 순간 실시간으로 호출돼요(discord.py 2.2+, Intents.moderation 필요, 봇에 감사 로그 보기 권한 필수). 3단계처럼 뒤늦게 뒤지는 게 아니라 디스코드가 먼저 알려주는 거라 훨씬 정확해요.
  • entry.action : 무슨 행동인지 나타내는 AuditLogAction 열거형이에요. 오늘은 추방·차단·해제 세 개만 골랐지만 channel_create, role_update, member_role_update 등 수십 가지가 있어요. 딕셔너리에 한 줄만 추가하면 로그 종류가 늘어나요.
  • entry.user / entry.target / entry.reason : 각각 실행한 관리자, 당한 사람, 그리고 관리자가 추방할 때 적은 사유예요. 사유를 안 적으면 None이니 "(사유 없음)"으로 바꿔줬어요.

💡 실행자가 None으로 나와요? 유저 정보 캐시에 없으면 그럴 수 있어요. entry.user_id(2.2+)는 항상 들어 있으니 f"<@{entry.user_id}>"로 멘션만 찍어도 돼요.

📌 6단계 - 채널 생성·삭제 + 로그 채널 설정 명령어

Python
@bot.event
async def on_guild_channel_create(channel: discord.abc.GuildChannel):
    embed = make_embed("📁 채널 생성", discord.Color.green())
    embed.add_field(name="채널", value=f"{channel.mention} (`{channel.name}`)", inline=True)
    embed.add_field(name="종류", value=str(channel.type), inline=True)
    await send_log(channel.guild, embed)


@bot.event
async def on_guild_channel_delete(channel: discord.abc.GuildChannel):
    embed = make_embed("🗂️ 채널 삭제", discord.Color.red())
    embed.add_field(name="채널", value=f"`{channel.name}`", inline=True)
    embed.add_field(name="종류", value=str(channel.type), inline=True)
    await send_log(channel.guild, embed)


@bot.tree.command(name="로그설정", description="[관리자] 서버 로그를 보낼 채널을 정해요")
@app_commands.describe(채널="로그가 쌓일 텍스트 채널")
@app_commands.checks.has_permissions(administrator=True)
async def set_log(interaction: discord.Interaction, 채널: discord.TextChannel):
    me = interaction.guild.me
    perms = 채널.permissions_for(me)
    if not (perms.send_messages and perms.embed_links):
        return await interaction.response.send_message(
            f"❌ {채널.mention}에 **메시지 보내기·링크 첨부** 권한이 없어요.", ephemeral=True)
    async with lock:
        log_channels[str(interaction.guild.id)] = 채널.id
        save()
    await interaction.response.send_message(f"✅ 이제 서버 로그를 {채널.mention}에 기록할게요!", ephemeral=True)
    await 채널.send(embed=make_embed("🟢 로그 채널로 설정됐어요", discord.Color.green(), interaction.user))


@bot.tree.command(name="로그해제", description="[관리자] 서버 로그 기록을 멈춰요")
@app_commands.checks.has_permissions(administrator=True)
async def unset_log(interaction: discord.Interaction):
    async with lock:
        log_channels.pop(str(interaction.guild.id), None)
        save()
    await interaction.response.send_message("⏹️ 로그 기록을 껐어요.", ephemeral=True)


@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
    if isinstance(error, app_commands.MissingPermissions):
        await interaction.response.send_message("❌ 관리자만 쓸 수 있는 명령어예요.", ephemeral=True)
    else:
        raise error


@bot.event
async def on_ready():
    load()
    await bot.tree.sync()
    print(f"✅ 로그인: {bot.user} / 로그 설정된 서버 {len(log_channels)}개")


bot.run(token)

/로그설정 명령어 실행 후 나에게만 보이는 응답과 로그 채널에 도착한 설정 완료 임베드, 봇이 미리 확인하는 항목 체크리스트

  • 채널: discord.TextChannel : 슬래시 명령어 인자의 타입을 채널로 두면 디스코드가 채널 선택 UI를 띄워줘요. #15 명령어 그룹 편에서 본 그 기능이에요.
  • 채널.permissions_for(me) : 로그 채널에 봇이 실제로 글을 쓸 수 있는지 설정 시점에 미리 검사해요. 나중에 이벤트가 터졌을 때 조용히 실패하는 것보다 지금 알려주는 게 훨씬 친절하죠.
  • bot.tree.error : 관리자가 아닌 사람이 명령어를 치면 MissingPermissions가 나는데, 안 잡으면 "상호작용에 실패했습니다"라는 불친절한 메시지만 떠요. #25에서 잡은 패턴 그대로예요.

📎 1~6단계 코드블록을 순서대로 이어 붙이면 그대로 완성본이에요. logbot.py로 저장하고 바로 돌리시면 됩니다.

📌 실행 결과

  1. python logbot.py 실행 → 콘솔에 ✅ 로그인: 봇이름 / 로그 설정된 서버 0개
  2. 관리자가 #서버-로그 같은 비공개 채널을 만들고 /로그설정 채널:#서버-로그 → 초록색 "🟢 로그 채널로 설정됐어요" 임베드가 도착해요.
  3. 아무 채널에서 메시지를 쓰고 지워보세요 → 🗑️ 빨간 임베드에 지운 내용이 그대로 남아요. 관리자가 남의 메시지를 지우면 "삭제한 사람"에 관리자 멘션이 찍힙니다.
  4. 메시지를 수정하면 ✏️ 주황 임베드에 수정 전·후가 나란히, 링크를 누르면 그 메시지로 점프해요.
  5. 부계정으로 서버에 들어왔다 나가보면 📥 입장(계정 생성일 "N분 전"!) → 📤 퇴장 순서로 쌓이고, 추방하면 📤 퇴장 뒤에 👢 추방 임베드가 실행자·사유와 함께 붙어요.

💡 로그 채널은 꼭 관리자만 보이게 권한을 잠가두세요. 지워진 메시지 내용이 그대로 남는 채널이라, 일반 멤버가 보면 "지웠는데 왜 남아있냐"는 항의가 들어오거든요. 한 달에 한 번쯤 오래된 로그를 정리하는 것도 잊지 마시고요.

💡 보너스 - 로그 종류 늘리기

MOD_ACTIONS 딕셔너리에 항목만 추가하면 감사 로그 종류가 늘어나요. 자주 쓰는 것들이에요.

Python
discord.AuditLogAction.member_update: ("⏳ 타임아웃/닉네임 변경", discord.Color.gold()),
discord.AuditLogAction.role_create: ("🎨 역할 생성", discord.Color.green()),
discord.AuditLogAction.role_delete: ("🎨 역할 삭제", discord.Color.red()),
discord.AuditLogAction.message_bulk_delete: ("🧹 대량 삭제 (관리자)", discord.Color.dark_red()),

entry.changes.before / entry.changes.after를 찍어보면 "역할 이름이 A에서 B로", "타임아웃이 언제까지" 같은 세부 변경 내용까지 뽑을 수 있어요. 음성채널 입퇴장 로그는 #31에서 쓴 on_voice_state_update에 임베드 한 줄만 붙이면 끝이고요.

📌 자주 나는 에러

  1. 삭제 로그에 내용이 "(내용 없음)"으로만 나와요 → 원인: 개발자 포털에서 Message Content Intent를 안 켰거나 코드에 intents.message_content = True가 빠짐 → 해결: 둘 다 켜고 봇 재시작. 봇을 켜기 전에 올라온 메시지는 캐시에 없어서 원래 안 보이는 게 정상이에요.
  2. on_member_join / on_member_remove가 안 불려요 → 원인: Server Members Intent 미설정 → 해결: 포털 Bot 탭에서 켜고 intents.members = True 확인. 이걸 안 켜면 에러도 없이 조용히 안 불려서 헷갈려요.
  3. discord.errors.Forbidden: 403 Forbidden (error code: 50013): Missing Permissions → 원인: 봇 역할에 감사 로그 보기 권한이 없음 → 해결: 서버 설정 → 역할 → 봇 역할에 "감사 로그 보기" 체크. on_audit_log_entry_create도 이 권한이 없으면 안 불려요.
  4. HTTPException: 400 Bad Request (error code: 50035): Invalid Form Body — In embeds.0.fields.2.value: Must be 1024 or fewer in length → 원인: 임베드 필드에 1024자를 넘는 내용을 넣음 → 해결: 예제의 cut()처럼 자르기. 긴 메시지가 잦다면 내용을 discord.File로 텍스트 파일 첨부하는 방법도 좋아요.
  5. TypeError: 'async_generator' object is not iterable → 원인: guild.audit_logs()를 for로 돌림 → 해결: async for 로 바꾸기. 비동기 이터레이터예요.
  6. 수정 로그가 링크 붙일 때마다 두 번 떠요 → 원인: 디스코드가 링크 미리보기(임베드)를 붙이면서 수정 이벤트를 한 번 더 보냄 → 해결: 예제처럼 before.content == after.content면 건너뛰기.
  7. discord.errors.PrivilegedIntentsRequired로 봇이 시작조차 안 돼요 → 원인: 코드에서는 특권 인텐트를 켰는데 포털에서는 안 켠 상태 → 해결: 포털 Bot 탭 → Privileged Gateway Intents에서 두 개 모두 ON.

🙌 마무리

오늘은 raw 이벤트 + 감사 로그(Audit Log) 조회 + on_audit_log_entry_create 세 가지를 조합해서, 메시지 삭제·수정부터 입퇴장·역할 변경·추방·차단까지 한 채널에 임베드로 기록하는 로그봇을 완성했어요. 정리하면 이래요.

  • 지워진 메시지 내용은 캐시에 있을 때만 볼 수 있다 → max_messages를 넉넉히, 특권 인텐트 2개 ON
  • 누가 지웠는지는 guild.audit_logs()에서 target·channel·시간으로 찾는다 (본인 삭제는 기록 없음)
  • 추방·차단은 on_audit_log_entry_create로 실행자·사유까지 실시간으로 받는다
  • 임베드 필드 1024자 제한, 로그 채널은 관리자 전용으로 잠그기

다음 편에서는 #31에서 예고했던 임시 음성방 확장 — 방장 위임·강퇴·초대 전용(화이트리스트) 방과 텍스트 채널이 같이 생기는 "파티방"을 다뤄볼게요. 기대해주세요 🙌

여러분 서버에선 어떤 로그가 제일 필요하세요? 댓글로 알려주시면 보너스 항목에 추가해볼게요 😎


📚 함께 보면 좋은 글

연재 · 32편 중 31번째

디스코드 봇 만들기

  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 실전 기록 블로그