<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>디스호스트 기술 블로그</title>
    <link>https://dishost.tistory.com/</link>
    <description>쉽고 안정적인 디스코드 봇 호스팅 서비스, 디스호스트의 기술 블로그입니다. 디스호스트는 24시간 구동되는 서버를 통해 디스코드 봇을 대신 구동시켜 드리는 서비스를 제공하고 있습니다.</description>
    <language>ko</language>
    <pubDate>Sun, 14 Jun 2026 10:26:50 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>디스호스트</managingEditor>
    <image>
      <title>디스호스트 기술 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/7936972/attach/c8df017a89a149ca918a8d2ea9a3e187</url>
      <link>https://dishost.tistory.com</link>
    </image>
    <item>
      <title>디스코드 음악 봇 만들기</title>
      <link>https://dishost.tistory.com/149</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;음악 봇은 오래된 기능처럼 보이지만 여전히 수요가 있습니다.&lt;br /&gt;&lt;br /&gt;특히 소규모 커뮤니티나 개인 서버에서는 음성 채널 기능을 테스트할 때 가장 자주 만드는 봇 중 하나입니다.&lt;br /&gt;&lt;br /&gt;다만 다른 기능형 봇보다 의존성이 많고, FFmpeg 같은 외부 도구에서 자주 막힙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;code&gt;discord.py&lt;/code&gt;와 FFmpeg를 이용해 URL 기반 오디오를 재생하는 가장 기본적인 음악 봇을 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 먼저 알아둘 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음악 봇은 텍스트 봇보다 준비물이 많습니다.&lt;br /&gt;&lt;br /&gt;특히 아래 두 가지가 중요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;음성 채널 접속 권한&lt;/li&gt;
&lt;li&gt;FFmpeg 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FFmpeg가 없으면 재생 단계에서 바로 멈춥니다.&lt;br /&gt;&lt;br /&gt;따라서 설치 단계에서 FFmpeg가 실제로 잡히는지 먼저 확인해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 음성 재생에 필요한 도구부터 맞춘다&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;pip install -U discord.py yt-dlp python-dotenv
brew install ffmpeg&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;macOS 기준으로는 `brew install ffmpeg`가 가장 빠릅니다.&lt;br /&gt;다른 OS라면 설치 방식이 다를 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 토큰과 명령어 접두어 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
GUILD_ID=테스트_서버_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 글에서는 슬래시 명령어로 음성 채널 접속과 재생을 처리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 음성 채널 접속과 재생 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

import discord
from discord import app_commands
from dotenv import load_dotenv
import yt_dlp

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))

intents = discord.Intents.default()
intents.voice_states = True
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)


def extract_audio_url(query: str) -&amp;gt; str:
    with yt_dlp.YoutubeDL({&quot;format&quot;: &quot;bestaudio&quot;, &quot;quiet&quot;: True}) as ydl:
        info = ydl.extract_info(query, download=False)
        return info[&quot;url&quot;]


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;재생&quot;, description=&quot;입력한 URL의 오디오를 재생합니다.&quot;, guild=guild)
@app_commands.describe(url=&quot;재생할 오디오 URL&quot;)
async def play_music(interaction: discord.Interaction, url: str):
    if not isinstance(interaction.user, discord.Member) or interaction.user.voice is None:
        await interaction.response.send_message(&quot;먼저 음성 채널에 들어가 주세요.&quot;, ephemeral=True)
        return

    await interaction.response.defer()
    voice_channel = interaction.user.voice.channel
    voice_client = interaction.guild.voice_client if interaction.guild else None

    if voice_client is None:
        voice_client = await voice_channel.connect()
    elif voice_client.channel != voice_channel:
        await voice_client.move_to(voice_channel)

    audio_url = extract_audio_url(url)
    source = discord.FFmpegPCMAudio(audio_url)
    voice_client.play(source)
    await interaction.followup.send(f&quot;재생 시작: {url}&quot;)


@tree.command(name=&quot;중지&quot;, description=&quot;현재 재생을 중지하고 퇴장합니다.&quot;, guild=guild)
async def stop_music(interaction: discord.Interaction):
    voice_client = interaction.guild.voice_client if interaction.guild else None
    if voice_client is None:
        await interaction.response.send_message(&quot;현재 음성 채널에 연결되어 있지 않습니다.&quot;, ephemeral=True)
        return

    await voice_client.disconnect()
    await interaction.response.send_message(&quot;재생을 중지하고 음성 채널에서 나갔습니다.&quot;)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;음악 봇 기본형은 결국 `yt-dlp`로 오디오 URL을 뽑고, FFmpeg로 재생하는 구조입니다.&lt;br /&gt;텍스트 봇보다 외부 환경 의존성이 크다는 점만 기억해 두면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 입장&amp;middot;재생&amp;middot;정지 순서로 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;봇을 음성 채널에 부른 뒤 `/재생`으로 테스트 URL을 넣어 봅니다.&lt;br /&gt;정상이라면 바로 소리가 나와야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. FFmpeg와 음성 연결에서 자주 끊긴다&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소리가 안 나는 경우 대부분 FFmpeg 설치 문제이거나 봇 음성 권한 문제입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Opus not loaded&lt;/code&gt; 관련 메시지가 나오는 경우 오디오 환경 구성이 추가로 필요할 수 있습니다.&lt;/li&gt;
&lt;li&gt;특정 URL에서만 실패하는 경우 &lt;code&gt;yt-dlp&lt;/code&gt; 버전이 오래됐을 가능성이 큽니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 운영 서버에서는 음악 전용 채널을 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 채팅 채널에서 음악 명령어를 섞으면 금방 어수선해집니다.&lt;br /&gt;&lt;br /&gt;전용 봇 명령어 채널이나 음성 채널 묶음을 따로 둬야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널 구조는 &lt;a href=&quot;https://blog.dishost.kr/109&quot;&gt;디스코드 카테고리 채널 정리하는 법&lt;/a&gt;과 같이 보면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 배포 전에는 로컬에서 먼저 안정화한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음악 봇은 텍스트 봇보다 실패 지점이 많습니다.&lt;br /&gt;&lt;br /&gt;따라서 로컬에서 FFmpeg, 권한, 재생 흐름이 모두 정상인지 먼저 확인한 뒤 배포해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영 환경이 필요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음성 기능도 결국 계속 켜져 있어야 멤버가 필요할 때 바로 쓸 수 있습니다.&lt;br /&gt;&lt;br /&gt;실운영용이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 계속 켜진 환경이 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 음악 재생 뒤에는 FFmpeg 오류 대응까지 묶어 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음악 봇을 붙였다면 그 직후 가장 많이 만나는 FFmpeg 환경 오류를 바로 점검해야 합니다.&lt;br /&gt;&lt;br /&gt;그 전에 음성 채널 구조를 다시 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/102&quot;&gt;디스코드 서버 꾸미기, 채널 역할 배치 가이드&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/58&quot;&gt;디스코드 역할 설정 완벽 가이드, 관리자 운영진 멤버 봇 역할을 처음부터 정리하는 법&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/149</guid>
      <comments>https://dishost.tistory.com/149#entry149comment</comments>
      <pubDate>Sun, 24 May 2026 17:04:43 +0900</pubDate>
    </item>
    <item>
      <title>이미지 생성 AI 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/145</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트 기반 챗봇보다 반응이 빠르게 오는 기능 중 하나가 이미지 생성입니다.&lt;br /&gt;&lt;br /&gt;프롬프트 한 줄로 샘플 이미지를 받아 보는 기능은 이벤트, 커뮤니티 놀이, 창작 서버에서 특히 잘 먹힙니다.&lt;br /&gt;&lt;br /&gt;다만 호출 비용과 사용량이 바로 늘 수 있으므로 기본형부터 보수적으로 붙여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 슬래시 명령어로 프롬프트를 입력받아 이미지 생성 API를 호출하고, 결과 이미지를 디스코드 채널에 보내는 기본 봇을 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 이미지 생성 봇은 어디서 잘 맞을까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 같은 서버에서 특히 체감이 큽니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;창작 커뮤니티&lt;/li&gt;
&lt;li&gt;이벤트 채널&lt;/li&gt;
&lt;li&gt;밈 이미지 놀이 서버&lt;/li&gt;
&lt;li&gt;샘플 시안이 필요한 운영 채널&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 응답 기본 구조는 &lt;a href=&quot;https://blog.dishost.kr/144&quot;&gt;AI 자동 답변 FAQ 디스코드 봇 만들기&lt;/a&gt;와 비슷합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 이미지 채널과 키 값을 먼저 넣기&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
OPENAI_API_KEY=여기에_OpenAI_API_키
GUILD_ID=테스트_서버_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이미지 생성도 API 키 관리가 중요합니다.&lt;br /&gt;키를 코드에 직접 넣으면 안 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 이미지 생성 호출 패키지 준비&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;pip install -U discord.py openai python-dotenv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;API 호출 뒤 받은 이미지 URL을 그대로 채널에 보내는 가장 단순한 형태부터 갑니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 프롬프트를 이미지로 바꾸는 코드&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import os

import discord
from discord import app_commands
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
OPENAI_API_KEY = os.getenv(&quot;OPENAI_API_KEY&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))

ai_client = OpenAI(api_key=OPENAI_API_KEY)
intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;이미지생성&quot;, description=&quot;프롬프트로 이미지를 생성합니다.&quot;, guild=guild)
@app_commands.describe(prompt=&quot;이미지 생성 프롬프트&quot;)
async def generate_image(interaction: discord.Interaction, prompt: str):
    await interaction.response.defer()

    result = ai_client.images.generate(
        model=&quot;gpt-image-1&quot;,
        prompt=prompt,
        size=&quot;1024x1024&quot;,
    )

    image_base64 = result.data[0].b64_json
    file = discord.File(fp=discord.utils._BytesIO(base64.b64decode(image_base64)), filename=&quot;generated.png&quot;)
    await interaction.followup.send(content=f&quot;프롬프트: {prompt}&quot;, file=file)


if not TOKEN or not OPENAI_API_KEY:
    raise ValueError(&quot;환경변수 설정이 비어 있습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위 예제는 이미지 생성 결과를 파일로 변환해 보내는 기본형입니다.&lt;br /&gt;실제로는 `base64`와 `io.BytesIO` import를 같이 넣어야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 빠뜨리기 쉬운 import&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드 맨 위에 아래 두 줄을 추가해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;import base64
from io import BytesIO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 파일 생성 부분은 아래처럼 바꾸면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;file = discord.File(fp=BytesIO(base64.b64decode(image_base64)), filename=&quot;generated.png&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;처음에는 이런 import 누락으로 자주 멈춥니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 짧은 프롬프트부터 출력 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`/이미지생성 prompt:노을이 지는 바다 위 고양이`처럼 실행해 봅니다.&lt;br /&gt;이미지가 생성돼 채널에 올라오면 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 응답 대기와 비용 제한에서 막히는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호출은 되는데 이미지가 안 올라오는 경우 &lt;code&gt;base64&lt;/code&gt; 처리나 &lt;code&gt;BytesIO&lt;/code&gt; import 누락을 먼저 봅니다.&lt;/li&gt;
&lt;li&gt;비용이 너무 빨리 느는 경우 이미지 생성은 텍스트보다 비용 체감이 크므로 전용 채널이나 역할 제한을 둬야 합니다.&lt;/li&gt;
&lt;li&gt;응답이 오래 걸리는 경우 정상일 수 있습니다.&lt;br /&gt;&lt;br /&gt;먼저 &lt;code&gt;defer()&lt;/code&gt;로 응답 유예를 걸어 둬야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 이벤트 채널과 같이 쓰기 좋다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 생성은 혼자 쓰는 기능보다 이벤트와 묶을 때 체감이 더 큽니다.&lt;br /&gt;&lt;br /&gt;짧은 프롬프트 대회, 썸네일 시안 뽑기, 주간 이미지 이벤트처럼 응용하기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 운영은 &lt;a href=&quot;https://blog.dishost.kr/113&quot;&gt;디스코드 이벤트 채널 기획법&lt;/a&gt;과 같이 보면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영과 사용량 제한을 같이 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 생성은 남용되기 쉽습니다.&lt;br /&gt;&lt;br /&gt;역할 제한, 채널 제한, 하루 호출 횟수 제한 같은 기준을 둬야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실운영용 배포가 필요하다면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 상시 실행 환경도 같이 준비해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 이미지 출력 뒤에는 읽기 기능과 채널 분리를 붙인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 생성이 됐다면 다음에는 업로드된 이미지를 읽고 설명하는 Vision 기능으로 넘어갈 수 있습니다.&lt;br /&gt;&lt;br /&gt;그 전에 AI 호출 구조를 다시 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/142&quot;&gt;디스코드 요약 봇 만들기&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/143&quot;&gt;디스코드 번역 봇 만들기&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/145</guid>
      <comments>https://dishost.tistory.com/145#entry145comment</comments>
      <pubDate>Wed, 20 May 2026 17:02:59 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 번역 봇 만들기</title>
      <link>https://dishost.tistory.com/143</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;해외 유저가 질문을 던졌는데 운영진이 매번 브라우저 번역기와 디스코드를 왔다 갔다 하고 있다면, 대화는 이미 한 템포 늦어지고 있는 겁니다.&lt;br /&gt;&lt;br /&gt;번역 봇의 역할은 AI 데모를 보여 주는 것이 아니라 다국어 서버에서 응답 속도를 늦추지 않는 보조 레이어를 만드는 데 있습니다.&lt;br /&gt;&lt;br /&gt;이 글에서는 슬래시 명령어로 문장을 입력받아 OpenAI API로 한국어 또는 영어 번역 결과를 돌려주는 가장 작은 다국어 응답 보조 봇을 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 번역 봇은 다국어 응답 병목이 생기는 채널에 붙인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 짧은 문장을 빠르게 확인해야 하는 곳에서 잘 맞습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해외 유저 문의 응답&lt;/li&gt;
&lt;li&gt;다국어 공지 초안 확인&lt;/li&gt;
&lt;li&gt;게임 커뮤니티 대화 보조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 기반 텍스트 처리 구조는 &lt;a href=&quot;https://blog.dishost.kr/142&quot;&gt;디스코드 요약 봇 만들기&lt;/a&gt;와 거의 비슷합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 번역 대상 채널과 API 값 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
OPENAI_API_KEY=여기에_OpenAI_API_키
GUILD_ID=테스트_서버_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;번역도 결국 외부 API 호출이므로 키 분리는 필수입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 번역 호출 패키지부터 맞춘다&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;pip install -U discord.py openai python-dotenv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;AI 기능 글들을 따라왔다면 이미 설치돼 있을 가능성이 큽니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 입력 언어를 받아 번역하는 코드&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import os

import discord
from discord import app_commands
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
OPENAI_API_KEY = os.getenv(&quot;OPENAI_API_KEY&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))

ai_client = OpenAI(api_key=OPENAI_API_KEY)
intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;번역&quot;, description=&quot;입력한 문장을 번역합니다.&quot;, guild=guild)
@app_commands.describe(text=&quot;번역할 문장&quot;, target=&quot;ko 또는 en&quot;)
async def translate_text(interaction: discord.Interaction, text: str, target: str):
    target = target.lower().strip()
    if target not in {&quot;ko&quot;, &quot;en&quot;}:
        await interaction.response.send_message(&quot;target 값은 ko 또는 en만 사용할 수 있습니다.&quot;, ephemeral=True)
        return

    await interaction.response.defer(ephemeral=True)
    target_language = &quot;한국어&quot; if target == &quot;ko&quot; else &quot;영어&quot;

    response = ai_client.responses.create(
        model=&quot;gpt-4.1-mini&quot;,
        input=[
            {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: f&quot;입력 문장을 자연스럽고 짧게 {target_language}로 번역하세요.&quot;},
            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: text},
        ],
    )

    translated = response.output_text.strip()
    embed = discord.Embed(title=&quot;번역 결과&quot;, color=discord.Color.green())
    embed.add_field(name=&quot;원문&quot;, value=text[:1024], inline=False)
    embed.add_field(name=&quot;번역문&quot;, value=translated[:1024], inline=False)
    await interaction.followup.send(embed=embed, ephemeral=True)


if not TOKEN or not OPENAI_API_KEY:
    raise ValueError(&quot;환경변수 설정이 비어 있습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;기본형은 문장을 입력받고 목표 언어만 정하면 끝입니다.&lt;br /&gt;처음에는 한국어와 영어 두 방향만 잡아도 충분합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 짧은 문장과 긴 문장을 같이 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`/번역 text:Hello target:ko`처럼 실행해 결과를 확인합니다.&lt;br /&gt;한국어를 영어로, 영어를 한국어로 양방향 테스트해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 언어 판별과 길이 제한에서 막히는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;번역이 너무 길거나 말투가 과한 경우 시스템 프롬프트를 더 짧고 건조하게 조정해야 합니다.&lt;/li&gt;
&lt;li&gt;OpenAI 호출 자체가 실패하는 경우 API 키, 모델 이름, 사용량 제한을 먼저 봅니다.&lt;/li&gt;
&lt;li&gt;슬래시 명령어가 안 보이는 경우 서버 sync와 명령어 스코프를 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 번역은 공지보다 문의 응답 속도에서 먼저 체감된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다국어 공지 초안을 빠르게 만들거나, 짧은 문의 답변을 번역해 줄 때 특히 편합니다.&lt;br /&gt;&lt;br /&gt;완벽한 번역보다 운영진이 응답을 끊기지 않게 이어 가는 데 더 큰 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공지 흐름은 &lt;a href=&quot;https://blog.dishost.kr/106&quot;&gt;디스코드 공지 채널 운영법&lt;/a&gt;, 문의 흐름은 &lt;a href=&quot;https://blog.dishost.kr/122&quot;&gt;디스코드 문의 동선 설계법&lt;/a&gt;을 같이 보면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 민감한 문구는 자동 번역에 최종 판단을 맡기지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역 결과가 항상 정확하다고 가정하면 곤란합니다.&lt;br /&gt;&lt;br /&gt;운영 정책, 제재 안내, 결제 관련 문구는 최종 확인을 사람이 맡아야 합니다.&lt;br /&gt;&lt;br /&gt;번역 봇은 속도를 보조하는 도구이지 책임을 넘기는 장치는 아닙니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영과 비용도 같이 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역 봇도 호출량이 늘면 비용이 늘어납니다.&lt;br /&gt;&lt;br /&gt;자주 쓰는 서버라면 명령어 전용 채널을 따로 두거나, 짧은 문장만 받게 제한해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포가 필요하다면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 상시 실행 환경을 같이 준비하면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 번역은 문장과 문의 동선이 받쳐 줘야 쓸모가 난다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.dishost.kr/106&quot;&gt;디스코드 공지 채널 운영법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/122&quot;&gt;디스코드 문의 동선 설계법&lt;/a&gt;을 같이 보면 번역봇이 들어갈 위치와 문장 톤이 함께 정리됩니다.&lt;br /&gt;&lt;br /&gt;번역 품질보다 AI 호출 구조와 프롬프트 방향을 더 손보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/142&quot;&gt;디스코드 요약 봇 만들기&lt;/a&gt;를 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/143</guid>
      <comments>https://dishost.tistory.com/143#entry143comment</comments>
      <pubDate>Mon, 18 May 2026 17:02:06 +0900</pubDate>
    </item>
    <item>
      <title>2026년 기준, 디스코드 유용한 봇 추천 (MEE6, Dyno, Carlbot, Zira, The Patch...)</title>
      <link>https://dishost.tistory.com/14</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드는 2026년 현재 단순한 게이머용 애플리케이션의 범주를 벗어나 다양한 분야의 커뮤니케이션 플랫폼으로 자리매김하였습니다. 기업 내부 커뮤니케이션, 학습 커뮤니티, 취미 모임 등 다양한 목적으로 활용되면서 디스코드 서버의 기능적 확장이 필수적인 요소가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 봇은 단순 자동화 도구를 넘어 서버의 정체성과 분위기를 좌우하는 중요한 요소입니다. 올바른 봇 선택은 서버 관리자의 피로도를 현저히 낮추고, 멤버들의 참여도와 만족도를 높이는 핵심 전략이 됩니다. 이 글에서는 2026년 현재 유용한 디스코드 봇 8가지와 각각의 효과적인 활용법을 상세히 다루고자 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2026년 디스코드 유용한 봇 추천&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. MEE6&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MEE6는 2019년 첫 등장 이후 꾸준히 발전을 거듭하며 디스코드 봇 시장의 표준을 제시해왔습니다. 이미 많은 서버에서 활용되고 있으며, 2026년에는 더욱 고도화된 기능을 갖춘 MEE6 2025 에디션이 출시되었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 기능&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;레벨링 시스템&lt;/b&gt;: 다양한 활동에 따른 경험치 부여와 자동 역할 지급&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여러 관리자 명령어&lt;/b&gt;: 서버 관리에 필요한 다양한 명령어 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설치 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MEE6 &lt;a href=&quot;https://mee6.xyz/&quot;&gt;공식 웹사이트&lt;/a&gt;에 접속하여 &quot;Add to Discord&quot; 버튼을 클릭한 후 안내에 따라 서버에 봇을 추가하세요! 웹 대시보드에서 원하는 기능을 활성화하고 설정할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Dyno Bot&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dyno Bot은 디스코드 초창기부터 MEE6와 양대 산맥을 이루며 발전해온 서버 관리 봇의 대표주자입니다. 서버 관리를 자동화하여 관리자의 부담을 덜어주는 다양한 기능을 제공합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 기능&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;고급 자동화 워크플로우&lt;/b&gt;: 사용자 지정 트리거와 조건부 실행 자동화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다중 단계 경고 시스템&lt;/b&gt;: 단계별 제재 체계와 누진 제재 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설치 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dyno.gg/&quot;&gt;Dyno 공식 웹사이트&lt;/a&gt;에 접속하여 &quot;Add to Server&quot; 버튼을 클릭하십시오. 서버 선택 후 권한을 승인하면 웹 대시보드에서 필요한 모듈을 활성화할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Carl-bot&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Carl-bot은 반응 역할(Reaction Roles)에 특화된 봇으로, 역할 관리와 모더레이션에 탁월한 기능을 제공합니다. 특히 강력한 로깅 시스템과 태그 기능이 특징입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 기능&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;고급 반응 역할&lt;/b&gt;: 다중 패널, 토글와, 일회성, 임시 역할 등 다양한 반응 역할 옵션&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모듈식 로그 시스템&lt;/b&gt;: 60개 이상의 이벤트 유형에 대한 세부적인 로깅&lt;/li&gt;
&lt;li&gt;&lt;b&gt;강력한 태그 시스템&lt;/b&gt;: 서버별 맞춤 명령어 응답 생성 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설치 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://carl.gg/&quot;&gt;Carl-bot 공식 웹사이트&lt;/a&gt;에 접속한 뒤, &quot;Invite&quot; 버튼을 클릭하여 서버에 추가할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Zira&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zira는 반응 기반 역할 관리에 특화된 봇으로, 직관적인 인터페이스와 강력한 사용자 지정 옵션을 제공합니다. 특히 대규모 서버에서 자동화된 역할 분배를 효율적으로 처리합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 기능&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;채널별 반응 역할&lt;/b&gt;: 여러 채널에 걸쳐 다양한 반응 역할 메시지 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;역할 토글 기능&lt;/b&gt;: 하나의 반응으로 여러 역할을 동시에 추가/제거&lt;/li&gt;
&lt;li&gt;&lt;b&gt;역할 검증 시스템&lt;/b&gt;: 특정 역할 보유 여부에 따른 조건부 역할 지급&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설치 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://zira.gg/&quot;&gt;Zira 공식 웹사이트&lt;/a&gt;에 접속하여 &quot;Invite&quot; 버튼을 클릭하십시오. 서버 추가 후 &lt;code&gt;z/setup&lt;/code&gt; 명령어를 통해 반응 역할 설정을 시작할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 티토커 TTS 봇&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티토커 TTS 봇은 텍스트를 음성으로 변환하는 기능을 제공하여, 서버 내에서 음성 채널과 텍스트 채널 간의 경계를 허물어줍니다. 마이크를 쓰는 것을 꺼려하는 사용자에게 유용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 기능&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;다양한 음성 지원&lt;/b&gt;: 여러 언어와 음성 옵션 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 지정 음성&lt;/b&gt;: 개인화된 음성 설정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설치 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://discord.com/oauth2/authorize?client_id=941683178920378439&amp;amp;scope=bot%20applications.commands&amp;amp;permissions=298512084048&quot;&gt;티토커 봇 초대 링크&lt;/a&gt;에서 &quot;Add to Server&quot; 버튼을 클릭하여 서버에 추가할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. The Patch&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The Patch는 게임 업데이트와 패치 노트에 특화된 봇으로, 인기 게임에 대한 최신 업데이트 정보를 실시간으로 제공합니다. 2025년 버전에서는 AI 기반 패치 노트 분석 기능이 추가되었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 기능&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;게임 업데이트 알림&lt;/b&gt;: 선택한 게임의 패치와 업데이트 실시간 알림&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패치 노트 요약&lt;/b&gt;: 긴 패치 노트를 주요 변경사항 중심으로 간결하게 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설치 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://thepatch.notepads.kr/&quot;&gt;The Patch 공식 웹사이트&lt;/a&gt;에서 &quot;초대하기&quot; 버튼을 클릭하세요. 서버 추가 후 &lt;code&gt;/알림설정&lt;/code&gt;, &lt;code&gt;/채널설정&lt;/code&gt; 명령어를 통해 관심 있는 게임을 설정하고 알림을 받을 채널을 지정할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 노래하는 하리보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노래하는 하리보 봇은 현재 우리나라에서 가장 많은 서버에서 사용되는 디스코드 봇입니다. 이 봇은 음악 재생 기능을 중심으로 다양한 엔터테인먼트 기능을 제공합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 기능&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;음악 재생&lt;/b&gt;: YouTube, Spotify 등 다양한 플랫폼에서 음악 재생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;음악 큐 관리&lt;/b&gt;: 사용자 지정 큐와 재생 목록 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설치 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://haribosinging.github.io/&quot;&gt;노래하는 하리보 공식 웹사이트&lt;/a&gt;에 접속하여 &quot;서버에 초대하기&quot; 버튼을 클릭한 후 안내에 따라 서버에 봇을 추가하세요!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 알로항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알로항 봇 역시 노래하는 하리보 봇과 같이 음악 재생 기능을 중심으로 다양한 엔터테인먼트 기능을 제공합니다. 특히, 알로항은 사용자 친화적인 인터페이스로 많은 사랑을 받고 있습니다. 프리미엄 기능을 통해 안정적인 음악 재생 역시 지원합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 기능&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;음악 재생&lt;/b&gt;: YouTube, Spotify 등 다양한 플랫폼에서 음악 재생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;음악 큐 관리&lt;/b&gt;: 사용자 지정 큐와 재생 목록 기능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프리미엄 기능&lt;/b&gt;: 안정적인 음악 재생과 추가 기능 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설치 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://koreanbots.dev/bots/826698986970677278/invite&quot;&gt;알로항 초대 링크&lt;/a&gt;에 접속하여 &quot;서버에 초대하기&quot; 버튼을 클릭한 후 안내에 따라 서버에 봇을 추가하세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7qURH/btsNIdi13KT/a3W6FHYkzOE6bfVGiZwA8k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7qURH/btsNIdi13KT/a3W6FHYkzOE6bfVGiZwA8k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7qURH/btsNIdi13KT/a3W6FHYkzOE6bfVGiZwA8k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7qURH%2FbtsNIdi13KT%2Fa3W6FHYkzOE6bfVGiZwA8k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;400&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>디스코드</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/14</guid>
      <comments>https://dishost.tistory.com/14#entry14comment</comments>
      <pubDate>Mon, 18 May 2026 14:21:27 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 경제 봇 만들기</title>
      <link>https://dishost.tistory.com/141</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;코인 기능부터 붙였는데 며칠 지나지 않아 잔액만 불어나고 운영진 지급 명령까지 아무렇게나 열려 있다면, 그 경제 봇은 재미 장치가 아니라 밸런스 사고 예고편입니다.&lt;br /&gt;&lt;br /&gt;경제 봇의 핵심은 화려한 상점이 아니라 보상을 어디서 만들고 누가 통제하느냐를 먼저 잠그는 데 있습니다.&lt;br /&gt;&lt;br /&gt;이 글에서는 출석으로 코인을 적립하고 잔액을 확인하는 가장 작은 경제 구조를 만들면서, 왜 입문 단계에서는 적립 경로와 운영진 통제가 먼저 필요한지도 같이 봅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 경제 봇은 재미보다 보상 회계가 먼저다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 커뮤니티, 길드 서버, 출석 이벤트형 커뮤니티에서 특히 자주 씁니다.&lt;br /&gt;&lt;br /&gt;활동 자체에 작은 보상을 붙이면 재방문 이유를 만들기 쉽기 때문입니다.&lt;br /&gt;&lt;br /&gt;다만 이 보상이 어디서 생기고 어디서 새는지 추적할 수 있어야 운영이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출석 기능을 먼저 만들어 두었다면 경제 구조를 얹기 훨씬 편합니다.&lt;br /&gt;&lt;br /&gt;출석 기본형은 &lt;a href=&quot;https://blog.dishost.kr/139&quot;&gt;디스코드 출석 체크 봇 만들기&lt;/a&gt;에서 다뤘습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 적립 경로를 하나만 열어 두고 시작한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경제 봇을 처음 만들 때 상점, 송금, 도박, 아이템까지 한 번에 넣기 쉽습니다.&lt;br /&gt;&lt;br /&gt;그렇게 시작하면 오류 포인트가 급격히 늘어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입문 단계에서는 아래 두 가지면 충분합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코인 적립&lt;/li&gt;
&lt;li&gt;코인 잔액 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적립 경로를 출석 하나로 제한해 두면 중복 지급, 명령어 악용, 밸런스 붕괴 지점을 훨씬 쉽게 추적할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 지급 명령은 테스트용이라도 먼저 잠가 둘 생각을 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에는 &lt;code&gt;/코인지급&lt;/code&gt;가 들어 있지만, 이건 어디까지나 테스트용 확인 장치입니다.&lt;br /&gt;&lt;br /&gt;실운영에서는 운영진 역할 체크 없이 두면 가장 먼저 악용되는 지점이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경제 봇은 잔액 기능보다 지급 권한 통제가 먼저입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 서버 테스트 값 먼저 넣기&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
GUILD_ID=테스트_서버_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 글에서는 슬래시 명령어 세 개로 기본 경제 구조를 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 출석 적립과 잔액 조회를 묶는 최소 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os
from datetime import datetime
from collections import defaultdict

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))
wallets = defaultdict(lambda: {&quot;coins&quot;: 0, &quot;last_attendance&quot;: &quot;&quot;})

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;출석보상&quot;, description=&quot;오늘 출석 보상 코인을 받습니다.&quot;, guild=guild)
async def daily_reward(interaction: discord.Interaction):
    today = datetime.now().strftime(&quot;%Y-%m-%d&quot;)
    wallet = wallets[interaction.user.id]

    if wallet[&quot;last_attendance&quot;] == today:
        await interaction.response.send_message(&quot;오늘은 이미 출석 보상을 받았습니다.&quot;, ephemeral=True)
        return

    wallet[&quot;last_attendance&quot;] = today
    wallet[&quot;coins&quot;] += 100
    await interaction.response.send_message(
        f&quot;출석 보상 지급 완료. 현재 잔액은 {wallet['coins']} 코인입니다.&quot;,
        ephemeral=True,
    )


@tree.command(name=&quot;잔액&quot;, description=&quot;현재 코인 잔액을 확인합니다.&quot;, guild=guild)
async def balance(interaction: discord.Interaction):
    wallet = wallets[interaction.user.id]
    await interaction.response.send_message(f&quot;현재 잔액은 {wallet['coins']} 코인입니다.&quot;, ephemeral=True)


@tree.command(name=&quot;코인지급&quot;, description=&quot;테스트용으로 코인을 지급합니다.&quot;, guild=guild)
@app_commands.describe(user=&quot;코인을 받을 유저&quot;, amount=&quot;지급할 코인&quot;)
async def add_coins(interaction: discord.Interaction, user: discord.Member, amount: int):
    wallets[user.id][&quot;coins&quot;] += amount
    await interaction.response.send_message(f&quot;{user.mention} 님에게 {amount} 코인을 지급했습니다.&quot;, ephemeral=True)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;경제 봇 기본형은 결국 유저별 잔액 저장입니다.&lt;br /&gt;거기에 출석 보상 같은 적립 경로를 하나 얹으면 최소 기능은 바로 완성됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 적립과 지급이 어떻게 갈리는지 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`/출석보상`, `/잔액`, `/코인지급`을 순서대로 실행하면서 잔액이 변하는지 확인합니다.&lt;br /&gt;같은 날 출석 보상이 중복 지급되지 않는지도 같이 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 저장 구조와 지급 통제에서 자주 막히는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재시작 뒤 코인이 사라지는 경우 메모리 저장 구조라 정상입니다.&lt;br /&gt;&lt;br /&gt;운영 단계에서는 파일 저장이나 DB를 붙여야 합니다.&lt;/li&gt;
&lt;li&gt;아무나 &lt;code&gt;/코인지급&lt;/code&gt;를 쓰는 경우 운영진 역할 체크가 비어 있을 수 있습니다.&lt;br /&gt;&lt;br /&gt;테스트용 지급 명령은 편하지만 그대로 운영에 들어가면 가장 먼저 새는 구간입니다.&lt;/li&gt;
&lt;li&gt;슬래시 명령어가 늦게 보이는 경우 서버 sync와 명령어 스코프를 먼저 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 경제 구조는 참여 장치 위에 얹을 때 안정적이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경제 봇이 혼자 있으면 숫자만 쌓이고 왜 모으는지 흐려질 수 있습니다.&lt;br /&gt;&lt;br /&gt;출석과 레벨링 위에 얹으면 보상 흐름이 더 자연스러워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참여 구조는 &lt;a href=&quot;https://blog.dishost.kr/139&quot;&gt;디스코드 출석 체크 봇 만들기&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/138&quot;&gt;디스코드 레벨링 봇 만들기&lt;/a&gt;와 같이 보면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 인플레이션을 못 잡을 구조는 빨리 잘라낸다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이템 구매, 송금, 강화, 확률형 기능을 한 번에 넣으면 오류 지점이 많아집니다.&lt;br /&gt;&lt;br /&gt;먼저 잔액과 적립 흐름이 안정적인지부터 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 코인이 계속 쌓이기만 하고 소모처가 없으면 숫자만 커지고 재미는 오히려 빨리 떨어집니다.&lt;br /&gt;&lt;br /&gt;이후 상점 기능을 붙일 계획이라면 지급량과 사용처를 같이 설계해야 하고, 아니면 작은 보상 시스템으로 선을 그어야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 보상 구조는 참여 장치와 함께 굴릴 때 산다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경제 구조도 결국 하루 어느 시간에든 요청이 들어옵니다.&lt;br /&gt;&lt;br /&gt;로컬 PC를 끄면 봇이 같이 멈춥니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실운영용이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 계속 켜진 환경이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.dishost.kr/139&quot;&gt;디스코드 출석 체크 봇 만들기&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/138&quot;&gt;디스코드 레벨링 봇 만들기&lt;/a&gt;을 함께 보면 적립 리듬과 장기 활동 흔적을 한 번에 정리할 수 있습니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/141</guid>
      <comments>https://dishost.tistory.com/141#entry141comment</comments>
      <pubDate>Sat, 16 May 2026 17:01:13 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 출석 체크 봇 만들기</title>
      <link>https://dishost.tistory.com/139</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;출석 기능은 단순해 보여도 실제 운영에서는 리셋 시각, 중복 방지, 보상 기준이 바로 붙습니다.&lt;br /&gt;&lt;br /&gt;그래서 &lt;code&gt;/출석&lt;/code&gt; 명령어 하나만 만들고 끝내면 금방 &quot;자정 기준이 뭐죠&quot;, &quot;재시작하니 기록이 사라졌어요&quot; 같은 질문이 나옵니다.&lt;br /&gt;&lt;br /&gt;이 글에서는 슬래시 명령어로 하루 1회 출석을 기록하고, 누적 출석 수를 보여 주는 기본형을 만들면서 어디서부터 운영형으로 갈라지는지도 같이 정리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 출석 기능은 리셋 기준부터 정해야 덜 꼬인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복 방문을 유도하고 싶을 때 잘 맞습니다.&lt;br /&gt;&lt;br /&gt;예를 들면 아래 상황입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스터디 서버 출석 확인&lt;/li&gt;
&lt;li&gt;게임 길드 일일 참여 체크&lt;/li&gt;
&lt;li&gt;커뮤니티 이벤트용 일일 보상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레벨링처럼 활동량 전체를 보는 기능과는 성격이 조금 다릅니다.&lt;br /&gt;&lt;br /&gt;레벨링은 &lt;a href=&quot;https://blog.dishost.kr/138&quot;&gt;디스코드 레벨링 봇 만들기&lt;/a&gt;에서 먼저 정리했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 하루 1회보다 리셋 시각이 더 중요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출석 기능은 기준이 명확해야 합니다.&lt;br /&gt;&lt;br /&gt;같은 날 여러 번 인정되면 바로 의미가 사라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 날짜 문자열 기준으로 하루 1회만 허용하는 구조가 가장 단순합니다.&lt;br /&gt;&lt;br /&gt;다만 실운영에서는 서버 기준 시간이 한국 시간인지, UTC 기준인지 먼저 정해 둬야 합니다.&lt;br /&gt;&lt;br /&gt;특히 자정 직전과 직후에 출석하는 유저가 많은 서버에서는 이 기준을 안내문에 같이 적어 두지 않으면 바로 분쟁이 생깁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 출석 채널과 저장 값 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
GUILD_ID=테스트_서버_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 글에서는 가장 기본형으로 슬래시 명령어 두 개만 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 날짜별 출석을 기록하는 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os
from datetime import datetime
from collections import defaultdict

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))
attendance_data = defaultdict(lambda: {&quot;last_date&quot;: &quot;&quot;, &quot;count&quot;: 0})

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;출석&quot;, description=&quot;오늘 출석을 기록합니다.&quot;, guild=guild)
async def check_attendance(interaction: discord.Interaction):
    today = datetime.now().strftime(&quot;%Y-%m-%d&quot;)
    data = attendance_data[interaction.user.id]

    if data[&quot;last_date&quot;] == today:
        await interaction.response.send_message(&quot;오늘은 이미 출석을 완료했습니다.&quot;, ephemeral=True)
        return

    data[&quot;last_date&quot;] = today
    data[&quot;count&quot;] += 1
    await interaction.response.send_message(
        f&quot;출석 완료. 현재 누적 출석은 {data['count']}회입니다.&quot;,
        ephemeral=True,
    )


@tree.command(name=&quot;출석정보&quot;, description=&quot;내 출석 현황을 봅니다.&quot;, guild=guild)
async def attendance_info(interaction: discord.Interaction):
    data = attendance_data[interaction.user.id]
    await interaction.response.send_message(
        f&quot;누적 출석: {data['count']}회\n마지막 출석일: {data['last_date'] or '없음'}&quot;,
        ephemeral=True,
    )


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;핵심은 유저별 마지막 출석 날짜와 누적 횟수를 저장하는 부분입니다.&lt;br /&gt;가장 작은 형태로 시작하면 이해가 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 중복 출석과 리셋 시각 점검&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`/출석`을 한 번 실행하고, 같은 날 다시 실행해 중복 출석이 막히는지 확인합니다.&lt;br /&gt;그다음 `/출석정보`로 누적 값이 보이는지 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 날짜 기준에서 자주 엉키는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 날 여러 번 출석되는 경우 날짜 비교 로직이 빠졌을 수 있습니다.&lt;/li&gt;
&lt;li&gt;재시작 뒤 기록이 사라지는 경우 메모리 저장 구조라 정상입니다.&lt;br /&gt;&lt;br /&gt;실운영에서는 파일 저장이나 DB 저장을 붙여야 합니다.&lt;/li&gt;
&lt;li&gt;슬래시 명령어가 안 보이는 경우 서버 sync나 &lt;code&gt;applications.commands&lt;/code&gt; 스코프를 먼저 봐야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 주간 보상보다 기록 안정화가 먼저다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 코인 지급, 역할 보상, 주간 랭킹까지 한 번에 넣지 않아도 됩니다.&lt;br /&gt;&lt;br /&gt;먼저 출석 기록이 안정적으로 쌓이는지 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 많이 막히는 부분은 기능 추가보다 저장 방식입니다.&lt;br /&gt;&lt;br /&gt;메모리 저장으로는 재시작 뒤 출석 이력이 바로 사라지기 때문에, 출석을 이벤트나 보상에 연결할 계획이라면 파일 저장이나 DB 전환 시점을 초반에 잡아 둬야 안전합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 참여 유도 기능과 묶기 좋다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출석 기능은 투표, 이벤트, 레벨링과 묶을 때 체감이 더 좋습니다.&lt;br /&gt;&lt;br /&gt;서버에 하루 한 번 들어올 이유를 만들기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 흐름은 &lt;a href=&quot;https://blog.dishost.kr/113&quot;&gt;디스코드 이벤트 채널 기획법&lt;/a&gt;, 활동 보상 흐름은 &lt;a href=&quot;https://blog.dishost.kr/138&quot;&gt;디스코드 레벨링 봇 만들기&lt;/a&gt;와 같이 보면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영 환경이 필요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출석은 하루 어느 시간에든 찍힐 수 있습니다.&lt;br /&gt;&lt;br /&gt;로컬 PC를 꺼 두면 출석 기능도 멈춥니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실운영용이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 계속 켜진 환경이 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 출석 다음에는 보상보다 참여 채널을 먼저 손본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출석이 안정적으로 쌓이기 시작하면 그다음은 보상과 비교 지표를 붙이는 순서가 자연스럽습니다.&lt;br /&gt;&lt;br /&gt;활동 보상 흐름을 넓혀 볼 때는 &lt;a href=&quot;https://blog.dishost.kr/138&quot;&gt;디스코드 레벨링 봇 만들기&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/113&quot;&gt;디스코드 이벤트 채널 기획법&lt;/a&gt;을 같이 보면 참여 기능이 어떻게 서로 다른 역할을 하는지 감이 잘 잡힙니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/139</guid>
      <comments>https://dishost.tistory.com/139#entry139comment</comments>
      <pubDate>Thu, 14 May 2026 17:00:19 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 레벨링 봇 만들기</title>
      <link>https://dishost.tistory.com/138</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;채팅이 조용한 서버를 살리겠다고 레벨링부터 붙였는데, 정작 늘어나는 건 짧은 도배 메시지뿐이라면 설계 방향이 잘못된 겁니다.&lt;br /&gt;&lt;br /&gt;레벨링의 핵심은 숫자를 올리는 기능이 아니라 멤버가 오래 머물 이유를 만드는 활동 루프입니다.&lt;br /&gt;&lt;br /&gt;이 글에서는 메시지 활동을 기준으로 경험치를 쌓고 일정 구간마다 레벨업 안내를 보내는 가장 작은 레벨링 구조를 만들면서, 왜 쿨타임과 알림 채널이 먼저 필요한지도 같이 봅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 레벨링은 장기 활동 리듬을 만들 때 붙인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반 서버에서는 멤버가 다시 말을 걸 이유가 부족해서 채팅이 쉽게 식습니다.&lt;br /&gt;&lt;br /&gt;레벨링은 그 공백을 메우는 장치입니다.&lt;br /&gt;&lt;br /&gt;특히 질문, 잡담, 커뮤니티형 서버에서 장기 활동 흔적을 만들 때 자주 씁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 아래 목적에 잘 맞습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채팅 참여 유도&lt;/li&gt;
&lt;li&gt;장기 활동 유저 구분&lt;/li&gt;
&lt;li&gt;역할 보상이나 이벤트 기준 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 보상까지 연결하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/116&quot;&gt;반응 역할 디스코드 봇 만들기&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/115&quot;&gt;자동 역할 부여 디스코드 봇 만들기&lt;/a&gt;을 같이 보면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 경험치는 너무 자주 주면 도배만 남는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 하나마다 바로 경험치를 주면 도배가 심해질 수 있습니다.&lt;br /&gt;&lt;br /&gt;그래서 보통은 쿨타임을 둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 30초에 한 번만 경험치를 주는 식이 무난합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 경험치 채널과 역할 값 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
LEVEL_UP_CHANNEL_ID=레벨업알림채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;레벨업 알림을 모든 채널에 뿌리지 않으려면 전용 채널이나 지정 채널 하나를 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 메시지당 경험치를 누적하는 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os
import time
from collections import defaultdict

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
LEVEL_UP_CHANNEL_ID = int(os.getenv(&quot;LEVEL_UP_CHANNEL_ID&quot;))
COOLDOWN_SECONDS = 30
xp_data = defaultdict(lambda: {&quot;xp&quot;: 0, &quot;level&quot;: 1, &quot;last_gain&quot;: 0.0})

intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
intents.guilds = True
client = discord.Client(intents=intents)


def xp_needed(level: int) -&amp;gt; int:
    return level * 100


@client.event
async def on_ready():
    print(f&quot;로그인 성공: {client.user}&quot;)


@client.event
async def on_message(message: discord.Message):
    if message.author.bot or message.guild is None:
        return

    data = xp_data[message.author.id]
    now = time.time()

    if now - data[&quot;last_gain&quot;] &amp;lt; COOLDOWN_SECONDS:
        return

    data[&quot;last_gain&quot;] = now
    data[&quot;xp&quot;] += 15

    target_xp = xp_needed(data[&quot;level&quot;])
    if data[&quot;xp&quot;] &amp;lt; target_xp:
        return

    data[&quot;xp&quot;] -= target_xp
    data[&quot;level&quot;] += 1

    channel = message.guild.get_channel(LEVEL_UP_CHANNEL_ID)
    if isinstance(channel, discord.TextChannel):
        await channel.send(f&quot;{message.author.mention} 님이 레벨 {data['level']}이 되었습니다.&quot;)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;레벨링 기본형은 의외로 단순합니다.&lt;br /&gt;메시지 활동을 집계하고, 일정 수치가 차면 레벨을 올리면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 먼저 정할 것은 숫자가 아니라 보상 연결 시점이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레벨업을 어디에 연결할지 먼저 정해야 합니다.&lt;br /&gt;&lt;br /&gt;단순 재미인지, 활동 배지인지, 이벤트 참여 기준인지에 따라 봇 성격이 완전히 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무 용도 없이 숫자만 올리면 초반엔 반응이 와도 오래 못 갑니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 경험치 누적과 쿨다운부터 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;테스트 채널에서 몇 번 메시지를 보내고, 30초 정도 간격을 두고 다시 보내 보면서 레벨업 알림이 뜨는지 확인합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 레벨 계산에서 자주 틀리는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경험치가 너무 빨리 오르는 경우 쿨타임이 없거나 너무 짧을 수 있습니다.&lt;/li&gt;
&lt;li&gt;레벨업 알림이 안 보이는 경우 알림 채널 ID가 틀리거나 봇이 채널을 못 볼 수 있습니다.&lt;/li&gt;
&lt;li&gt;재시작하면 기록이 사라지는 경우 지금 구조는 메모리 저장이라 정상입니다.&lt;br /&gt;&lt;br /&gt;운영 단계에서는 파일이나 DB 저장을 붙여야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 레벨링은 안티스팸과 한 세트로 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레벨링을 붙이면 일부 유저는 대화보다 경험치 파밍에 맞춰 움직이기 시작합니다.&lt;br /&gt;&lt;br /&gt;짧은 메시지 반복이 늘어나는 순간 레벨링은 참여 장치가 아니라 채팅 품질 저하 장치가 됩니다.&lt;br /&gt;&lt;br /&gt;그래서 레벨링과 안티스팸은 보통 같이 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 &lt;a href=&quot;https://blog.dishost.kr/131&quot;&gt;스팸 방지 디스코드 봇 만들기&lt;/a&gt;와 묶어서 보면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 역할 보상은 나중에 붙여도 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 레벨 5, 10, 20마다 역할 지급까지 넣지 않아도 됩니다.&lt;br /&gt;&lt;br /&gt;우선 레벨 계산과 알림이 잘 도는지부터 확인해야 빠릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 연동은 나중에 자동 역할 로직과 섞는 방식이 안정적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 레벨링 다음에는 활동 무대를 같이 손본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레벨링은 &lt;a href=&quot;https://blog.dishost.kr/131&quot;&gt;스팸 방지 디스코드 봇 만들기&lt;/a&gt;, &lt;a href=&quot;https://blog.dishost.kr/113&quot;&gt;디스코드 이벤트 채널 기획법&lt;/a&gt;, &lt;a href=&quot;https://blog.dishost.kr/102&quot;&gt;디스코드 서버 꾸미기, 채널 역할 배치 가이드&lt;/a&gt;를 함께 봐야 실제 참여 구조로 굳습니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/138</guid>
      <comments>https://dishost.tistory.com/138#entry138comment</comments>
      <pubDate>Wed, 13 May 2026 16:59:52 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 서버 초대가 검색되지 않을 때 해결법</title>
      <link>https://dishost.tistory.com/137</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;초대 링크는 정상인데 서버가 검색이나 공개 노출에서 잘 안 보인다고 느끼는 경우가 있습니다.&lt;br /&gt;&lt;br /&gt;이 문제는 초대 링크 자체의 고장과는 다른 이야기입니다.&lt;br /&gt;&lt;br /&gt;대부분은 커뮤니티 설정, 서버 공개 정책, 외부 디렉터리 기대치가 섞이면서 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 검색되지 않는다고 느껴질 때는 먼저 &lt;code&gt;링크 문제&lt;/code&gt;와 &lt;code&gt;공개 노출 문제&lt;/code&gt;를 나눠서 봐야 빠릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 초대 링크가 열린다고 검색 노출이 보장되지는 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이 헷갈리는 부분입니다.&lt;br /&gt;&lt;br /&gt;초대 링크는 직접 접근용이고, 검색 노출은 별도 기능과 조건을 탑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 아래 둘은 다른 문제입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;링크를 아는 사람만 들어오는 상태&lt;/li&gt;
&lt;li&gt;디스코드 안팎에서 서버가 더 넓게 노출되는 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크 자체 오류는 &lt;a href=&quot;https://blog.dishost.kr/136&quot;&gt;디스코드 초대 링크가 만료되거나 작동하지 않을 때 해결법&lt;/a&gt;으로 먼저 봐야 맞습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 커뮤니티 서버 전환 여부를 먼저 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개 기능을 쓰려면 기본 서버보다 커뮤니티 서버 설정이 먼저 필요한 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;아직 커뮤니티 서버가 아니라면 관련 기능이 아예 안 보일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 설정에서 아래를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;서버 설정 -&amp;gt; 커뮤니티 활성화
- 규칙 또는 가이드 채널 설정
- 커뮤니티 업데이트 채널 설정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 기본 설정이 빠지면 공개 노출 관련 단계로 넘어가기 어렵습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 공개 디렉터리 노출은 조건이 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 운영자는 커뮤니티로만 바꾸면 자동으로 검색되는 줄 아는데 그렇지 않습니다.&lt;br /&gt;&lt;br /&gt;공개 디렉터리나 추천 노출은 별도 기준을 타고, 지역이나 정책에 따라 체감이 다를 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;검색이 안 된다&lt;/code&gt;를 단순 버그로만 보면 헷갈립니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 서버 입구 품질이 먼저 받쳐 줘야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개 노출만 늘려도 입구 구조가 약하면 유입이 그대로 빠집니다.&lt;br /&gt;&lt;br /&gt;실제로는 검색 노출보다 소개문, 공지, 온보딩 구조가 더 크게 작동할 때가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 &lt;a href=&quot;https://blog.dishost.kr/111&quot;&gt;디스코드 서버 소개문 작성법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/103&quot;&gt;디스코드 서버 온보딩 채널 구성 예시&lt;/a&gt;를 같이 보면 정리하기 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 서버 이름과 소개문도 같이 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 체감이 약한 서버를 보면 이름과 소개문이 너무 추상적인 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;누가 들어와야 하는지, 어떤 주제를 다루는지 바로 안 보이면 유입이 약합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 이름과 소개문은 검색 엔진 제목처럼 바로 이해돼야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 초대 링크는 여러 군데에서 보여야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 내부 검색만 기다리는 것보다, 커뮤니티 글, SNS 프로필, 파트너 서버, 블로그 글에 초대 링크를 분산해야 현실적입니다.&lt;br /&gt;&lt;br /&gt;특히 초반 서버는 외부 유입 기반이 더 잘 먹히는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홍보 전략은 &lt;a href=&quot;https://blog.dishost.kr/112&quot;&gt;디스코드 서버 홍보 방법&lt;/a&gt;과 같이 보면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 체크리스트로 보면 빠르다&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초대 링크 자체는 정상 작동하는가&lt;/li&gt;
&lt;li&gt;커뮤니티 서버로 전환했는가&lt;/li&gt;
&lt;li&gt;규칙 채널과 커뮤니티 공지 채널이 설정돼 있는가&lt;/li&gt;
&lt;li&gt;서버 이름과 소개문이 분명한가&lt;/li&gt;
&lt;li&gt;외부 유입 경로를 따로 운영하고 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다섯 가지를 보면 원인 구분이 쉬워집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 검색되지 않는 문제와 비공개 운영은 다르다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 서버는 의도적으로 공개 노출을 줄이기도 합니다.&lt;br /&gt;&lt;br /&gt;운영 목적이 폐쇄 커뮤니티라면 검색이 안 되는 것이 꼭 문제는 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 먼저 서버 목표부터 분명히 해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 유입 추적도 같이 보면 좋다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 잘 노출되지 않는다고 느낄 때 실제로는 링크 유입은 꾸준히 들어오는 경우도 있습니다.&lt;br /&gt;&lt;br /&gt;검색 체감보다 실제 입장 수를 먼저 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유입 분석은 &lt;a href=&quot;https://blog.dishost.kr/134&quot;&gt;초대 링크 추적 디스코드 봇 만들기&lt;/a&gt;와 같이 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 검색 노출보다 먼저 입장 흐름을 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대와 공개 유입 흐름까지 정리됐다면 다음에는 서버 체류 시간을 높이는 기능형 봇들로 넘어갈 수 있습니다.&lt;br /&gt;&lt;br /&gt;그 전에 커뮤니티 설정 자체가 안 켜지는 문제를 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/104&quot;&gt;디스코드 커뮤니티 서버 활성화 방법&lt;/a&gt;을 다시 확인해야 합니다.&lt;/p&gt;</description>
      <category>디스코드 봇 오류 해결</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/137</guid>
      <comments>https://dishost.tistory.com/137#entry137comment</comments>
      <pubDate>Tue, 12 May 2026 16:59:26 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 초대 링크가 만료되거나 작동하지 않을 때 해결법</title>
      <link>https://dishost.tistory.com/136</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 초대 링크를 보냈는데 상대방이 &lt;code&gt;초대가 만료되었습니다&lt;/code&gt; 또는 &lt;code&gt;작동하지 않습니다&lt;/code&gt; 같은 메시지를 보는 경우가 있습니다.&lt;br /&gt;&lt;br /&gt;이 문제는 봇 초대 링크와 서버 초대 링크 양쪽에서 모두 자주 나옵니다.&lt;br /&gt;&lt;br /&gt;대부분은 링크 생성 옵션이나 권한, 복사 과정에서 원인이 잡힙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 링크가 안 열리면 새로 만드는 것보다 먼저 어떤 종류의 링크인지부터 구분해야 빠릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 서버 초대 링크인지 봇 초대 링크인지 나눈다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 링크는 생기는 방식이 다릅니다.&lt;br /&gt;&lt;br /&gt;서버 초대 링크는 채널에서 만들고, 봇 초대 링크는 Developer Portal의 OAuth2에서 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 해결 순서도 달라집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 초대 링크: 만료 시간, 사용 횟수, 채널 권한 확인&lt;/li&gt;
&lt;li&gt;봇 초대 링크: OAuth2 스코프, Client ID, 복사 상태 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇 초대 링크 기본 구조는 &lt;a href=&quot;https://blog.dishost.kr/59&quot;&gt;디스코드 봇 초대 링크 생성법, 권한이 꼬이지 않게 링크 만드는 방법&lt;/a&gt;에서 먼저 다뤘습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 서버 초대 링크는 만료 옵션을 먼저 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 초대 링크는 만들 때 만료 시간과 최대 사용 횟수를 정할 수 있습니다.&lt;br /&gt;&lt;br /&gt;초반에는 기본값을 그대로 두고 나중에 링크가 죽었다고 느끼는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 초대 생성 화면에서 아래를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;만료 안 함
최대 사용 횟수 제한 없음
임시 멤버 비활성화&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;홍보용 링크라면 특히 만료와 사용 횟수를 무제한으로 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 봇 초대 링크는 OAuth2 설정을 다시 만든다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇 초대 링크가 이상하면 예전 링크를 계속 고치기보다 새로 만들면 빠를 때가 많습니다.&lt;br /&gt;&lt;br /&gt;Developer Portal에서 아래 순서로 다시 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;OAuth2 -&amp;gt; URL Generator
- bot 체크
- applications.commands 체크
- 필요한 권한 체크&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그다음 새 URL을 다시 복사해 테스트합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 링크 일부가 잘려서 전달되는 경우도 많다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메신저나 문서에 붙여 넣는 과정에서 링크 끝이 빠지는 경우가 있습니다.&lt;br /&gt;&lt;br /&gt;특히 긴 봇 초대 URL은 줄바꿈이나 자동 링크 변환 때문에 잘리기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 확실한 방법은 브라우저 주소창에서 새 URL을 직접 열어 보는 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 초대 권한 자체가 없는 채널일 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 초대 링크는 해당 채널에서 초대를 만들 수 있어야 합니다.&lt;br /&gt;&lt;br /&gt;운영진 권한이 없거나 채널 권한이 꼬여 있으면 링크 생성이나 사용이 불안정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 구조가 불안하면 &lt;a href=&quot;https://blog.dishost.kr/57&quot;&gt;디스코드 채널 권한 설정법, 공지 채널과 관리자 채널이 꼬이지 않게 만드는 법&lt;/a&gt;부터 다시 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 서버가 검색 비공개 상태라 링크 공유만 되는 경우도 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 링크가 작동하지 않는 것처럼 느끼지만, 실제로는 공개 검색이 안 되는 것일 수도 있습니다.&lt;br /&gt;&lt;br /&gt;특히 커뮤니티 서버 설정과 공개 디렉터리 기능을 혼동할 때 자주 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 초대 링크 자체가 만료되거나 비활성화된 문제와, 서버 노출&amp;middot;검색 문제를 구분해서 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 체크리스트로 보면 빠르다&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 링크인지 봇 링크인지 구분했는가&lt;/li&gt;
&lt;li&gt;서버 초대라면 만료 시간과 사용 횟수 제한이 없는가&lt;/li&gt;
&lt;li&gt;봇 초대라면 OAuth2에서 새로 생성했는가&lt;/li&gt;
&lt;li&gt;링크 전체가 잘리지 않고 복사되었는가&lt;/li&gt;
&lt;li&gt;채널 권한이 정상인가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다섯 가지에서 대부분 원인이 나옵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 초대 링크를 여러 개 쓰는 서버라면 이름을 붙여 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홍보용, 이벤트용, 지인용 링크를 섞어 쓰면 어느 링크가 죽었는지 추적하기 어렵습니다.&lt;br /&gt;&lt;br /&gt;초대 링크 이름을 붙여 두면 문제 지점을 빨리 찾을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 링크 운영은 &lt;a href=&quot;https://blog.dishost.kr/134&quot;&gt;초대 링크 추적 디스코드 봇 만들기&lt;/a&gt;와 같이 보면 더 편합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 운영용 링크는 발급 뒤 바로 테스트한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크를 만들어 두고 바로 안 눌러 보면 나중에야 문제가 드러납니다.&lt;br /&gt;&lt;br /&gt;브라우저 시크릿 창이나 다른 계정으로 한 번 눌러 보는 습관이 있으면 문제를 빨리 잡을 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 초대 링크 다음에는 공개 노출 흐름을 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 링크는 열리는데 서버가 검색이나 공개 유입에서 안 보이는 경우라면 다음 글 흐름으로 넘어가면 됩니다.&lt;br /&gt;&lt;br /&gt;그 전에 권한과 서버 구조를 다시 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/112&quot;&gt;디스코드 서버 홍보 방법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/104&quot;&gt;디스코드 커뮤니티 서버 활성화 방법&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>디스코드 봇 오류 해결</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/136</guid>
      <comments>https://dishost.tistory.com/136#entry136comment</comments>
      <pubDate>Mon, 11 May 2026 16:58:58 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 봇 초대 후 오프라인으로 보일 때 해결법</title>
      <link>https://dishost.tistory.com/135</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 봇을 서버에 초대했는데 목록에는 보이지만 회색 점으로 오프라인 상태인 경우가 있습니다.&lt;br /&gt;&lt;br /&gt;이 상황은 초대한 것과 실행한 것이 별개라는 점을 놓칠 때 특히 자주 나옵니다.&lt;br /&gt;&lt;br /&gt;대부분은 코드보다 실행 환경이나 토큰, 권한 쪽에서 원인이 잡힙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대는 끝났는데 오프라인으로 보인다면 아래 순서대로 봐야 가장 빠릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 초대와 실행은 다른 단계다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇을 서버에 초대했다고 해서 자동으로 켜지지는 않습니다.&lt;br /&gt;&lt;br /&gt;초대는 서버에 앱을 등록한 것이고, 실제 온라인 상태는 코드가 실행 중이어야 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입문 단계에서는 이 부분에서 많이 막힙니다.&lt;br /&gt;&lt;br /&gt;기본 실행 구조가 아직 없다면 &lt;a href=&quot;https://blog.dishost.kr/53&quot;&gt;파이썬 디스코드 봇 만들기 처음부터 끝까지, discord.py 입문 가이드&lt;/a&gt;나 &lt;a href=&quot;https://blog.dishost.kr/63&quot;&gt;discord.js v14 디스코드 봇 만들기 처음부터 끝까지&lt;/a&gt;를 먼저 다시 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 터미널에서 코드가 실제로 켜져 있는지 확인한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 할 일입니다.&lt;br /&gt;&lt;br /&gt;프로그램이 실행 중이 아니라면 디스코드에서는 오프라인으로만 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 아래 명령어로 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;node main.js&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;터미널이 바로 종료되거나 오류가 나오면 그 문제부터 해결해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 토큰이 비어 있거나 잘못됐는지 확인한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.env&lt;/code&gt;를 쓰는 구조라면 토큰이 비어 있어도 봇이 조용히 종료될 수 있습니다.&lt;br /&gt;&lt;br /&gt;재발급한 새 토큰을 넣지 않은 경우도 흔합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래를 먼저 봅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; 파일 위치가 맞는가&lt;/li&gt;
&lt;li&gt;변수 이름이 코드와 같은가&lt;/li&gt;
&lt;li&gt;복사한 토큰이 최신 값인가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 관리 기본 구조는 &lt;a href=&quot;https://blog.dishost.kr/60&quot;&gt;.env 환경변수로 디스코드 토큰 숨기기, 하드코딩 없이 안전하게 관리하는 법&lt;/a&gt;에서 먼저 정리했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. &lt;code&gt;client.run()&lt;/code&gt; 또는 &lt;code&gt;client.login()&lt;/code&gt;이 실제로 호출되는지 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 켜졌는데 로그인 함수가 호출되지 않으면 봇은 온라인이 되지 않습니다.&lt;br /&gt;&lt;br /&gt;파일 맨 아래 실행 구문이 빠져 있는 경우도 생각보다 자주 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 예시는 아래처럼 끝나야 합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;JS 예시는 아래처럼 끝납니다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;client.login(token);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 한 줄이 빠지면 아무리 코드가 길어도 봇은 오프라인입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 터미널 오류 로그를 먼저 읽는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프라인 문제는 디스코드 화면보다 터미널이 더 중요합니다.&lt;br /&gt;&lt;br /&gt;아래 오류가 자주 나옵니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Invalid Token&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PrivilegedIntentsRequired&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;패키지 import 실패&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; 읽기 실패&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면상으로는 그냥 오프라인처럼 보여도, 실제 원인은 대부분 터미널에 그대로 나옵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 서버에 초대만 되고 권한이 부족한 것은 다른 문제다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 부족이면 온라인은 되지만 기능이 안 됩니다.&lt;br /&gt;&lt;br /&gt;오프라인 상태는 보통 로그인 자체가 안 된 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 오프라인 문제를 볼 때는 먼저 실행, 토큰, 로그인 함수부터 봐야 합니다.&lt;br /&gt;&lt;br /&gt;권한 문제는 &lt;a href=&quot;https://blog.dishost.kr/55&quot;&gt;디스코드 봇 권한 설정법, 50013과 50001을 막는 기본 구조 정리&lt;/a&gt;로 따로 보면 빠릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 로컬 PC를 끄면 다시 오프라인이 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 후 잠깐만 켜 두고 노트북을 닫으면 봇은 다시 오프라인으로 돌아갑니다.&lt;br /&gt;&lt;br /&gt;입문 단계에서 &lt;code&gt;아까는 켜졌는데 지금은 왜 꺼졌지&lt;/code&gt;가 여기서 자주 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상시 온라인 상태를 원하면 결국 계속 켜져 있는 서버가 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 체크리스트로 보면 빠르다&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행 명령어를 실제로 돌렸는가&lt;/li&gt;
&lt;li&gt;터미널이 종료되지 않고 유지되는가&lt;/li&gt;
&lt;li&gt;토큰 값이 비어 있지 않은가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client.run()&lt;/code&gt; 또는 &lt;code&gt;client.login()&lt;/code&gt;이 있는가&lt;/li&gt;
&lt;li&gt;PC를 끄거나 절전 상태로 두지 않았는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다섯 가지로 대부분 정리됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 배포 단계까지 같이 봐야 끝난다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발이 끝났는데 자꾸 오프라인으로 돌아간다면 로컬 실행만으로는 한계가 있습니다.&lt;br /&gt;&lt;br /&gt;운영용 봇은 결국 상시 실행 환경으로 옮겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포가 필요한 시점이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 별도 서버에 올려 둬야 현실적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 오프라인 문제 뒤에는 초대 링크 상태를 점검한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프라인 문제를 해결했다면 다음에는 초대 링크 자체가 만료되거나 작동하지 않는 문제를 점검하는 흐름이 자연스럽습니다.&lt;br /&gt;&lt;br /&gt;지금 단계에서 토큰이나 실행 구조를 더 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/60&quot;&gt;.env 환경변수로 디스코드 토큰 숨기기, 하드코딩 없이 안전하게 관리하는 법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/134&quot;&gt;초대 링크 추적 디스코드 봇 만들기&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>디스코드 봇 오류 해결</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/135</guid>
      <comments>https://dishost.tistory.com/135#entry135comment</comments>
      <pubDate>Sun, 10 May 2026 16:58:33 +0900</pubDate>
    </item>
    <item>
      <title>초대 링크 추적 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/134</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서버 홍보를 시작하면 어느 초대 링크로 유입이 들어오는지 궁금해집니다.&lt;br /&gt;&lt;br /&gt;링크를 여러 군데에 뿌려도 실제로 남는 유입이 어디서 오는지는 따로 봐야 합니다.&lt;br /&gt;&lt;br /&gt;이럴 때 초대 링크 사용량을 기록해 두면 홍보 효율과 유입 품질을 같이 보기 쉬워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 서버의 초대 링크 목록을 캐시해 두고, 새 멤버가 들어올 때 어떤 링크 사용량이 늘었는지 추적하는 기본 봇을 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 왜 초대 링크 추적이 필요할까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홍보는 많이 했는데 잔류가 낮을 수 있습니다.&lt;br /&gt;&lt;br /&gt;반대로 유입 수는 적어도 오래 남는 링크가 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 두 가지를 같이 봐야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어디서 유입됐는가&lt;/li&gt;
&lt;li&gt;들어온 뒤 남았는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홍보 문구와 서버 입구 구조는 &lt;a href=&quot;https://blog.dishost.kr/112&quot;&gt;디스코드 서버 홍보 방법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/103&quot;&gt;디스코드 서버 온보딩 채널 구성 예시&lt;/a&gt;에서 먼저 다뤘습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 초대 링크 사용량은 어떻게 추적할까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 봇이 멤버 입장 이벤트를 받을 때 현재 초대 링크 목록을 다시 읽어 옵니다.&lt;br /&gt;&lt;br /&gt;이전에 저장해 둔 사용량과 비교해서 어떤 링크의 &lt;code&gt;uses&lt;/code&gt;가 늘었는지 보면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완벽하게 100% 잡히는 구조는 아니지만, 초반 운영용 분석으로는 충분히 쓸 만합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 추적할 초대 링크 값 넣기&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
LOG_CHANNEL_ID=입장로그채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;로그 채널에는 어떤 초대 링크로 유입됐는지 기록을 남깁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 초대 사용량 변화를 읽는 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
LOG_CHANNEL_ID = int(os.getenv(&quot;LOG_CHANNEL_ID&quot;))
invite_cache: dict[int, dict[str, int]] = {}

intents = discord.Intents.default()
intents.members = True
intents.guilds = True
client = discord.Client(intents=intents)


async def refresh_invite_cache(guild: discord.Guild):
    invites = await guild.invites()
    invite_cache[guild.id] = {invite.code: invite.uses or 0 for invite in invites}


async def send_log(guild: discord.Guild, description: str):
    channel = guild.get_channel(LOG_CHANNEL_ID)
    if isinstance(channel, discord.TextChannel):
        embed = discord.Embed(title=&quot;초대 링크 추적 로그&quot;, description=description, color=discord.Color.green())
        await channel.send(embed=embed)


@client.event
async def on_ready():
    for guild in client.guilds:
        await refresh_invite_cache(guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@client.event
async def on_member_join(member: discord.Member):
    guild = member.guild
    before_uses = invite_cache.get(guild.id, {})
    invites = await guild.invites()

    used_invite = None
    for invite in invites:
        old_uses = before_uses.get(invite.code, 0)
        new_uses = invite.uses or 0
        if new_uses &amp;gt; old_uses:
            used_invite = invite
            break

    await refresh_invite_cache(guild)

    if used_invite is None:
        await send_log(guild, f&quot;{member.mention} 님이 입장했습니다. 사용된 초대 링크는 확인하지 못했습니다.&quot;)
        return

    await send_log(
        guild,
        (
            f&quot;{member.mention} 님이 입장했습니다.\n&quot;
            f&quot;사용된 초대 코드: {used_invite.code}\n&quot;
            f&quot;현재 사용 횟수: {used_invite.uses}&quot;
        ),
    )


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;핵심은 서버별 초대 링크 사용량을 캐시해 두고, 입장 순간에 증가한 링크를 찾는 부분입니다.&lt;br /&gt;홍보 실험을 여러 개 돌릴 때 특히 도움이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 먼저 확인할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 링크를 추적하려면 서버에 링크가 여러 개 있어야 의미가 있습니다.&lt;br /&gt;&lt;br /&gt;링크 하나만 쓰면 어디서 들어왔는지까지 구분할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 홍보용, 프로필용, 이벤트용처럼 링크를 나눠 둬야 합니다.&lt;br /&gt;&lt;br /&gt;이 부분은 &lt;a href=&quot;https://blog.dishost.kr/112&quot;&gt;디스코드 서버 홍보 방법&lt;/a&gt;에서 먼저 설명했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 입장 한 번으로 사용량 차이 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;서버에 초대 링크를 두 개 이상 만든 뒤, 각 링크로 테스트 계정을 입장시켜 봅니다.&lt;br /&gt;로그 채널에 어떤 초대 코드가 사용됐는지 기록되면 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 캐시와 재시작에서 자주 꼬이는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용된 링크를 못 찾는 경우 초대 링크가 너무 자주 재생성되거나, 캐시를 초기화한 직후 타이밍이 겹칠 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;guild.invites()&lt;/code&gt;가 실패하는 경우 봇에 초대 링크 조회 권한이 부족할 수 있습니다.&lt;/li&gt;
&lt;li&gt;입장 이벤트 자체가 안 도는 경우 &lt;code&gt;SERVER MEMBERS INTENT&lt;/code&gt; 설정을 먼저 봐야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 입장 퇴장 로그와 같이 보면 유입 품질이 보인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 링크 추적은 유입 경로를 보여 주고, 입장 퇴장 로그는 남는 정도를 보여 줍니다.&lt;br /&gt;&lt;br /&gt;둘을 같이 보면 어디서 들어온 유저가 더 오래 남는지 보기 쉬워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입장 로그는 &lt;a href=&quot;https://blog.dishost.kr/128&quot;&gt;입장 퇴장 로그 디스코드 봇 만들기&lt;/a&gt;와 같이 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 배포는 거의 필수다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유입은 사람이 없는 시간에도 들어옵니다.&lt;br /&gt;&lt;br /&gt;초대 링크 추적도 그 순간 켜져 있어야 기록이 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실운영용이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 계속 켜진 환경이 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 유입 추적 뒤에는 로그와 환영 메시지를 묶는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유입 분석까지 정리됐다면 이제 봇이 초대는 되었는데 온라인으로 안 뜨는 문제처럼 실제 운영에서 자주 만나는 오류 해결 글로 넘어갈 수 있습니다.&lt;br /&gt;&lt;br /&gt;그 전에 홍보 흐름을 더 다듬고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/112&quot;&gt;디스코드 서버 홍보 방법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/111&quot;&gt;디스코드 서버 소개문 작성법&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/134</guid>
      <comments>https://dishost.tistory.com/134#entry134comment</comments>
      <pubDate>Sat, 9 May 2026 16:58:06 +0900</pubDate>
    </item>
    <item>
      <title>욕설 필터 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/133</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;채팅이 활발해지면 광고보다 먼저 분위기를 무너뜨리는 것이 욕설과 공격적인 표현일 때가 많습니다.&lt;br /&gt;&lt;br /&gt;운영진이 일일이 보고 지우기 시작하면 대응이 늦고, 같은 기준을 유지하기도 어렵습니다.&lt;br /&gt;&lt;br /&gt;초반에는 완벽한 문맥 분석보다 기본 금칙어 필터를 먼저 두는 쪽이 현실적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 금칙어 목록을 기준으로 메시지를 삭제하고 경고 로그를 남기는 기본 욕설 필터 봇을 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 욕설 필터는 어디까지 맡길까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트 필터는 편하지만 한계도 분명합니다.&lt;br /&gt;&lt;br /&gt;문맥을 완벽하게 읽지는 못하고, 일반 대화를 오탐할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음에는 아래처럼 두는 게 낫습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명확한 금칙어만 필터링&lt;/li&gt;
&lt;li&gt;애매한 표현은 운영진 신고나 검토로 넘김&lt;/li&gt;
&lt;li&gt;로그를 남겨 기준을 조정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고 동선은 &lt;a href=&quot;https://blog.dishost.kr/130&quot;&gt;디스코드 신고 채널 운영 가이드&lt;/a&gt;와 같이 맞춰 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 금칙어 목록은 짧게 시작한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에 목록을 너무 넓게 잡으면 오탐이 많아집니다.&lt;br /&gt;&lt;br /&gt;명확하게 막고 싶은 단어부터 적게 시작해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 아래처럼 둡니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;BAD_WORDS = [&quot;욕설예시1&quot;, &quot;욕설예시2&quot;, &quot;금칙어예시&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;실제 운영 전에는 서버 분위기와 기준에 맞는 목록으로 교체해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 금칙어 채널과 로그 값 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
LOG_CHANNEL_ID=운영로그채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;삭제 사실과 대상 유저를 로그로 남기면 기준 조정이 훨씬 쉬워집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 금칙어 감지와 삭제 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
LOG_CHANNEL_ID = int(os.getenv(&quot;LOG_CHANNEL_ID&quot;))
BAD_WORDS = [&quot;욕설예시1&quot;, &quot;욕설예시2&quot;, &quot;금칙어예시&quot;]

intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
intents.guilds = True
client = discord.Client(intents=intents)


async def send_log(guild: discord.Guild, lines: list[str]):
    channel = guild.get_channel(LOG_CHANNEL_ID)
    if isinstance(channel, discord.TextChannel):
        embed = discord.Embed(title=&quot;욕설 필터 로그&quot;, description=&quot;\n&quot;.join(lines), color=discord.Color.red())
        await channel.send(embed=embed)


def has_bad_word(content: str) -&amp;gt; bool:
    lowered = content.lower()
    return any(word.lower() in lowered for word in BAD_WORDS)


@client.event
async def on_ready():
    print(f&quot;로그인 성공: {client.user}&quot;)


@client.event
async def on_message(message: discord.Message):
    if message.author.bot or message.guild is None:
        return

    if not has_bad_word(message.content):
        return

    try:
        await message.delete()
        await message.channel.send(
            f&quot;{message.author.mention} 운영 기준에 맞지 않는 표현이 감지되어 메시지를 삭제했습니다.&quot;,
            delete_after=5,
        )
        await send_log(
            message.guild,
            [
                f&quot;대상: {message.author.mention}&quot;,
                f&quot;채널: {message.channel.mention}&quot;,
                f&quot;메시지: {message.content[:150] or '내용 없음'}&quot;,
            ],
        )
    except discord.Forbidden:
        print(&quot;메시지를 삭제할 권한이 없습니다.&quot;)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;핵심은 아주 단순합니다.&lt;br /&gt;금칙어 목록에 걸리면 메시지를 지우고, 운영 로그에 남깁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 운영 전에 꼭 봐야 할 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;욕설 필터는 목록 자체가 기준입니다.&lt;br /&gt;&lt;br /&gt;따라서 어떤 표현을 자동 삭제하고, 어떤 표현은 운영진 검토로 남길지 먼저 합의해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 아래처럼 나누면 무난합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명확한 욕설: 자동 삭제&lt;/li&gt;
&lt;li&gt;애매한 비꼼, 공격성 표현: 신고나 경고 검토&lt;/li&gt;
&lt;li&gt;반복 위반: 처벌 로그와 연동&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 오탐과 누락을 같이 확인하기&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;테스트 채널에서 금칙어 목록에 있는 단어가 들어간 메시지를 보내 봅니다.&lt;br /&gt;메시지가 삭제되고 로그 채널에 기록되면 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 금칙어 목록에서 흔한 실수&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평범한 단어도 잡히는 경우 부분 문자열 검사라 오탐이 생길 수 있습니다.&lt;br /&gt;&lt;br /&gt;금칙어를 더 구체적으로 바꾸거나 정규식으로 세분화해야 합니다.&lt;/li&gt;
&lt;li&gt;로그는 남는데 메시지가 안 지워지는 경우 &lt;code&gt;Manage Messages&lt;/code&gt; 권한이 부족할 수 있습니다.&lt;/li&gt;
&lt;li&gt;운영진 발언까지 전부 잡히는 경우 역할 예외 처리를 넣지 않은 상태일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 링크 차단, 스팸 차단과 같이 붙여야 힘이 난다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;욕설만 막고 광고 링크를 열어 두면 운영 체감이 약합니다.&lt;br /&gt;&lt;br /&gt;반대로 광고만 막고 채팅 분위기를 방치해도 문제가 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;a href=&quot;https://blog.dishost.kr/132&quot;&gt;링크 차단 디스코드 봇 만들기&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/131&quot;&gt;스팸 방지 디스코드 봇 만들기&lt;/a&gt;를 같이 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 반복 위반은 기록으로 남겨야 누적이 보인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제만 하고 기록이 없으면 나중에 경고 기준이 흔들립니다.&lt;br /&gt;&lt;br /&gt;반복 위반은 처벌 로그나 신고 채널과 이어 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처벌 기준은 &lt;a href=&quot;https://blog.dishost.kr/129&quot;&gt;처벌 로그 디스코드 봇 만들기&lt;/a&gt;과 같이 정리해 두면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 금칙어만으로 끝내지 말고 운영 동선까지 묶는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 금칙어 필터까지 붙였다면 다음에는 유입 경로를 분석하는 초대 링크 추적 구조로 넘어갈 수 있습니다.&lt;br /&gt;&lt;br /&gt;지금 단계에서 운영 채널 구조를 다시 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/127&quot;&gt;디스코드 로그 기록 봇 만들기&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/110&quot;&gt;디스코드 서버 보안 설정 체크리스트&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/133</guid>
      <comments>https://dishost.tistory.com/133#entry133comment</comments>
      <pubDate>Fri, 8 May 2026 16:57:41 +0900</pubDate>
    </item>
    <item>
      <title>링크 차단 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/132</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스팸을 막기 시작하면 그다음 바로 걸리는 것이 광고 링크입니다.&lt;br /&gt;&lt;br /&gt;반복 도배보다 더 자주 들어오는 경우도 많고, 초대 링크 하나만 남기고 빠지는 식의 유입도 흔합니다.&lt;br /&gt;&lt;br /&gt;이럴 때는 일반 스팸 감지와 별도로 링크 차단 규칙을 하나 두면 운영이 훨씬 편합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 디스코드 초대 링크와 외부 URL을 감지해 삭제하고 운영 로그에 남기는 기본 링크 차단 봇을 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 링크 차단은 왜 따로 두는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스팸 방지 규칙만으로는 링크 광고를 깔끔하게 다루기 어렵습니다.&lt;br /&gt;&lt;br /&gt;메시지 개수는 적어도 광고 효과가 큰 경우가 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 둘을 분리해서 생각해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠른 반복 메시지 차단&lt;/li&gt;
&lt;li&gt;링크 포함 메시지 차단&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 스팸 흐름은 &lt;a href=&quot;https://blog.dishost.kr/131&quot;&gt;스팸 방지 디스코드 봇 만들기&lt;/a&gt;에서 먼저 정리했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 어떤 링크를 막을지 먼저 정한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 너무 복잡하게 가면 안 됩니다.&lt;br /&gt;&lt;br /&gt;대부분 서버는 아래 두 종류부터 막으면 충분합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;discord.gg&lt;/code&gt;, &lt;code&gt;discord.com/invite&lt;/code&gt; 형태 초대 링크&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://&lt;/code&gt;, &lt;code&gt;https://&lt;/code&gt;가 들어간 일반 외부 링크&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영용 서버라면 공지 채널이나 관리자 채널은 예외로 두는 경우가 많습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 차단할 채널과 예외 값 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
LOG_CHANNEL_ID=운영로그채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;링크 삭제 사실을 기록해 두려면 로그 채널 하나는 같이 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. URL 감지와 삭제 흐름 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os
import re

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
LOG_CHANNEL_ID = int(os.getenv(&quot;LOG_CHANNEL_ID&quot;))
INVITE_PATTERN = re.compile(r&quot;(discord\.gg/|discord\.com/invite/)&quot;, re.IGNORECASE)
URL_PATTERN = re.compile(r&quot;https?://&quot;, re.IGNORECASE)

intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
intents.guilds = True
client = discord.Client(intents=intents)


async def send_log(guild: discord.Guild, lines: list[str]):
    channel = guild.get_channel(LOG_CHANNEL_ID)
    if isinstance(channel, discord.TextChannel):
        embed = discord.Embed(title=&quot;링크 차단 로그&quot;, description=&quot;\n&quot;.join(lines), color=discord.Color.orange())
        await channel.send(embed=embed)


def has_blocked_link(content: str) -&amp;gt; bool:
    return bool(INVITE_PATTERN.search(content) or URL_PATTERN.search(content))


@client.event
async def on_ready():
    print(f&quot;로그인 성공: {client.user}&quot;)


@client.event
async def on_message(message: discord.Message):
    if message.author.bot or message.guild is None:
        return

    if not has_blocked_link(message.content):
        return

    try:
        await message.delete()
        await message.channel.send(
            f&quot;{message.author.mention} 외부 링크 또는 초대 링크는 이 채널에서 허용되지 않습니다.&quot;,
            delete_after=5,
        )
        await send_log(
            message.guild,
            [
                f&quot;대상: {message.author.mention}&quot;,
                f&quot;채널: {message.channel.mention}&quot;,
                f&quot;내용: {message.content[:150] or '내용 없음'}&quot;,
            ],
        )
    except discord.Forbidden:
        print(&quot;링크 메시지를 삭제할 권한이 없습니다.&quot;)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;기본형은 단순합니다.&lt;br /&gt;메시지에 링크 패턴이 보이면 삭제하고, 짧은 경고와 로그를 남깁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 먼저 손볼 만한 부분&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 입문용이라 모든 채널을 동일하게 검사합니다.&lt;br /&gt;&lt;br /&gt;실운영에서는 공지 채널, 관리자 채널, 제휴 채널 같은 예외를 두는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아래처럼 확장할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 채널 ID는 검사 제외&lt;/li&gt;
&lt;li&gt;특정 역할은 예외 처리&lt;/li&gt;
&lt;li&gt;디스코드 초대 링크만 막고 일반 URL은 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 허용 링크와 차단 링크를 같이 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;테스트 채널에서 일반 링크와 디스코드 초대 링크를 각각 보내 봅니다.&lt;br /&gt;메시지가 삭제되고 경고 문구와 로그가 남으면 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 정규식과 예외 처리에서 자주 막히는 곳&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;링크가 안 지워지는 경우 봇에 &lt;code&gt;Manage Messages&lt;/code&gt; 권한이 없는 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;공지 채널에서도 삭제돼 버리는 경우 채널 예외 처리를 아직 안 넣은 상태일 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;discord.com/invite&lt;/code&gt;는 잡는데 단축 주소를 놓치는 경우 정규식 패턴을 조금 더 넓혀야 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 스팸 방지와 같이 써야 의미가 커진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크 차단만 있고 반복 메시지 차단이 없으면 운영 체감이 반쪽입니다.&lt;br /&gt;&lt;br /&gt;반대로 반복 메시지 차단만 있고 링크 규칙이 없으면 광고 대응이 늦습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 보통은 &lt;a href=&quot;https://blog.dishost.kr/131&quot;&gt;스팸 방지 디스코드 봇 만들기&lt;/a&gt;와 묶어서 쓰면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 신고 흐름과 로그도 같이 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오탐이나 예외 상황은 반드시 생깁니다.&lt;br /&gt;&lt;br /&gt;삭제 사실을 로그로 남기고, 필요하면 신고나 문의 채널에서 복구 요청을 받을 수 있게 둬야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 기준은 &lt;a href=&quot;https://blog.dishost.kr/130&quot;&gt;디스코드 신고 채널 운영 가이드&lt;/a&gt;, 기본 로그 구조는 &lt;a href=&quot;https://blog.dishost.kr/127&quot;&gt;디스코드 로그 기록 봇 만들기&lt;/a&gt;과 같이 보면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 링크 차단 뒤에는 스팸 로그까지 붙여 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크 차단까지 붙였다면 다음에는 금칙어와 욕설처럼 텍스트 필터를 다루는 흐름으로 이어갈 수 있습니다.&lt;br /&gt;&lt;br /&gt;지금 단계에서 보안 설정을 다시 점검하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/110&quot;&gt;디스코드 서버 보안 설정 체크리스트&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/129&quot;&gt;처벌 로그 디스코드 봇 만들기&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/132</guid>
      <comments>https://dishost.tistory.com/132#entry132comment</comments>
      <pubDate>Thu, 7 May 2026 16:57:14 +0900</pubDate>
    </item>
    <item>
      <title>스팸 방지 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/131</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 조금만 커져도 반복 메시지, 광고 링크, 도배 같은 문제가 바로 들어옵니다.&lt;br /&gt;&lt;br /&gt;운영진이 수동으로만 지우기 시작하면 대응이 늦고 기준도 흔들리기 쉽습니다.&lt;br /&gt;&lt;br /&gt;초반에는 완벽한 안티스팸 시스템보다 기본 차단선 하나를 자동으로 세워 두는 쪽이 훨씬 안전합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 짧은 시간 안에 같은 유저가 메시지를 많이 보내면 경고 후 메시지를 정리하는 기본 스팸 방지 봇을 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 어떤 기준으로 막을까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스팸 기준을 너무 빡빡하게 잡으면 일반 대화도 막힙니다.&lt;br /&gt;&lt;br /&gt;반대로 너무 느슨하면 자동화 의미가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 아래 정도 기준이 무난합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;10초 안에 5개 이상 메시지&lt;/li&gt;
&lt;li&gt;같은 내용 반복 전송&lt;/li&gt;
&lt;li&gt;초대 링크나 외부 광고 링크 반복 게시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;광고성 신고 흐름은 &lt;a href=&quot;https://blog.dishost.kr/130&quot;&gt;디스코드 신고 채널 운영 가이드&lt;/a&gt;와도 같이 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 기준 채널과 예외 값 넣기&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
LOG_CHANNEL_ID=운영로그채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;스팸 차단 결과를 운영 로그 채널에 남겨 두면 기준을 조정하기 쉬워집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 반복 메시지를 잡는 기본 필터 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os
import time
from collections import defaultdict, deque

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
LOG_CHANNEL_ID = int(os.getenv(&quot;LOG_CHANNEL_ID&quot;))
WINDOW_SECONDS = 10
MESSAGE_LIMIT = 5

intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
intents.guilds = True
client = discord.Client(intents=intents)
user_message_times: dict[int, deque[float]] = defaultdict(deque)


async def send_log(guild: discord.Guild, lines: list[str]):
    channel = guild.get_channel(LOG_CHANNEL_ID)
    if not isinstance(channel, discord.TextChannel):
        return

    embed = discord.Embed(title=&quot;스팸 차단 로그&quot;, description=&quot;\n&quot;.join(lines), color=discord.Color.red())
    await channel.send(embed=embed)


def is_invite_or_link(content: str) -&amp;gt; bool:
    lowered = content.lower()
    return &quot;discord.gg/&quot; in lowered or &quot;http://&quot; in lowered or &quot;https://&quot; in lowered


@client.event
async def on_ready():
    print(f&quot;로그인 성공: {client.user}&quot;)


@client.event
async def on_message(message: discord.Message):
    if message.author.bot or message.guild is None:
        return

    now = time.time()
    timestamps = user_message_times[message.author.id]
    timestamps.append(now)

    while timestamps and now - timestamps[0] &amp;gt; WINDOW_SECONDS:
        timestamps.popleft()

    if len(timestamps) &amp;gt;= MESSAGE_LIMIT or (is_invite_or_link(message.content) and len(timestamps) &amp;gt;= 3):
        try:
            await message.channel.purge(limit=MESSAGE_LIMIT, check=lambda m: m.author.id == message.author.id)
        except discord.Forbidden:
            return

        warning_text = f&quot;{message.author.mention} 너무 빠른 반복 메시지가 감지되어 최근 메시지를 정리했습니다.&quot;
        await message.channel.send(warning_text, delete_after=5)
        await send_log(
            message.guild,
            [
                f&quot;대상: {message.author.mention}&quot;,
                f&quot;채널: {message.channel.mention}&quot;,
                f&quot;기준: {WINDOW_SECONDS}초 안에 {MESSAGE_LIMIT}개 이상 메시지&quot;,
                f&quot;최근 메시지: {message.content[:100] or '내용 없음'}&quot;,
            ],
        )
        timestamps.clear()
        return


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;가장 단순한 구조는 시간창 안의 메시지 개수를 세고, 기준을 넘으면 최근 메시지를 정리하는 방식입니다.&lt;br /&gt;실전에서는 링크, 멘션 폭탄, 같은 문자열 반복 여부를 더 세분화할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 코드에서 먼저 손볼 부분&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 입문용 기본형이라 그대로 운영에 넣기 전에 기준을 조정해야 합니다.&lt;br /&gt;&lt;br /&gt;특히 일반 채팅이 빠른 서버에서는 &lt;code&gt;10초 안에 5개&lt;/code&gt; 기준이 너무 빡빡할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 아래 둘을 먼저 조정합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;WINDOW_SECONDS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MESSAGE_LIMIT&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 도배 패턴부터 먼저 찍어 보기&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;테스트 채널에서 같은 계정으로 빠르게 여러 메시지를 보내 봅니다.&lt;br /&gt;기준을 넘으면 최근 메시지가 정리되고 경고 문구와 로그가 남아야 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 기준값 조정에서 흔한 실패&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지가 안 지워지는 경우 봇에 &lt;code&gt;Manage Messages&lt;/code&gt; 권한이 없을 수 있습니다.&lt;/li&gt;
&lt;li&gt;일반 대화도 과하게 잡히는 경우 기준이 너무 낮은 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;테스트 서버에서 먼저 값을 조정해야 합니다.&lt;/li&gt;
&lt;li&gt;광고 링크를 한두 번만 보내도 안 잡히는 경우 지금 구조는 기본형이라 링크 차단 전용 규칙이 강하지 않습니다.&lt;br /&gt;&lt;br /&gt;다음 글에서 링크 차단을 따로 다루면 더 잘 맞습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 운영진 예외 처리를 둘지 결정한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영진이 공지나 이벤트 안내를 짧은 시간에 여러 번 보낼 수도 있습니다.&lt;br /&gt;&lt;br /&gt;이럴 때까지 잡히면 불편합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 운영진 역할은 예외 처리하거나, 공지 채널은 검사 대상에서 빼야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 신고 채널과 로그 채널이 같이 있어야 기준을 조정하기 쉽다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스팸 자동 차단은 완벽하지 않습니다.&lt;br /&gt;&lt;br /&gt;오탐이 나오거나 놓치는 경우가 반드시 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 신고 채널과 운영 로그를 같이 두고 기준을 조금씩 조정하는 쪽이 현실적입니다.&lt;br /&gt;&lt;br /&gt;신고 동선은 &lt;a href=&quot;https://blog.dishost.kr/130&quot;&gt;디스코드 신고 채널 운영 가이드&lt;/a&gt;, 기본 로그 구조는 &lt;a href=&quot;https://blog.dishost.kr/127&quot;&gt;디스코드 로그 기록 봇 만들기&lt;/a&gt;과 같이 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영이라 배포가 사실상 필수다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스팸은 사람이 없는 시간에 더 자주 들어옵니다.&lt;br /&gt;&lt;br /&gt;그래서 안티스팸 봇은 항상 켜져 있어야 의미가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영용으로 옮길 때는 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 상시 실행 환경을 같이 준비해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 스팸 기준은 로그와 권한을 함께 봐야 굳어진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스팸 기본 차단선까지 세웠다면 이제 반복 광고 링크만 더 세게 막는 링크 차단 구조로 넘어갈 수 있습니다.&lt;br /&gt;&lt;br /&gt;지금 단계에서 권한과 로그 구조를 다시 점검하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/110&quot;&gt;디스코드 서버 보안 설정 체크리스트&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/129&quot;&gt;처벌 로그 디스코드 봇 만들기&lt;/a&gt;을 함께 보면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/131</guid>
      <comments>https://dishost.tistory.com/131#entry131comment</comments>
      <pubDate>Wed, 6 May 2026 16:56:47 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 신고 채널 운영 가이드</title>
      <link>https://dishost.tistory.com/130</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;신고 채널은 만들기보다 운영 기준을 세우는 일이 더 중요합니다.&lt;br /&gt;&lt;br /&gt;입구만 열어 두고 기준이 없으면 일반 문의와 감정 싸움이 한데 섞이기 쉽습니다.&lt;br /&gt;&lt;br /&gt;신고 채널은 민감한 내용을 안전하게 받고, 처리 기록을 남기고, 후속 조치를 분리하는 구조여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고 시스템은 채널 하나가 아니라 문의 동선과 처리 기준 전체로 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 신고 채널을 일반 문의와 섞지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 지켜야 할 기준입니다.&lt;br /&gt;&lt;br /&gt;버그 문의, 계정 문의, 신고 접수를 같은 채널로 받으면 분위기가 바로 무거워집니다.&lt;br /&gt;&lt;br /&gt;신고는 별도 버튼이나 별도 티켓 카테고리로 분리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문의 카테고리 구조는 &lt;a href=&quot;https://blog.dishost.kr/121&quot;&gt;디스코드 티켓 카테고리 설계법&lt;/a&gt;, 전체 흐름은 &lt;a href=&quot;https://blog.dishost.kr/122&quot;&gt;디스코드 문의 동선 설계법&lt;/a&gt;과 같이 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 신고 전에 안내할 문구를 짧게 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고 채널 입구 문구는 길게 쓰지 말아야 합니다.&lt;br /&gt;&lt;br /&gt;대신 무엇을 포함해서 적어야 하는지 명확해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 아래 정도면 충분합니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;신고 내용, 발생 시점, 관련 유저, 증거 링크를 함께 남겨 주세요.
허위 신고나 장난 신고는 제재 대상이 될 수 있습니다.
민감한 내용은 신고 티켓으로 접수해 주세요.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;짧지만 운영 기준과 입력 항목이 같이 들어 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 공개 채널보다 비공개 티켓이 더 잘 맞는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고는 다른 유저가 같이 보는 공개 채널보다 비공개 티켓 구조가 안전합니다.&lt;br /&gt;&lt;br /&gt;특히 증거 이미지, 메시지 링크, 신고 대상 정보가 들어갈 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고용 티켓을 따로 여는 구조는 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt;와 함께 설계해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 신고 항목은 최소한 네 개를 받는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 아래 네 가지를 기본으로 두면 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신고 대상&lt;/li&gt;
&lt;li&gt;발생 시점&lt;/li&gt;
&lt;li&gt;신고 사유&lt;/li&gt;
&lt;li&gt;증거 링크 또는 첨부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 항목만 있어도 운영진이 첫 판단을 하기 쉬워집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 처리 담당자와 처리 기준을 분리하지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누가 신고를 보는지, 어떤 기준으로 경고나 타임아웃을 주는지 운영진 내부 합의가 필요합니다.&lt;br /&gt;&lt;br /&gt;신고는 감정 소모가 큰 영역이라 기준이 없으면 처리 편차가 크게 느껴집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 신고 채널을 열기 전 아래를 먼저 정해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 응답 담당자&lt;/li&gt;
&lt;li&gt;증거 확인 방식&lt;/li&gt;
&lt;li&gt;허위 신고 처리 기준&lt;/li&gt;
&lt;li&gt;처벌 수위 기록 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 결과는 신고자에게 짧게라도 안내한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 전부 공개할 필요는 없습니다.&lt;br /&gt;&lt;br /&gt;다만 신고를 넣은 사람이 아무 피드백도 못 받으면 채널 신뢰도가 떨어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;확인 중&lt;/code&gt;, &lt;code&gt;처리 완료&lt;/code&gt;, &lt;code&gt;추가 정보 필요&lt;/code&gt; 정도 상태 안내는 남겨 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 처벌 로그와 반드시 연결한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고만 받고 처벌 기록이 따로 없으면 나중에 기준을 확인하기 어렵습니다.&lt;br /&gt;&lt;br /&gt;신고 결과가 경고, 타임아웃, 킥으로 이어졌다면 처벌 로그 채널에 남겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 &lt;a href=&quot;https://blog.dishost.kr/129&quot;&gt;처벌 로그 디스코드 봇 만들기&lt;/a&gt;와 바로 연결됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 거짓 신고와 장난 신고 기준도 미리 적는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고 시스템이 열리면 장난 신고도 반드시 들어옵니다.&lt;br /&gt;&lt;br /&gt;처음부터 허위 신고 기준과 제재 가능성을 짧게 적어 둬야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래야 운영진이 대응할 때도 기준이 흔들리지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 로그와 보안 구조를 같이 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고 처리는 기록과 권한이 함께 가야 합니다.&lt;br /&gt;&lt;br /&gt;신고 채널이 누구에게 보이는지, 로그 채널에 무엇을 남기는지, 운영진 역할이 어디까지 접근하는지 같이 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 기본 구조는 &lt;a href=&quot;https://blog.dishost.kr/110&quot;&gt;디스코드 서버 보안 설정 체크리스트&lt;/a&gt;, 운영 로그는 &lt;a href=&quot;https://blog.dishost.kr/127&quot;&gt;디스코드 로그 기록 봇 만들기&lt;/a&gt;에서 다시 확인해 두면 운영 기준이 흔들리지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 신고 입구를 열었다면 기록 구조까지 이어야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고 동선과 처리 기준을 세웠다면 마지막으로 반복 메시지와 광고를 자동으로 막는 스팸 방지 구조까지 붙일 수 있습니다.&lt;br /&gt;&lt;br /&gt;그 전에 문의 입구 전체를 다시 다듬고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/122&quot;&gt;디스코드 문의 동선 설계법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/121&quot;&gt;디스코드 티켓 카테고리 설계법&lt;/a&gt;을 같이 점검하면 됩니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/130</guid>
      <comments>https://dishost.tistory.com/130#entry130comment</comments>
      <pubDate>Tue, 5 May 2026 16:56:21 +0900</pubDate>
    </item>
    <item>
      <title>처벌 로그 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/129</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;경고, 타임아웃, 킥, 밴 같은 처벌은 남길수록 민감해집니다.&lt;br /&gt;&lt;br /&gt;그만큼 누가 왜 조치했는지 기록이 없으면 운영진 내부에서도 기준이 흐려지기 쉽습니다.&lt;br /&gt;&lt;br /&gt;처벌 로그 봇은 제재 자체보다 제재 이력을 일정한 형식으로 남기는 데 의미가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 슬래시 명령어로 경고를 기록하고, 타임아웃 로그를 남기는 기본 구조를 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 왜 별도 로그가 필요한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 삭제 로그나 입장 로그와 달리 처벌 기록은 나중에 다시 볼 일이 많습니다.&lt;br /&gt;&lt;br /&gt;재제 기준이 일관됐는지, 특정 유저에게 어떤 이력이 쌓였는지 확인할 때 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고 흐름과 같이 붙여 볼 계획이라면 문의&amp;middot;운영 동선이 섞이지 않게 먼저 분리해 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 처벌 로그 채널 값 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
GUILD_ID=테스트_서버_ID
MOD_LOG_CHANNEL_ID=처벌로그채널_ID
MOD_ROLE_ID=운영진역할_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;처벌 로그 채널은 운영진만 보게 두는 것이 기본입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 제재 기록을 남기는 기본 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os
from datetime import timedelta

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))
MOD_LOG_CHANNEL_ID = int(os.getenv(&quot;MOD_LOG_CHANNEL_ID&quot;))
MOD_ROLE_ID = int(os.getenv(&quot;MOD_ROLE_ID&quot;))

intents = discord.Intents.default()
intents.members = True
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)


def is_moderator(member: discord.Member, mod_role_id: int) -&amp;gt; bool:
    return any(role.id == mod_role_id for role in member.roles)


async def send_mod_log(guild_obj: discord.Guild, title: str, lines: list[str], color: discord.Color):
    channel = guild_obj.get_channel(MOD_LOG_CHANNEL_ID)
    if not isinstance(channel, discord.TextChannel):
        return

    embed = discord.Embed(title=title, description=&quot;\n&quot;.join(lines), color=color)
    await channel.send(embed=embed)


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;경고&quot;, description=&quot;유저 경고 로그를 남깁니다.&quot;, guild=guild)
@app_commands.describe(user=&quot;경고할 유저&quot;, reason=&quot;경고 사유&quot;)
async def warn_user(interaction: discord.Interaction, user: discord.Member, reason: str):
    if interaction.guild is None or not isinstance(interaction.user, discord.Member):
        await interaction.response.send_message(&quot;서버 안에서만 사용할 수 있습니다.&quot;, ephemeral=True)
        return

    if not is_moderator(interaction.user, MOD_ROLE_ID):
        await interaction.response.send_message(&quot;운영진만 사용할 수 있습니다.&quot;, ephemeral=True)
        return

    await send_mod_log(
        interaction.guild,
        &quot;경고 로그&quot;,
        [
            f&quot;대상: {user.mention}&quot;,
            f&quot;처리자: {interaction.user.mention}&quot;,
            f&quot;사유: {reason}&quot;,
        ],
        discord.Color.orange(),
    )
    await interaction.response.send_message(&quot;경고 로그를 남겼습니다.&quot;, ephemeral=True)


@tree.command(name=&quot;타임아웃&quot;, description=&quot;유저를 일정 시간 타임아웃 처리합니다.&quot;, guild=guild)
@app_commands.describe(user=&quot;타임아웃할 유저&quot;, minutes=&quot;타임아웃 시간(분)&quot;, reason=&quot;사유&quot;)
async def timeout_user(interaction: discord.Interaction, user: discord.Member, minutes: int, reason: str):
    if interaction.guild is None or not isinstance(interaction.user, discord.Member):
        await interaction.response.send_message(&quot;서버 안에서만 사용할 수 있습니다.&quot;, ephemeral=True)
        return

    if not is_moderator(interaction.user, MOD_ROLE_ID):
        await interaction.response.send_message(&quot;운영진만 사용할 수 있습니다.&quot;, ephemeral=True)
        return

    try:
        await user.timeout(timedelta(minutes=minutes), reason=reason)
        await send_mod_log(
            interaction.guild,
            &quot;타임아웃 로그&quot;,
            [
                f&quot;대상: {user.mention}&quot;,
                f&quot;처리자: {interaction.user.mention}&quot;,
                f&quot;시간: {minutes}분&quot;,
                f&quot;사유: {reason}&quot;,
            ],
            discord.Color.red(),
        )
        await interaction.response.send_message(&quot;타임아웃 처리와 로그 기록이 완료되었습니다.&quot;, ephemeral=True)
    except discord.Forbidden:
        await interaction.response.send_message(&quot;봇 권한이 부족해 타임아웃을 처리하지 못했습니다.&quot;, ephemeral=True)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;핵심은 `처벌 실행`과 `처벌 기록`을 같은 흐름으로 묶는 점입니다.&lt;br /&gt;기록만 남기는 명령어와 실제 제재 명령어를 분리해 두면 운영 기준을 잡기 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 로그 형식부터 먼저 맞춰 보기&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;테스트 서버에서 운영진 역할이 붙은 계정으로 `/경고`, `/타임아웃`을 실행해 봅니다.&lt;br /&gt;로그 채널에 기록이 남고, 타임아웃 대상 유저가 실제로 채팅을 못 치면 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 제재 기록에서 자주 빠지는 값&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타임아웃이 안 되는 경우 봇 역할이 대상 유저 역할보다 낮거나 &lt;code&gt;Moderate Members&lt;/code&gt; 권한이 없을 수 있습니다.&lt;/li&gt;
&lt;li&gt;경고 명령어가 아무나 실행되는 경우 역할 체크를 빠뜨렸을 가능성이 큽니다.&lt;/li&gt;
&lt;li&gt;로그 채널에 안 남는 경우 채널 ID 오류나 채널 접근 권한 부족을 먼저 봅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 로그 형식은 최대한 고정한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처벌 로그마다 문구가 제각각이면 나중에 검색하기 어렵습니다.&lt;br /&gt;&lt;br /&gt;대상, 처리자, 사유, 시간 정도는 항상 같은 순서로 남겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 일관성이 운영팀 내부 기준을 만드는 데도 도움이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 신고 흐름과 같이 붙으면 더 유용하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처벌 기록은 보통 신고 처리 뒤에 생깁니다.&lt;br /&gt;&lt;br /&gt;그래서 신고 접수 채널, 티켓, 처벌 로그는 서로 연결되는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문의 구조는 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt;, 동선은 &lt;a href=&quot;https://blog.dishost.kr/122&quot;&gt;디스코드 문의 동선 설계법&lt;/a&gt;과 같이 보면 흐름이 잘 맞습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 실제 운영에서는 증거 링크도 남겨야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경고 사유만 짧게 적으면 나중에 판단이 어려울 수 있습니다.&lt;br /&gt;&lt;br /&gt;가능하다면 메시지 링크, 스크린샷 위치, 신고 채널 티켓 링크도 같이 남겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 가장 기본적인 기록 구조만 넣었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영용이라 배포가 필요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처벌 로그도 사건이 생길 때 바로 남아야 의미가 있습니다.&lt;br /&gt;&lt;br /&gt;실운영에서는 결국 계속 켜져 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포까지 같이 보려면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 상시 실행 환경을 같이 준비해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 처벌 기록은 신고 접수와 붙을 때 힘이 난다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처벌 기록까지 잡았다면 이제 신고 채널 자체를 어떻게 운영할지 기준을 세우는 단계가 이어집니다.&lt;br /&gt;&lt;br /&gt;로그 구조 전체를 다시 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/127&quot;&gt;디스코드 로그 기록 봇 만들기&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/128&quot;&gt;입장 퇴장 로그 디스코드 봇 만들기&lt;/a&gt;를 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/129</guid>
      <comments>https://dishost.tistory.com/129#entry129comment</comments>
      <pubDate>Mon, 4 May 2026 16:55:54 +0900</pubDate>
    </item>
    <item>
      <title>입장 퇴장 로그 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/128</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;누가 언제 들어오고 나갔는지 기록해 두면 서버 흐름을 보기 훨씬 쉬워집니다.&lt;br /&gt;&lt;br /&gt;유입이 갑자기 늘었는지, 특정 시점 이후 이탈이 많아졌는지, 인증 절차 이후에 나가는지 같은 흐름을 잡기 좋습니다.&lt;br /&gt;&lt;br /&gt;입장 퇴장 로그는 운영 기록 중에서도 가장 자주 보는 축에 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 멤버 입장과 퇴장을 전용 로그 채널에 남기는 기본 봇을 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 입장 로그에 먼저 넣을 정보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입장 퇴장 이벤트를 받으려면 &lt;code&gt;SERVER MEMBERS INTENT&lt;/code&gt;가 필요합니다.&lt;br /&gt;&lt;br /&gt;이 설정이 빠지면 코드가 맞아도 멤버 로그가 아예 안 들어옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 환영 흐름과 같이 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/114&quot;&gt;자동 환영 인사 디스코드 봇 만들기&lt;/a&gt;을 같이 보면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 로그 채널 값 먼저 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
LOG_CHANNEL_ID=입장퇴장_로그채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;로그 채널은 운영진만 보게 두는 구성이 무난합니다.&lt;br /&gt;공개 채널에 계속 퇴장 기록이 보이면 분위기가 어수선해질 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 입장과 퇴장을 남기는 최소 코드&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import os

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
LOG_CHANNEL_ID = int(os.getenv(&quot;LOG_CHANNEL_ID&quot;))

intents = discord.Intents.default()
intents.members = True
client = discord.Client(intents=intents)


async def send_embed(embed: discord.Embed):
    channel = client.get_channel(LOG_CHANNEL_ID)
    if isinstance(channel, discord.TextChannel):
        await channel.send(embed=embed)


@client.event
async def on_ready():
    print(f&quot;로그인 성공: {client.user}&quot;)


@client.event
async def on_member_join(member: discord.Member):
    embed = discord.Embed(title=&quot;멤버 입장&quot;, color=discord.Color.green())
    embed.add_field(name=&quot;유저&quot;, value=f&quot;{member} ({member.id})&quot;, inline=False)
    embed.add_field(name=&quot;계정 생성일&quot;, value=member.created_at.strftime(&quot;%Y-%m-%d %H:%M:%S UTC&quot;), inline=False)
    embed.set_thumbnail(url=member.display_avatar.url)
    await send_embed(embed)


@client.event
async def on_member_remove(member: discord.Member):
    embed = discord.Embed(title=&quot;멤버 퇴장&quot;, color=discord.Color.red())
    embed.add_field(name=&quot;유저&quot;, value=f&quot;{member} ({member.id})&quot;, inline=False)
    embed.add_field(name=&quot;퇴장 시 닉네임&quot;, value=member.display_name, inline=False)
    await send_embed(embed)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;입장 로그에는 계정 생성일을 같이 남겨 둬야 합니다.&lt;br /&gt;새로 만든 계정이 갑자기 많이 들어오는지 볼 때 도움이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Portal 설정도 같이 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Developer Portal에서 아래 항목을 켭니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Bot -&amp;gt; Privileged Gateway Intents
- SERVER MEMBERS INTENT 활성화&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 설정 하나 때문에 처음에 많이 막힙니다.&lt;br /&gt;`on_member_join`과 `on_member_remove`가 조용히 안 도는 경우 대부분 여기입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 입장과 퇴장이 각각 찍히는지 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;테스트 서버에서 부계정으로 들어오고 나가 보면서 로그를 확인합니다.&lt;br /&gt;입장과 퇴장 임베드가 각각 찍히면 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Member 이벤트에서 자주 끊기는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입장 로그가 안 뜨는 경우 &lt;code&gt;SERVER MEMBERS INTENT&lt;/code&gt; 설정 누락이 가장 흔합니다.&lt;/li&gt;
&lt;li&gt;퇴장 로그만 이상하게 늦게 보이는 경우 디스코드 이벤트 전달 지연이 있을 수 있지만, 대개는 봇 상태나 권한을 먼저 봐야 합니다.&lt;/li&gt;
&lt;li&gt;공개 채널에 로그를 둬서 분위기가 어수선한 경우 입장 퇴장은 전용 운영 채널로 옮겨야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 환영 메시지와 함께 보면 유입 흐름이 보인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입장 로그는 기록이고, 환영 메시지는 행동 유도입니다.&lt;br /&gt;&lt;br /&gt;두 기능을 같이 두면 유입은 들어오는데 인증 전에 나가는지, 환영 뒤에 머무는지 같은 흐름을 보기 쉬워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환영 동선은 &lt;a href=&quot;https://blog.dishost.kr/114&quot;&gt;자동 환영 인사 디스코드 봇 만들기&lt;/a&gt;, 인증 흐름은 &lt;a href=&quot;https://blog.dishost.kr/117&quot;&gt;규칙 동의 인증 디스코드 봇 만들기&lt;/a&gt;과 같이 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 로그만 보고 판단하지는 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퇴장 로그가 많다고 해서 반드시 문제인 것은 아닙니다.&lt;br /&gt;&lt;br /&gt;홍보 유입이 늘면 자연스럽게 이탈도 같이 늘 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 입장 퇴장 로그는 소개문, 온보딩, 인증 구조와 같이 봐야 의미가 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영 환경이 필요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입장과 퇴장은 언제 일어날지 모릅니다.&lt;br /&gt;&lt;br /&gt;PC를 끄면 기록이 그대로 비게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영용이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 계속 켜진 환경이 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 입구 로그 다음에는 환영 흐름을 같이 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입장 퇴장 로그까지 분리했다면 다음에는 제재와 경고 같은 처벌 기록을 따로 남기는 구조로 이어갈 수 있습니다.&lt;br /&gt;&lt;br /&gt;입구 품질을 먼저 다시 볼 때는 &lt;a href=&quot;https://blog.dishost.kr/112&quot;&gt;디스코드 서버 홍보 방법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/103&quot;&gt;디스코드 서버 온보딩 채널 구성 예시&lt;/a&gt;를 함께 확인하면 흐름이 잘 보입니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/128</guid>
      <comments>https://dishost.tistory.com/128#entry128comment</comments>
      <pubDate>Sun, 3 May 2026 16:55:27 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 로그 기록 봇 만들기</title>
      <link>https://dishost.tistory.com/127</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 운영하다 보면 나중에 다시 봐야 하는 순간이 자주 생깁니다.&lt;br /&gt;&lt;br /&gt;메시지가 지워졌는지, 채널 이름이 바뀌었는지, 누가 어떤 변경을 했는지 기억만으로는 금방 한계가 옵니다.&lt;br /&gt;&lt;br /&gt;이럴 때 기본 로그 기록 봇을 붙여 두면 운영 판단이 훨씬 쉬워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 메시지 삭제, 수정, 채널 생성 로그를 한 채널에 남기는 기본형부터 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 로그 봇이 필요한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반 서버에서는 없어도 버틸 수 있습니다.&lt;br /&gt;&lt;br /&gt;하지만 운영진이 둘 이상이거나 채널 변경이 잦아지면 로그가 없는 상태가 더 불편해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 아래 같은 상황에서 자주 필요해집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;누가 메시지를 지웠는지 확인하고 싶을 때&lt;/li&gt;
&lt;li&gt;채널 이름이나 구조 변경 이력을 보고 싶을 때&lt;/li&gt;
&lt;li&gt;분쟁이나 문의에 대응할 근거가 필요할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 기본 구조를 먼저 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/110&quot;&gt;디스코드 서버 보안 설정 체크리스트&lt;/a&gt;와 같이 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 로그 채널은 따로 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 공지나 운영 채팅에 로그를 섞으면 금방 읽기 어려워집니다.&lt;br /&gt;&lt;br /&gt;보통은 &lt;code&gt;로그&lt;/code&gt;, &lt;code&gt;mod-log&lt;/code&gt;, &lt;code&gt;audit-log&lt;/code&gt; 같은 전용 채널을 따로 둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한은 운영진과 봇만 보게 두는 구성이 무난합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 로그 채널과 토큰 값 넣기&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
LOG_CHANNEL_ID=로그채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;로그 채널 ID 하나만 받아 두면 대부분 이벤트를 한곳으로 모을 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 메시지 수정과 삭제를 남기는 코드&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import os

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
LOG_CHANNEL_ID = int(os.getenv(&quot;LOG_CHANNEL_ID&quot;))

intents = discord.Intents.default()
intents.guilds = True
intents.messages = True
intents.message_content = True
client = discord.Client(intents=intents)


async def send_log(embed: discord.Embed):
    channel = client.get_channel(LOG_CHANNEL_ID)
    if isinstance(channel, discord.TextChannel):
        await channel.send(embed=embed)


@client.event
async def on_ready():
    print(f&quot;로그인 성공: {client.user}&quot;)


@client.event
async def on_message_delete(message: discord.Message):
    if message.author.bot or message.guild is None:
        return

    embed = discord.Embed(title=&quot;메시지 삭제 로그&quot;, color=discord.Color.red())
    embed.add_field(name=&quot;작성자&quot;, value=message.author.mention, inline=False)
    embed.add_field(name=&quot;채널&quot;, value=message.channel.mention, inline=False)
    embed.add_field(name=&quot;내용&quot;, value=message.content or &quot;첨부파일 또는 빈 메시지&quot;, inline=False)
    await send_log(embed)


@client.event
async def on_message_edit(before: discord.Message, after: discord.Message):
    if before.author.bot or before.guild is None:
        return

    if before.content == after.content:
        return

    embed = discord.Embed(title=&quot;메시지 수정 로그&quot;, color=discord.Color.orange())
    embed.add_field(name=&quot;작성자&quot;, value=before.author.mention, inline=False)
    embed.add_field(name=&quot;채널&quot;, value=before.channel.mention, inline=False)
    embed.add_field(name=&quot;수정 전&quot;, value=before.content or &quot;없음&quot;, inline=False)
    embed.add_field(name=&quot;수정 후&quot;, value=after.content or &quot;없음&quot;, inline=False)
    await send_log(embed)


@client.event
async def on_guild_channel_create(channel: discord.abc.GuildChannel):
    embed = discord.Embed(title=&quot;채널 생성 로그&quot;, color=discord.Color.green())
    embed.add_field(name=&quot;채널 이름&quot;, value=channel.name, inline=False)
    embed.add_field(name=&quot;채널 종류&quot;, value=str(channel.type), inline=False)
    await send_log(embed)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;처음에는 너무 많은 이벤트를 한 번에 붙이지 말아야 합니다.&lt;br /&gt;삭제, 수정, 채널 생성 정도만 잡아도 운영 체감이 크게 달라집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 로그가 남는 이벤트부터 좁게 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그다음 테스트 채널에서 메시지를 하나 보내고 지워 봅니다.&lt;br /&gt;메시지를 수정해 보고, 테스트 채널을 하나 새로 만들어 봅니다.&lt;br /&gt;각 이벤트가 로그 채널에 임베드로 쌓이면 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 로그 채널에서 흔히 생기는 누락&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;삭제 로그에 내용이 안 남는 경우 메시지가 캐시에 없었거나 첨부파일만 있던 경우일 수 있습니다.&lt;/li&gt;
&lt;li&gt;이벤트가 아예 안 들어오는 경우 인텐트가 부족하거나 봇이 채널을 못 보는 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;로그 채널이 너무 시끄러운 경우 이벤트 종류를 줄이거나 카테고리별 로그 채널을 나눠야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 로그는 많을수록 좋은 게 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 이벤트를 다 기록하면 처음에는 든든해 보여도 곧 읽지 않게 됩니다.&lt;br /&gt;&lt;br /&gt;실제로 필요한 기록부터 남겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 메시지 삭제, 수정, 입장/퇴장, 처벌 기록 정도가 먼저입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 특화 로그는 따로 분리해도 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입장 퇴장 로그, 처벌 로그는 운영 목적이 다릅니다.&lt;br /&gt;&lt;br /&gt;그래서 나중에는 별도 채널이나 별도 봇 구조로 나누는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 다음 글들에서 각각 따로 정리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영이라 배포가 거의 필수다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그는 사건이 생긴 뒤에 켜는 기능이 아닙니다.&lt;br /&gt;&lt;br /&gt;항상 켜져 있어야 의미가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실서버에 붙일 생각이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 계속 켜져 있는 환경을 같이 준비해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 로그 다음에는 신고와 처벌 흐름을 묶는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 로그 구조를 잡았다면 다음에는 멤버 입장과 퇴장 같은 서버 출입 로그를 따로 정리하는 흐름이 자연스럽습니다.&lt;br /&gt;&lt;br /&gt;보안 관점에서 먼저 점검하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/110&quot;&gt;디스코드 서버 보안 설정 체크리스트&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/104&quot;&gt;디스코드 커뮤니티 서버 활성화 방법&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/127</guid>
      <comments>https://dishost.tistory.com/127#entry127comment</comments>
      <pubDate>Sat, 2 May 2026 16:55:00 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 모달 입력창 기본 사용법</title>
      <link>https://dishost.tistory.com/126</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;모달을 쓰고 싶은데 예제가 너무 크거나 지원서 전체 코드만 보이면 오히려 구조가 안 잡힐 때가 있습니다.&lt;br /&gt;&lt;br /&gt;그럴 때 필요한 건 완성형 운영 예제가 아니라, 입력칸 정의와 제출 처리만 남긴 가장 작은 레퍼런스입니다.&lt;br /&gt;&lt;br /&gt;이 글은 지원서나 문의 흐름 전체가 아니라, 디스코드 모달의 기본 뼈대를 이해하는 참고 글로 보면 맞습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 긴 입력을 받을 때 '명령어 인자' 대신 쓰는 기본 UI다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 입력 길이가 길거나 형식을 맞춰 받고 싶을 때 잘 맞습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문의 요약 받기&lt;/li&gt;
&lt;li&gt;신고 사유 받기&lt;/li&gt;
&lt;li&gt;지원 이유 받기&lt;/li&gt;
&lt;li&gt;버그 제보 내용 받기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실전 예시를 먼저 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/125&quot;&gt;모달 폼 접수 디스코드 봇 만들기&lt;/a&gt;를 같이 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 모달을 붙이기 전에 확인할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬래시 명령어 기본 구조는 이미 있다는 전제로 갑니다.&lt;br /&gt;&lt;br /&gt;아직 slash 명령어가 익숙하지 않다면 &lt;a href=&quot;https://blog.dishost.kr/61&quot;&gt;슬래시 명령어 디스코드 봇 만드는 법, 파이썬 discord.py app_commands 입문&lt;/a&gt;부터 먼저 맞춰야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 명령어와 채널 값 먼저 넣기&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
GUILD_ID=테스트_서버_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 글에서는 가장 작은 예제로 `/문의폼` 명령어를 만들고 모달을 띄웁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 모달 호출과 응답의 최소 구조&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import os

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)


class FeedbackModal(discord.ui.Modal, title=&quot;문의 폼&quot;):
    subject = discord.ui.TextInput(label=&quot;제목&quot;, max_length=50)
    detail = discord.ui.TextInput(label=&quot;상세 내용&quot;, style=discord.TextStyle.paragraph, max_length=500)

    async def on_submit(self, interaction: discord.Interaction):
        embed = discord.Embed(title=&quot;모달 제출 결과&quot;, color=discord.Color.blurple())
        embed.add_field(name=&quot;제목&quot;, value=str(self.subject), inline=False)
        embed.add_field(name=&quot;상세 내용&quot;, value=str(self.detail), inline=False)
        await interaction.response.send_message(embed=embed, ephemeral=True)


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;문의폼&quot;, description=&quot;기본 모달 입력창을 엽니다.&quot;, guild=guild)
async def open_feedback_modal(interaction: discord.Interaction):
    await interaction.response.send_modal(FeedbackModal())


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 예제는 가장 기본적인 모달 흐름만 남겼습니다.&lt;br /&gt;슬래시 명령어가 모달을 열고, 제출된 값을 바로 ephemeral 응답으로 보여 주는 구조입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 코드에서 봐야 할 핵심&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;discord.ui.Modal&lt;/code&gt;을 상속한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TextInput&lt;/code&gt;으로 입력칸을 만든다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;on_submit()&lt;/code&gt;에서 제출 뒤 동작을 처리한다&lt;/li&gt;
&lt;li&gt;명령어나 버튼에서 &lt;code&gt;send_modal()&lt;/code&gt;을 호출한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달은 결국 입력 UI를 따로 여는 컴포넌트라고 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 입력창이 실제로 뜨는지 먼저 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;실행 후 `/문의폼`을 입력합니다.&lt;br /&gt;모달 창이 뜨고 값을 넣어 제출하면 본인에게만 결과가 보이면 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 응답 타이밍에서 자주 막히는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명령어는 보이는데 모달이 안 뜨는 경우 응답을 &lt;code&gt;send_message()&lt;/code&gt;로 보내고 있을 수 있습니다.&lt;br /&gt;&lt;br /&gt;모달은 &lt;code&gt;send_modal()&lt;/code&gt;로 열어야 합니다.&lt;/li&gt;
&lt;li&gt;제출 직후 &lt;code&gt;This interaction failed&lt;/code&gt;가 뜨는 경우 &lt;code&gt;on_submit()&lt;/code&gt; 안에서 응답을 안 보냈거나 예외가 난 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;입력칸 개수를 너무 많이 넣은 경우 초반에는 무엇이 필수인지 흐려져서 제출률이 떨어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 어디에 응용할 수 있을까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 모달 구조를 익혀 두면 아래로 바로 확장됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제휴 신청 폼&lt;/li&gt;
&lt;li&gt;신고 접수 폼&lt;/li&gt;
&lt;li&gt;이벤트 참가 신청 폼&lt;/li&gt;
&lt;li&gt;버그 제보 폼&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 입력칸과 제출 후 저장 위치만 바꾸면 되는 경우가 많습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 모달은 다른 자동화 글과 같이 보면 더 잘 잡힌다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 결과를 검토 채널에 보내고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/125&quot;&gt;모달 폼 접수 디스코드 봇 만들기&lt;/a&gt;가 바로 이어집니다.&lt;br /&gt;&lt;br /&gt;버튼과 함께 붙이고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt; 구조와도 잘 맞습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 기본기를 잡았다면 이제 운영 흐름 안에 끼워 넣는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달 기본기를 익혔다면 다음에는 이 입력 UI를 실제 운영 흐름 안에 끼워 넣는 단계로 가면 됩니다.&lt;br /&gt;&lt;br /&gt;검토 채널까지 붙은 실전형 예제를 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/125&quot;&gt;모달 폼 접수 디스코드 봇 만들기&lt;/a&gt;를, 슬래시 명령어 구조부터 다시 확인하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/66&quot;&gt;디스코드 봇 slash command 안 뜰 때 기본 점검&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/126</guid>
      <comments>https://dishost.tistory.com/126#entry126comment</comments>
      <pubDate>Fri, 1 May 2026 16:54:34 +0900</pubDate>
    </item>
    <item>
      <title>모달 폼 접수 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/125</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문의나 지원서를 받을 때 DM으로 질문을 하나씩 주고받는 방식이 길고 지저분하게 느껴지는 순간이 있습니다.&lt;br /&gt;&lt;br /&gt;그럴 때 모달 폼은 &quot;첫 입력을 한 번에 정리해서 받는 장치&quot;로 아주 강합니다.&lt;br /&gt;&lt;br /&gt;이 글은 모달 자체 설명보다, 버튼을 눌렀을 때 여러 입력칸을 한 번에 받아 검토 채널로 보내는 수집 패턴에 집중합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 첫 입력을 한 번에 정리해서 받아야 할 때 맞는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 같은 상황에서 특히 잘 맞습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지원서 접수&lt;/li&gt;
&lt;li&gt;제휴 문의 접수&lt;/li&gt;
&lt;li&gt;신고 사유 입력&lt;/li&gt;
&lt;li&gt;긴 설명이 필요한 운영 문의&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문을 순차 DM으로 받는 방식과 비교하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/124&quot;&gt;지원서 접수 디스코드 봇 만들기&lt;/a&gt;를 같이 보면 차이가 잘 보입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 버튼과 채널 ID 먼저 묶기&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
GUILD_ID=테스트_서버_ID
REVIEW_CHANNEL_ID=검토채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;버튼을 눌렀을 때 결과를 보낼 검토 채널 하나를 먼저 준비합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 모달 제출값을 받는 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))
REVIEW_CHANNEL_ID = int(os.getenv(&quot;REVIEW_CHANNEL_ID&quot;))

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)


class ApplyModal(discord.ui.Modal, title=&quot;지원 폼&quot;):
    nickname = discord.ui.TextInput(label=&quot;닉네임&quot;, placeholder=&quot;디스코드 닉네임 또는 활동명을 입력해 주세요.&quot;, max_length=30)
    reason = discord.ui.TextInput(label=&quot;지원 이유&quot;, style=discord.TextStyle.paragraph, max_length=500)
    experience = discord.ui.TextInput(label=&quot;관련 경험&quot;, style=discord.TextStyle.paragraph, required=False, max_length=500)

    async def on_submit(self, interaction: discord.Interaction):
        review_channel = client.get_channel(REVIEW_CHANNEL_ID)
        if not isinstance(review_channel, discord.TextChannel):
            await interaction.response.send_message(&quot;검토 채널을 찾지 못했습니다.&quot;, ephemeral=True)
            return

        embed = discord.Embed(title=&quot;새 모달 지원서&quot;, color=discord.Color.blurple())
        embed.add_field(name=&quot;지원자&quot;, value=interaction.user.mention, inline=False)
        embed.add_field(name=&quot;닉네임&quot;, value=str(self.nickname), inline=False)
        embed.add_field(name=&quot;지원 이유&quot;, value=str(self.reason), inline=False)
        embed.add_field(name=&quot;관련 경험&quot;, value=str(self.experience) or &quot;없음&quot;, inline=False)

        await review_channel.send(embed=embed)
        await interaction.response.send_message(&quot;지원서가 접수되었습니다. 검토 후 안내드리겠습니다.&quot;, ephemeral=True)


class ApplyView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=None)

    @discord.ui.button(label=&quot;지원서 작성&quot;, style=discord.ButtonStyle.primary, custom_id=&quot;apply:open&quot;)
    async def open_modal(self, interaction: discord.Interaction, button: discord.ui.Button):
        await interaction.response.send_modal(ApplyModal())


@client.event
async def on_ready():
    client.add_view(ApplyView())
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;지원패널&quot;, description=&quot;모달 지원 패널을 보냅니다.&quot;, guild=guild)
async def apply_panel(interaction: discord.Interaction):
    embed = discord.Embed(
        title=&quot;지원서 접수&quot;,
        description=&quot;아래 버튼을 눌러 지원 폼을 작성해 주세요.&quot;,
        color=discord.Color.green(),
    )
    await interaction.response.send_message(embed=embed, view=ApplyView())


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;핵심은 `discord.ui.Modal`을 상속해 입력칸을 정의하고, 버튼 클릭에서 `send_modal()`을 호출하는 구조입니다.&lt;br /&gt;폼 기반 입력이 필요한 상황에서 가장 먼저 익혀 둘 패턴입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 입력창 열기부터 제출까지 점검&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;실행 후 `/지원패널`을 입력합니다.&lt;br /&gt;채널에 버튼이 붙은 패널이 올라오고, 버튼을 누르면 모달이 열려야 정상입니다.&lt;br /&gt;제출 뒤 검토 채널에 임베드가 올라오는지도 같이 확인합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 모달 제출에서 자주 놓치는 점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버튼은 보이는데 모달이 안 뜨는 경우 버튼 콜백에서 &lt;code&gt;interaction.response.send_modal()&lt;/code&gt;을 호출하지 않았을 수 있습니다.&lt;/li&gt;
&lt;li&gt;재시작 뒤 버튼이 죽는 경우 persistent view 등록이 빠졌을 가능성이 큽니다.&lt;/li&gt;
&lt;li&gt;검토 채널에 결과가 안 쌓이는 경우 채널 ID 오류나 봇 권한 부족을 먼저 봅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 모달은 질문 수를 너무 많이 넣으면 이탈이 늘어난다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력칸이 많아질수록 제출률이 내려갑니다.&lt;br /&gt;&lt;br /&gt;처음에는 핵심 질문 세 개 안팎으로 시작하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 세부 질문은 이후 면접 채널이나 DM에서 이어가면 더 자연스럽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 모달은 티켓과도 잘 붙는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼으로 티켓을 열고, 첫 입력을 모달로 받는 구조도 많이 씁니다.&lt;br /&gt;&lt;br /&gt;예를 들어 문의 유형과 요약 내용을 먼저 모달로 받은 뒤 전용 채널을 열면 초기 정리가 훨씬 쉬워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt;과 같이 확장하기 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 기본 원리를 따로 이해해 두면 응용이 쉬워진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달은 지원서뿐 아니라 신고 접수, 오류 제보, 제휴 신청에도 그대로 쓸 수 있습니다.&lt;br /&gt;&lt;br /&gt;입력칸 정의, 제출 처리, 검토 채널 전송이라는 틀만 이해하면 대부분 비슷하게 확장됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영이라면 배포를 같이 준비한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달 패널도 결국 버튼 기반 기능이라 언제든 눌릴 수 있습니다.&lt;br /&gt;&lt;br /&gt;실서버에서 쓰려면 봇이 계속 살아 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영용으로 붙일 생각이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 상시 실행 환경을 같이 준비해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 수집 패턴을 이해했다면 이제 모달 자체를 더 작게 뜯어본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글로 모달 수집 패턴을 익혔다면 다음에는 질문 수와 입력칸 설계를 더 단순하게 다듬어야 합니다.&lt;br /&gt;&lt;br /&gt;순차 DM 수집과 비교하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/124&quot;&gt;지원서 접수 디스코드 봇 만들기&lt;/a&gt;를 같이 보고, 티켓 입구와 붙여서 생각하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt;를 함께 보면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/125</guid>
      <comments>https://dishost.tistory.com/125#entry125comment</comments>
      <pubDate>Thu, 30 Apr 2026 16:54:08 +0900</pubDate>
    </item>
    <item>
      <title>지원서 접수 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/124</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지원서를 받는데 답변 형식이 매번 달라서 운영진이 비교도 못 하고 있다면, 그건 사람 문제가 아니라 수집 방식 문제입니다.&lt;br /&gt;&lt;br /&gt;이 글의 역할은 티켓처럼 즉시 대화하는 구조가 아니라, 같은 질문을 같은 순서로 받아 비동기 검토가 가능하게 만드는 데 있습니다.&lt;br /&gt;&lt;br /&gt;즉 이 글은 운영진 모집이나 신청 접수처럼 구조화된 답변을 DM 흐름으로 모으는 워크플로 글입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 대화보다 '같은 질문을 빠짐없이 받는 것'이 중요할 때 쓴다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 같은 상황에서 유용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운영진 지원서 접수&lt;/li&gt;
&lt;li&gt;이벤트 신청 폼&lt;/li&gt;
&lt;li&gt;파트너 제휴 문의 초안 수집&lt;/li&gt;
&lt;li&gt;기본 정보를 빠짐없이 받아야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문의 대기열 구조와 같이 쓰고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/123&quot;&gt;고객 지원 대기열 디스코드 봇 만들기&lt;/a&gt;와 묶어서 생각하면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 지원 접수용 변수 먼저 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
GUILD_ID=테스트_서버_ID
REVIEW_CHANNEL_ID=검토채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;답변 결과를 운영진이 보는 검토 채널 하나가 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 질문 수집과 검토 채널 전달 코드&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import asyncio
import os

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))
REVIEW_CHANNEL_ID = int(os.getenv(&quot;REVIEW_CHANNEL_ID&quot;))
QUESTIONS = [
    &quot;닉네임을 적어 주세요.&quot;,
    &quot;지원하는 역할을 적어 주세요.&quot;,
    &quot;관련 경험이 있다면 간단히 적어 주세요.&quot;,
    &quot;하루 평균 활동 가능한 시간을 적어 주세요.&quot;,
]

intents = discord.Intents.default()
intents.messages = True
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;지원시작&quot;, description=&quot;DM으로 지원서 질문을 시작합니다.&quot;, guild=guild)
async def apply_start(interaction: discord.Interaction):
    await interaction.response.send_message(&quot;DM으로 지원서를 시작합니다.&quot;, ephemeral=True)

    try:
        dm_channel = await interaction.user.create_dm()
        answers = []

        for question in QUESTIONS:
            await dm_channel.send(question)

            def check(message: discord.Message) -&amp;gt; bool:
                return message.author.id == interaction.user.id and message.channel.id == dm_channel.id

            reply = await client.wait_for(&quot;message&quot;, timeout=300, check=check)
            answers.append(reply.content.strip())

        review_channel = client.get_channel(REVIEW_CHANNEL_ID)
        if isinstance(review_channel, discord.TextChannel):
            embed = discord.Embed(title=&quot;새 지원서&quot;, color=discord.Color.green())
            embed.add_field(name=&quot;지원자&quot;, value=interaction.user.mention, inline=False)
            for question, answer in zip(QUESTIONS, answers):
                embed.add_field(name=question, value=answer or &quot;응답 없음&quot;, inline=False)
            await review_channel.send(embed=embed)

        await dm_channel.send(&quot;지원서 접수가 완료되었습니다. 검토 후 안내드리겠습니다.&quot;)
    except discord.Forbidden:
        await interaction.followup.send(&quot;DM을 보낼 수 없습니다. 개인 메시지 허용 설정을 먼저 확인해 주세요.&quot;, ephemeral=True)
    except asyncio.TimeoutError:
        await interaction.followup.send(&quot;응답 시간이 지나 지원서가 종료되었습니다. 다시 /지원시작 명령어를 실행해 주세요.&quot;, ephemeral=True)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 구조는 답변을 순서대로 받아 검토 채널에 모으는 가장 기본형입니다.&lt;br /&gt;질문을 바꾸고 싶다면 `QUESTIONS` 리스트만 바꿔도 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 질문 입력부터 검토 채널까지 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그다음 `/지원시작`을 실행합니다.&lt;br /&gt;DM으로 질문이 순서대로 오고, 마지막 답변 뒤 검토 채널에 임베드가 올라오면 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 지원 흐름에서 자주 끊기는 곳&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DM이 안 오는 경우 유저 개인 설정에서 서버 멤버 DM 차단을 켠 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;중간에 응답이 끊기는 경우 기본 타임아웃 300초를 넘겼을 수 있습니다.&lt;br /&gt;&lt;br /&gt;지원서가 길다면 시간을 늘려야 합니다.&lt;/li&gt;
&lt;li&gt;검토 채널에 아무것도 안 올라오는 경우 &lt;code&gt;REVIEW_CHANNEL_ID&lt;/code&gt;가 틀렸거나 봇이 채널을 못 보는 경우가 많습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 질문 수는 너무 많지 않게 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목이 많을수록 중간 이탈이 늘어납니다.&lt;br /&gt;&lt;br /&gt;초반에는 꼭 필요한 질문만 남겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 4~6개 안쪽이 무난합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 일반 문의와 지원서를 분리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원서는 정해진 형식이 필요하고, 일반 문의는 유연성이 필요합니다.&lt;br /&gt;&lt;br /&gt;둘을 같은 동선으로 섞으면 둘 다 불편해지기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 문의는 티켓이나 대기열로 받고, 지원서는 별도 명령어나 버튼으로 받으면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 운영 단계에서는 저장 방식도 강화한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 구조는 검토 채널 메시지에 결과가 남는 정도로 충분합니다.&lt;br /&gt;&lt;br /&gt;지원량이 많아지면 CSV 저장이나 DB 저장을 붙이면 더 편해질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 먼저 질문 흐름을 빠르게 구현하는 데 집중했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 배포는 거의 필수다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원서는 운영 시간 밖에도 들어올 수 있습니다.&lt;br /&gt;&lt;br /&gt;개발용 PC를 꺼 두면 질문 흐름도 같이 멈춥니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 모집에 쓸 생각이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 상시 실행 환경을 같이 준비해야 현실적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. DM 순차형이 답답해지면 모달형으로 넘어간다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 질문씩 순서대로 받는 구조가 길고 답답하게 느껴지면, 이 글의 질문 수를 줄이거나 검토 채널 포맷부터 더 짧게 다듬는 게 먼저입니다.&lt;br /&gt;&lt;br /&gt;입구 버튼과 검토 채널 동선부터 다시 정리하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/121&quot;&gt;디스코드 티켓 카테고리 설계법&lt;/a&gt;을 같이 놓고 보면 동선이 정리됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/124</guid>
      <comments>https://dishost.tistory.com/124#entry124comment</comments>
      <pubDate>Wed, 29 Apr 2026 16:53:42 +0900</pubDate>
    </item>
    <item>
      <title>고객 지원 대기열 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/123</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문의가 적을 때는 누가 먼저 왔는지 채널만 훑어도 보입니다.&lt;br /&gt;&lt;br /&gt;문제가 시작되는 시점은 문의 수보다 담당자 수가 늘어날 때입니다.&lt;br /&gt;&lt;br /&gt;이 글에서는 문의자를 큐에 넣고, 담당자가 다음 문의를 하나씩 꺼내는 가장 작은 대기열 구조를 만들어서 순서가 왜 자주 꼬이는지부터 정리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 문의 수보다 담당자 수가 늘 때 필요해진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 상황이 보이면 대기열이 도움이 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문의가 동시에 여러 개 들어온다&lt;/li&gt;
&lt;li&gt;담당 운영진이 둘 이상이다&lt;/li&gt;
&lt;li&gt;처리 순서를 자주 놓친다&lt;/li&gt;
&lt;li&gt;먼저 온 문의보다 나중 문의가 먼저 답변되는 일이 반복된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문의 입구 구조가 아직 흐릿하다면 &lt;a href=&quot;https://blog.dishost.kr/122&quot;&gt;디스코드 문의 동선 설계법&lt;/a&gt;부터 먼저 맞춰야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 대기열 채널과 역할 값 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
GUILD_ID=테스트_서버_ID
QUEUE_CHANNEL_ID=문의대기열_채널_ID
SUPPORT_ROLE_ID=문의담당_역할_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;대기열 상태를 임베드로 보여 줄 채널 하나를 따로 두면 관리가 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 문의 순서를 줄 세우는 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os
from collections import deque

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))
QUEUE_CHANNEL_ID = int(os.getenv(&quot;QUEUE_CHANNEL_ID&quot;))
SUPPORT_ROLE_ID = int(os.getenv(&quot;SUPPORT_ROLE_ID&quot;))

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)
support_queue: deque[int] = deque()


async def send_queue_status(guild_obj: discord.Guild):
    channel = guild_obj.get_channel(QUEUE_CHANNEL_ID)
    if not isinstance(channel, discord.TextChannel):
        return

    if support_queue:
        lines = [f&quot;{index + 1}. &amp;lt;@{user_id}&amp;gt;&quot; for index, user_id in enumerate(support_queue)]
        description = &quot;\n&quot;.join(lines)
    else:
        description = &quot;현재 대기 중인 문의가 없습니다.&quot;

    embed = discord.Embed(title=&quot;고객 지원 대기열&quot;, description=description, color=discord.Color.orange())
    await channel.send(embed=embed)


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;대기열추가&quot;, description=&quot;본인을 문의 대기열에 추가합니다.&quot;, guild=guild)
async def enqueue(interaction: discord.Interaction):
    if interaction.user.id in support_queue:
        await interaction.response.send_message(&quot;이미 대기열에 등록되어 있습니다.&quot;, ephemeral=True)
        return

    support_queue.append(interaction.user.id)
    await interaction.response.send_message(&quot;대기열에 등록되었습니다.&quot;, ephemeral=True)
    if interaction.guild:
        await send_queue_status(interaction.guild)


@tree.command(name=&quot;다음문의&quot;, description=&quot;대기열에서 다음 문의자를 호출합니다.&quot;, guild=guild)
async def dequeue(interaction: discord.Interaction):
    if interaction.guild is None:
        await interaction.response.send_message(&quot;서버 안에서만 사용할 수 있습니다.&quot;, ephemeral=True)
        return

    member = interaction.guild.get_member(interaction.user.id)
    support_role = interaction.guild.get_role(SUPPORT_ROLE_ID)
    if member is None or support_role is None or support_role not in member.roles:
        await interaction.response.send_message(&quot;문의 담당 역할이 있는 사용자만 실행할 수 있습니다.&quot;, ephemeral=True)
        return

    if not support_queue:
        await interaction.response.send_message(&quot;대기 중인 문의가 없습니다.&quot;, ephemeral=True)
        return

    next_user_id = support_queue.popleft()
    await interaction.response.send_message(f&quot;다음 문의자: &amp;lt;@{next_user_id}&amp;gt;&quot;)
    await send_queue_status(interaction.guild)


@tree.command(name=&quot;대기열보기&quot;, description=&quot;현재 문의 대기열을 봅니다.&quot;, guild=guild)
async def show_queue(interaction: discord.Interaction):
    if support_queue:
        lines = [f&quot;{index + 1}. &amp;lt;@{user_id}&amp;gt;&quot; for index, user_id in enumerate(support_queue)]
        message = &quot;\n&quot;.join(lines)
    else:
        message = &quot;현재 대기 중인 문의가 없습니다.&quot;

    await interaction.response.send_message(message, ephemeral=True)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 구조는 메모리 기반 큐라 재시작하면 대기열이 비워집니다.&lt;br /&gt;대신 작동 원리를 잡고 운영 흐름을 확인하기에는 충분합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 접수부터 호출까지 한 번 돌려 보기&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그다음 아래처럼 확인합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;일반 유저가 &lt;code&gt;/대기열추가&lt;/code&gt; 실행&lt;/li&gt;
&lt;li&gt;문의 담당자가 &lt;code&gt;/대기열보기&lt;/code&gt;로 순서 확인&lt;/li&gt;
&lt;li&gt;담당자가 &lt;code&gt;/다음문의&lt;/code&gt; 실행&lt;/li&gt;
&lt;li&gt;큐에서 첫 번째 유저가 빠지는지 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 순번 관리에서 흔히 어긋나는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 유저가 중복으로 계속 들어가는 경우 큐 등록 전 중복 체크가 빠졌을 수 있습니다.&lt;/li&gt;
&lt;li&gt;담당자가 &lt;code&gt;다음문의&lt;/code&gt;를 못 쓰는 경우 역할 ID가 틀렸거나 테스트 계정에 역할이 안 붙어 있을 수 있습니다.&lt;/li&gt;
&lt;li&gt;대기열 메시지가 너무 많이 쌓이는 경우 상태 임베드를 수정하지 않고 새 메시지로만 쌓는 구조이기 때문입니다.&lt;br /&gt;&lt;br /&gt;운영 단계에서는 마지막 상태 메시지를 수정하는 식으로 바꿔야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 티켓과 붙여야 진짜 운영 도구가 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열만 따로 두면 실제 문의 내용과 분리돼 어색할 수 있습니다.&lt;br /&gt;&lt;br /&gt;보통은 티켓 생성 직후 자동 등록하거나, 접수 메시지와 대기열 메시지를 같이 갱신하는 구조로 가야 운영진이 덜 헤맵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티켓 구조가 먼저 필요하다면 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/121&quot;&gt;디스코드 티켓 카테고리 설계법&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 운영 단계에서는 상태 메시지와 담당자 배정도 같이 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열이 실제로 중요해지면 메모리 저장만으로는 부족합니다.&lt;br /&gt;&lt;br /&gt;재시작 뒤에도 순서가 유지되게 파일 저장이나 DB를 붙여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 자주 놓치는 부분은 상태 메시지를 계속 새로 보내서 채널이 지저분해지는 문제입니다.&lt;br /&gt;&lt;br /&gt;운영 단계에서는 마지막 대기열 메시지를 수정하거나, 문의를 잡아 간 운영진 이름까지 같이 남기는 쪽이 훨씬 실용적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 문의량보다 운영팀 규모를 먼저 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문의가 많아도 담당자가 한 명이면 복잡한 큐 기능보다 기본 응답 템플릿과 처리 시간 공지가 더 중요할 때가 많습니다.&lt;br /&gt;&lt;br /&gt;반대로 담당자가 여러 명이면 큐 번호 하나만 있어도 체감 정리가 크게 좋아집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영이라면 결국 서버에 올려야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열은 문의가 들어오는 순간부터 의미가 있습니다.&lt;br /&gt;&lt;br /&gt;개발 PC를 끄면 순서 관리도 같이 끊깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영용으로 옮길 때는 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 계속 켜져 있는 환경을 같이 준비해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 대기열 뒤에는 접수 양식과 분류가 붙어야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문의 순서를 잡았다면 다음에는 접수 양식과 처리 분류를 붙여서 운영 부담을 더 줄일 수 있습니다.&lt;br /&gt;&lt;br /&gt;입구 설계와 티켓 흐름을 함께 다듬을 때는 &lt;a href=&quot;https://blog.dishost.kr/122&quot;&gt;디스코드 문의 동선 설계법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt;을 이어서 보면 흐름이 잡힙니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/123</guid>
      <comments>https://dishost.tistory.com/123#entry123comment</comments>
      <pubDate>Tue, 28 Apr 2026 16:53:17 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 문의 동선 설계법</title>
      <link>https://dishost.tistory.com/122</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문의 채널은 있는데 정작 유저가 어디를 눌러야 할지 몰라 첫 질문도 못 남긴다면, 티켓 봇이 있어도 절반은 실패입니다.&lt;br /&gt;&lt;br /&gt;이 단계에서 중요한 건 카테고리보다 첫 클릭입니다.&lt;br /&gt;&lt;br /&gt;이 글은 문의를 어떻게 처리할지보다, 유저가 어떤 문구를 보고 어느 버튼을 눌러야 하는지를 설계하는 입구 분기 글입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 유저는 분류 체계보다 '내 상황과 비슷한 버튼'을 찾는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반 서버에서 &lt;code&gt;버그문의&lt;/code&gt;, &lt;code&gt;결제문의&lt;/code&gt;, &lt;code&gt;파트너문의&lt;/code&gt;, &lt;code&gt;일반문의&lt;/code&gt;를 전부 따로 열면 오히려 더 어렵습니다.&lt;br /&gt;&lt;br /&gt;처음 들어온 사람은 자기 문의가 어디에 해당하는지 바로 판단하지 못하는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음에는 &lt;code&gt;문의-안내&lt;/code&gt; 채널 하나에서 시작하고, 버튼이나 짧은 설명으로 분기시켜야 합니다.&lt;br /&gt;&lt;br /&gt;티켓 생성 구조는 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt;, 카테고리 구조는 &lt;a href=&quot;https://blog.dishost.kr/121&quot;&gt;디스코드 티켓 카테고리 설계법&lt;/a&gt;과 같이 보면 흐름이 잘 맞습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 문의 전에 먼저 읽을 것을 분리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 들어오는 문의 중 일부는 이미 공지나 규칙에 적혀 있는 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;문의 채널만 크게 열어 두면 운영진이 같은 답을 계속 반복하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 입구 문구는 아래 순서가 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 공지 확인
2. 자주 묻는 질문 확인
3. 그래도 해결되지 않으면 문의 버튼 클릭&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 순서만 있어도 불필요한 반복 문의가 꽤 줄어듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 버튼 이름은 문의 유형보다 행동 기준으로 짓는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;기술&lt;/code&gt;, &lt;code&gt;운영&lt;/code&gt;, &lt;code&gt;일반&lt;/code&gt; 같은 이름은 운영진은 이해해도 유저는 헷갈릴 수 있습니다.&lt;br /&gt;&lt;br /&gt;처음에는 &lt;code&gt;버그 신고&lt;/code&gt;, &lt;code&gt;계정 문의&lt;/code&gt;, &lt;code&gt;운영 문의&lt;/code&gt;처럼 행동에 가까운 표현이 낫습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저는 분류 체계보다 자기 상황과 비슷한 문구를 먼저 찾습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 공지 문구는 세 줄 안쪽이 가장 읽히기 쉽다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문의 안내가 길어지면 거의 안 읽힙니다.&lt;br /&gt;&lt;br /&gt;입구 문구는 짧고 바로 행동이 보여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 아래처럼 둘 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;문의 전 공지와 안내 채널을 먼저 확인해 주세요.
해결되지 않으면 아래 버튼을 눌러 문의를 열면 됩니다.
문의 유형에 맞는 버튼을 선택하면 전용 채널이 생성됩니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 정도면 행동 흐름이 바로 보입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 담당 역할도 같이 보이게 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문의가 열렸는데 누가 처리하는지 전혀 안 보이면 응답이 늦게 느껴집니다.&lt;br /&gt;&lt;br /&gt;실제 응답 시간이 비슷해도 체감 속도는 다르게 느껴집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 문의 채널 생성 직후 아래 두 가지를 같이 보여 줘야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;담당 역할 멘션&lt;/li&gt;
&lt;li&gt;예상 처리 순서 또는 운영 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문의를 받은 뒤 아무 안내가 없는 구조는 생각보다 불안감을 크게 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 신고 동선은 일반 문의와 분리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고는 공개 문의와 성격이 다릅니다.&lt;br /&gt;&lt;br /&gt;같은 입구에 두더라도 결과적으로는 다른 티켓 카테고리나 다른 담당자를 타게 만들어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감한 내용이 섞이면 일반 문의 동선 전체가 무거워집니다.&lt;br /&gt;&lt;br /&gt;이 단계에서는 신고 버튼이나 신고 티켓을 별도 분기로 빼는 정도만 해도 운영 피로도가 크게 줄어듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 첫 응답 템플릿을 미리 만들어 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문의가 들어온 뒤 운영진이 매번 처음 문장을 새로 쓰면 속도가 느려집니다.&lt;br /&gt;&lt;br /&gt;초반에는 첫 응답 템플릿 하나만 있어도 체감 품질이 꽤 올라갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 아래 정도면 충분합니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;문의가 접수되었습니다.
확인 후 순서대로 답변드리겠습니다.
추가 정보가 필요하면 이 채널에 이어서 남겨 주세요.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;짧지만 접수, 대기, 추가 행동이 다 들어 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 문의 동선은 채널 구조와 함께 봐야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입구 문구만 잘 써도 해결되지 않는 경우가 있습니다.&lt;br /&gt;&lt;br /&gt;버튼을 눌러도 채널이 안 보이거나, 담당 역할만 보고 유저가 못 보는 구조가 섞이면 흐름이 끊깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 문의 동선을 잡을 때는 권한 구조도 같이 봐야 합니다.&lt;br /&gt;&lt;br /&gt;권한이 불안하다면 &lt;a href=&quot;https://blog.dishost.kr/97&quot;&gt;디스코드 채널이 안 보일 때 권한 해결법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/99&quot;&gt;Missing Access(50001) 오류 해결법&lt;/a&gt;을 같이 확인해야 빠릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 운영팀 규모에 맞춰 단순하게 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영진이 두세 명인데 문의 단계를 여섯 칸으로 나누면 오래 못 갑니다.&lt;br /&gt;&lt;br /&gt;처리 단계, 담당자, 로그 방식까지 전부 복잡해지기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 &lt;code&gt;문의 접수 -&amp;gt; 담당자 확인 -&amp;gt; 해결 -&amp;gt; 종료&lt;/code&gt; 정도만 굴러가도 충분합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 문의 입구를 줄였다면 이제 실제 수집 방식이 따라온다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문의 입구가 정리됐다면 다음에는 버튼을 눌렀을 때 어떤 채널과 권한 구조가 열릴지를 정해야 합니다.&lt;br /&gt;&lt;br /&gt;티켓 채널 생성 흐름과 카테고리&amp;middot;담당자 분리는 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/121&quot;&gt;디스코드 티켓 카테고리 설계법&lt;/a&gt;을 이어서 보면 자연스럽게 정리됩니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/122</guid>
      <comments>https://dishost.tistory.com/122#entry122comment</comments>
      <pubDate>Mon, 27 Apr 2026 16:52:52 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 티켓 카테고리 설계법</title>
      <link>https://dishost.tistory.com/121</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;티켓 채널은 잘 열리는데 운영이 오히려 더 지저분해졌다면, 문제는 봇이 아니라 백오피스 정보구조입니다.&lt;br /&gt;&lt;br /&gt;문의, 신고, 결제, 제휴가 한 카테고리에 다 몰리면 티켓 자동화는 사실상 아무 것도 해결하지 못합니다.&lt;br /&gt;&lt;br /&gt;이 글은 티켓 채널을 여는 글이 아니라, 열린 티켓을 어떤 카테고리와 권한 체계 아래에 배치할지 설계하는 글입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 티켓이 열리는 것과 티켓이 정리되는 것은 다른 일이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 &lt;code&gt;티켓&lt;/code&gt; 카테고리 하나로도 충분합니다.&lt;br /&gt;&lt;br /&gt;다만 아래 상황이 보이면 분리 시점을 검토해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운영 문의와 신고 문의가 자주 섞인다&lt;/li&gt;
&lt;li&gt;담당 역할이 서로 다르다&lt;/li&gt;
&lt;li&gt;처리 우선순위가 다르다&lt;/li&gt;
&lt;li&gt;기록 보존 기간이 다르다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 카테고리 안에 모든 문의를 넣으면 결국 운영진이 제목만 보고 내용을 추측하게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 초반 서버에서 무난한 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 서버라면 아래 두 칸 정도가 관리하기 편합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;문의 티켓
신고 티켓&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;문의는 일반 질문, 오류, 계정 관련 이슈를 처리합니다.&lt;br /&gt;신고는 공개 채널에 바로 적기 어려운 내용을 따로 받는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고 채널 운영 기준은 뒤에서 따로 정리하겠지만, 이 단계에서는 일반 문의와 신고를 같은 카테고리에 섞지 않는 원칙만 먼저 잡아 두면 충분합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 권한은 카테고리에서 먼저 잡는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티켓 채널을 매번 수동으로 고치는 방식은 오래 못 갑니다.&lt;br /&gt;&lt;br /&gt;카테고리 권한을 먼저 맞춰 두고, 새 채널은 그 구조를 그대로 따라가게 만들어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 아래처럼 잡을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;@everyone
- View Channel 차단

문의담당
- View Channel 허용
- Send Messages 허용

운영진
- View Channel 허용
- Send Messages 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;채널별 예외는 최소화해야 관리가 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 티켓 이름 규칙도 정해 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ticket-닉네임&lt;/code&gt;, &lt;code&gt;문의-001&lt;/code&gt;, &lt;code&gt;신고-닉네임&lt;/code&gt;처럼 형식을 먼저 정해 두면 찾기 쉽습니다.&lt;br /&gt;&lt;br /&gt;유저 이름만 넣으면 중복이 생길 수 있으니 ID 일부나 번호를 같이 붙여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 아래처럼 갑니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;문의-jisoo-1042
신고-minho-8821&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 방식은 검색과 로그 정리에도 편합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 열기 채널과 처리 채널을 분리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이 헷갈리는 부분인데, 문의 버튼을 누르는 채널과 실제 문의를 처리하는 채널은 분리해야 합니다.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;문의-안내&lt;/code&gt; 채널은 항상 깔끔하게 유지하고, 실제 대화는 새 티켓 채널에서 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt;와 같이 봐야 흐름이 잘 보입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 카테고리 수를 너무 빨리 늘리지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영팀이 아직 작다면 카테고리를 많이 나눌수록 오히려 더 헷갈립니다.&lt;br /&gt;&lt;br /&gt;문의량이 적은데 &lt;code&gt;결제&lt;/code&gt;, &lt;code&gt;버그&lt;/code&gt;, &lt;code&gt;제안&lt;/code&gt;, &lt;code&gt;파트너&lt;/code&gt;, &lt;code&gt;신고&lt;/code&gt;를 전부 따로 열어 두면 빈 카테고리만 늘어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 두세 개 안쪽으로 묶고, 정말 담당자가 갈릴 때만 나눠야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 닫힌 티켓 처리 기준도 필요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티켓 카테고리 설계는 열려 있는 채널만 보는 일이 아닙니다.&lt;br /&gt;&lt;br /&gt;닫힌 티켓을 보관할지, 로그 채널만 남길지, 완전히 삭제할지도 같이 정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 세 가지 중 하나를 미리 정해 둬야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;종료 즉시 삭제&lt;/li&gt;
&lt;li&gt;종료 후 아카이브 카테고리로 이동&lt;/li&gt;
&lt;li&gt;로그 채널에 요약만 남기고 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 방식을 쓰든 운영진끼리 기준이 같아야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 카테고리 설계는 문의 동선과 붙어 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카테고리만 깔끔해도 유저가 어디로 들어가야 하는지 모르면 효과가 약합니다.&lt;br /&gt;&lt;br /&gt;문의 버튼 문구, 공지 문구, 담당 역할, 처리 순서가 같은 방향을 가리켜야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 카테고리 설계를 끝냈다면 문의 동선까지 이어서 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 실제 운영에서 많이 보는 실수&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신고와 일반 문의를 같은 채널로 받는 경우&lt;/li&gt;
&lt;li&gt;카테고리 권한보다 채널별 개별 권한이 더 많아진 경우&lt;/li&gt;
&lt;li&gt;닫힌 티켓을 계속 쌓아 두고 정리 기준이 없는 경우&lt;/li&gt;
&lt;li&gt;담당 역할이 명확하지 않아 첫 응답이 늦어지는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티켓 구조는 봇 코드보다 운영 기준에서 더 오래 문제를 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 카테고리를 나눴다면 이제 유저 입구 문구를 맞춘다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카테고리 구조를 잡았다면 다음에는 유저가 헷갈리지 않게 안내 문구를 짧게 정리해야 합니다.&lt;br /&gt;&lt;br /&gt;티켓 채널 생성 흐름과 권한 구조를 함께 다시 볼 때는 &lt;a href=&quot;https://blog.dishost.kr/120&quot;&gt;티켓 디스코드 봇 만들기&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/97&quot;&gt;디스코드 채널이 안 보일 때 권한 해결법&lt;/a&gt;을 이어서 보면 정리가 됩니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/121</guid>
      <comments>https://dishost.tistory.com/121#entry121comment</comments>
      <pubDate>Sun, 26 Apr 2026 16:52:26 +0900</pubDate>
    </item>
    <item>
      <title>티켓 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/120</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문의가 늘기 시작했는데도 아직 일반 채팅에서 처리하고 있다면, 답변 품질보다 문의 채널 구조가 먼저 무너집니다.&lt;br /&gt;&lt;br /&gt;티켓 봇의 핵심은 고급 분류가 아니라 &quot;유저 한 명당 개인 문의 공간을 바로 열어 준다&quot;는 데 있습니다.&lt;br /&gt;&lt;br /&gt;이 글은 복잡한 지원 센터가 아니라, 버튼 하나로 개인 문의 채널을 생성하는 가장 작은 티켓 자동화 글입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 티켓의 본질은 개인 문의 공간을 분리하는 것이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드부터 쓰기 전에 아래 세 가지를 먼저 정합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;티켓을 어느 카테고리 아래에 만들 것인가&lt;/li&gt;
&lt;li&gt;누가 볼 수 있어야 하는가&lt;/li&gt;
&lt;li&gt;닫을 때 채널을 삭제할지 보관할지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 공지 흐름은 &lt;a href=&quot;https://blog.dishost.kr/106&quot;&gt;디스코드 공지 채널 운영법&lt;/a&gt;과도 연결됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 필요한 역할과 채널&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 정도를 먼저 준비합니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;역할
- 운영진
- 문의담당

채널
- 문의-안내
카테고리
- 티켓&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`문의-안내` 채널에는 버튼 메시지만 두고, 실제 대화는 티켓 카테고리 아래 새 채널에서 진행하는 방식이 깔끔합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 카테고리와 담당 역할 값 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
TICKET_CATEGORY_ID=티켓_카테고리_ID
SUPPORT_ROLE_ID=문의담당_역할_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;채널 이름을 문자열로 합칠 때는 유저 ID를 같이 붙여 두면 중복 충돌을 줄이기 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 문의 채널을 여닫는 기본 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
TICKET_CATEGORY_ID = int(os.getenv(&quot;TICKET_CATEGORY_ID&quot;))
SUPPORT_ROLE_ID = int(os.getenv(&quot;SUPPORT_ROLE_ID&quot;))

intents = discord.Intents.default()
intents.guilds = True
client = discord.Client(intents=intents)


class TicketView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=None)

    @discord.ui.button(label=&quot;문의 열기&quot;, style=discord.ButtonStyle.primary, custom_id=&quot;ticket:create&quot;)
    async def create_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
        if interaction.guild is None:
            await interaction.response.send_message(&quot;서버 안에서만 사용할 수 있습니다.&quot;, ephemeral=True)
            return

        category = interaction.guild.get_channel(TICKET_CATEGORY_ID)
        support_role = interaction.guild.get_role(SUPPORT_ROLE_ID)

        if not isinstance(category, discord.CategoryChannel) or support_role is None:
            await interaction.response.send_message(&quot;카테고리 또는 담당 역할을 찾지 못했습니다.&quot;, ephemeral=True)
            return

        channel_name = f&quot;ticket-{interaction.user.name}-{interaction.user.id}&quot;
        existing_channel = discord.utils.get(interaction.guild.text_channels, name=channel_name)
        if existing_channel is not None:
            await interaction.response.send_message(f&quot;이미 열린 티켓이 있습니다: {existing_channel.mention}&quot;, ephemeral=True)
            return

        overwrites = {
            interaction.guild.default_role: discord.PermissionOverwrite(view_channel=False),
            interaction.user: discord.PermissionOverwrite(view_channel=True, send_messages=True, read_message_history=True),
            support_role: discord.PermissionOverwrite(view_channel=True, send_messages=True, read_message_history=True),
            interaction.guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True, manage_channels=True),
        }

        ticket_channel = await interaction.guild.create_text_channel(
            name=channel_name,
            category=category,
            overwrites=overwrites,
            reason=&quot;티켓 문의 채널 생성&quot;,
        )

        await ticket_channel.send(
            f&quot;{interaction.user.mention} 님 문의가 접수되었습니다.\n&quot;
            f&quot;{support_role.mention} 확인 후 이 채널에서 답변해 주세요.&quot;,
            view=CloseTicketView(),
        )
        await interaction.response.send_message(f&quot;티켓이 생성되었습니다: {ticket_channel.mention}&quot;, ephemeral=True)


class CloseTicketView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=None)

    @discord.ui.button(label=&quot;티켓 닫기&quot;, style=discord.ButtonStyle.danger, custom_id=&quot;ticket:close&quot;)
    async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
        await interaction.response.send_message(&quot;이 채널을 닫습니다.&quot;, ephemeral=True)
        await interaction.channel.delete(reason=&quot;티켓 종료&quot;)


@client.event
async def on_ready():
    client.add_view(TicketView())
    client.add_view(CloseTicketView())
    print(f&quot;로그인 성공: {client.user}&quot;)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;핵심은 안내 채널에서 버튼을 누르면 새 개인 문의 채널이 열리고, 닫기 버튼으로 바로 정리하는 흐름입니다.&lt;br /&gt;재시작 뒤에도 버튼이 살아 있게 하려면 `custom_id`와 `add_view()`를 같이 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 안내 메시지 올리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;문의-안내&lt;/code&gt; 채널에는 아래 같은 문구를 먼저 올립니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;문의가 필요하면 아래 버튼을 눌러 주세요.
새 채널이 열리면 그 안에서 내용을 남기면 됩니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 메시지를 보낼 때 `view=TicketView()`를 같이 붙이면 됩니다.&lt;br /&gt;초반에는 한 채널에 문의 버튼 하나만 두면 덜 헷갈립니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 채널 생성과 persistent view에서 막히는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버튼은 눌리는데 채널 생성이 안 되는 경우 봇에 &lt;code&gt;Manage Channels&lt;/code&gt; 권한이 없거나 카테고리 ID가 틀린 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;티켓이 여러 개 중복 생성되는 경우 기존 채널 존재 여부 체크가 없을 때 자주 나옵니다.&lt;/li&gt;
&lt;li&gt;닫기 버튼이 안 먹는 경우 재시작 뒤 persistent view 등록이 빠졌을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 티켓은 채널 생성보다 분류가 더 중요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티켓 기능 자체는 금방 만들 수 있습니다.&lt;br /&gt;&lt;br /&gt;문제는 문의 유형이 섞이기 시작했을 때입니다.&lt;br /&gt;&lt;br /&gt;결제 문의, 서버 문의, 신고 문의가 같은 카테고리에 몰리면 운영 효율이 떨어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실제 운영 단계에서는 카테고리 설계와 문의 동선을 같이 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 로그를 남길지 미리 정한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널을 바로 삭제하면 깔끔하지만 기록이 남지 않습니다.&lt;br /&gt;&lt;br /&gt;반대로 전부 보관하면 카테고리가 금방 무거워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 &lt;code&gt;닫기 -&amp;gt; 로그 채널에 요약 남기기 -&amp;gt; 채널 삭제&lt;/code&gt; 정도 기준을 잡아 두면 무난합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영 기능이라 배포가 사실상 필수다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티켓 봇은 운영 시간 밖에도 문의가 들어옵니다.&lt;br /&gt;&lt;br /&gt;노트북을 끄면 문의 버튼이 바로 죽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서버에 붙여 둘 생각이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 계속 켜져 있는 환경이 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 티켓 생성 다음에는 분류와 입구 문구가 따라와야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티켓 생성 자체가 됐다면 다음 단계는 채널을 더 많이 여는 게 아니라, 안내 문구와 권한 구조를 더 선명하게 세우는 일입니다.&lt;br /&gt;&lt;br /&gt;입구 문구를 다시 다듬을 때는 &lt;a href=&quot;https://blog.dishost.kr/106&quot;&gt;디스코드 공지 채널 운영법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/57&quot;&gt;디스코드 채널 권한 설정법, 공지 채널과 관리자 채널이 꼬이지 않게 만드는 법&lt;/a&gt;을 먼저 확인하면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/120</guid>
      <comments>https://dishost.tistory.com/120#entry120comment</comments>
      <pubDate>Sat, 25 Apr 2026 16:52:02 +0900</pubDate>
    </item>
    <item>
      <title>온보딩 체크리스트 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/119</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;새 멤버가 규칙을 읽었는지, 자기소개를 했는지, 질문 채널까지 갔는지를 운영자가 일일이 확인하는 순간 온보딩이 이미 무거워진 겁니다.&lt;br /&gt;&lt;br /&gt;온보딩 체크리스트 봇의 역할은 입구 채널을 대신하는 게 아니라, 새 멤버가 해야 할 첫 행동을 한 장짜리 진행표로 보여 주는 데 있습니다.&lt;br /&gt;&lt;br /&gt;이 글은 입구 안내를 인터랙티브 완료 흐름으로 바꾸는 자동화 글입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 입구 설명문을 진행표로 바꾸고 싶을 때 쓴다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 &lt;code&gt;/온보딩&lt;/code&gt; 명령어를 실행하면 개인용 체크리스트 임베드와 버튼이 뜨고, 사용자가 버튼을 누르며 진행 상태를 확인하는 기본 구조를 만듭니다.&lt;br /&gt;&lt;br /&gt;완전한 데이터 저장까지는 넣지 않고, 작동 원리를 이해하기 쉬운 형태부터 갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입구 채널 구조가 먼저 필요하다면 &lt;a href=&quot;https://blog.dishost.kr/103&quot;&gt;디스코드 서버 온보딩 채널 구성 예시&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/114&quot;&gt;자동 환영 인사 디스코드 봇 만들기&lt;/a&gt;을 먼저 보고 와야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 체크 대상 채널 값부터 정리&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
GUILD_ID=테스트_서버_ID
RULES_CHANNEL_ID=규칙채널_ID
INTRO_CHANNEL_ID=자기소개채널_ID
QUESTION_CHANNEL_ID=질문채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;체크리스트 문구 안에 채널 멘션을 넣으려면 채널 ID를 미리 받아 두면 수월합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 개인 진행표를 갱신하는 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))
RULES_CHANNEL_ID = int(os.getenv(&quot;RULES_CHANNEL_ID&quot;))
INTRO_CHANNEL_ID = int(os.getenv(&quot;INTRO_CHANNEL_ID&quot;))
QUESTION_CHANNEL_ID = int(os.getenv(&quot;QUESTION_CHANNEL_ID&quot;))

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)
user_progress: dict[int, set[str]] = {}


def build_embed(user_id: int) -&amp;gt; discord.Embed:
    progress = user_progress.get(user_id, set())

    def check(key: str) -&amp;gt; str:
        return &quot;완료&quot; if key in progress else &quot;미완료&quot;

    description = &quot;\n&quot;.join([
        f&quot;1. 규칙 확인: {check('rules')}&quot;,
        f&quot;2. 자기소개 작성: {check('intro')}&quot;,
        f&quot;3. 질문 채널 확인: {check('question')}&quot;,
    ])

    embed = discord.Embed(title=&quot;온보딩 체크리스트&quot;, description=description, color=discord.Color.blurple())
    embed.add_field(name=&quot;규칙 채널&quot;, value=f&quot;&amp;lt;#{RULES_CHANNEL_ID}&amp;gt;&quot;, inline=False)
    embed.add_field(name=&quot;자기소개 채널&quot;, value=f&quot;&amp;lt;#{INTRO_CHANNEL_ID}&amp;gt;&quot;, inline=False)
    embed.add_field(name=&quot;질문 채널&quot;, value=f&quot;&amp;lt;#{QUESTION_CHANNEL_ID}&amp;gt;&quot;, inline=False)
    return embed


class OnboardingView(discord.ui.View):
    def __init__(self, user_id: int):
        super().__init__(timeout=300)
        self.user_id = user_id

    async def toggle(self, interaction: discord.Interaction, key: str):
        if interaction.user.id != self.user_id:
            await interaction.response.send_message(&quot;본인 체크리스트만 수정할 수 있습니다.&quot;, ephemeral=True)
            return

        progress = user_progress.setdefault(self.user_id, set())
        if key in progress:
            progress.remove(key)
        else:
            progress.add(key)

        await interaction.response.edit_message(embed=build_embed(self.user_id), view=self)

    @discord.ui.button(label=&quot;규칙 확인&quot;, style=discord.ButtonStyle.secondary)
    async def rules(self, interaction: discord.Interaction, button: discord.ui.Button):
        await self.toggle(interaction, &quot;rules&quot;)

    @discord.ui.button(label=&quot;자기소개 작성&quot;, style=discord.ButtonStyle.secondary)
    async def intro(self, interaction: discord.Interaction, button: discord.ui.Button):
        await self.toggle(interaction, &quot;intro&quot;)

    @discord.ui.button(label=&quot;질문 채널 확인&quot;, style=discord.ButtonStyle.secondary)
    async def question(self, interaction: discord.Interaction, button: discord.ui.Button):
        await self.toggle(interaction, &quot;question&quot;)


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;온보딩&quot;, description=&quot;개인용 온보딩 체크리스트를 엽니다.&quot;, guild=guild)
async def onboarding(interaction: discord.Interaction):
    user_progress.setdefault(interaction.user.id, set())
    await interaction.response.send_message(
        embed=build_embed(interaction.user.id),
        view=OnboardingView(interaction.user.id),
        ephemeral=True,
    )


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 코드는 체크 상태를 메모리에만 저장합니다.&lt;br /&gt;재시작하면 초기화되지만, 입구 자동화 흐름을 이해하기에는 충분합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 버튼 갱신과 개인 제한 먼저 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;실행 후 `/온보딩`을 입력합니다.&lt;br /&gt;체크리스트가 본인에게만 보이는 ephemeral 메시지로 뜨고, 버튼을 누를 때마다 상태가 바뀌면 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. ephemeral 상태와 저장 범위에서 막히는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버튼을 남이 눌러 버리는 경우 유저 ID 확인 분기를 안 넣은 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;채널 멘션이 이상하게 보이는 경우 채널 ID를 잘못 넣었을 가능성이 큽니다.&lt;/li&gt;
&lt;li&gt;재시작 후 체크 상태가 사라지는 경우 지금 구조는 메모리 저장이라 정상입니다.&lt;br /&gt;&lt;br /&gt;운영 단계에서는 파일이나 DB 저장을 붙여야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 체크리스트는 세 칸 안팎이 가장 읽히기 쉽다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목을 너무 많이 넣으면 체크리스트도 안 읽힙니다.&lt;br /&gt;&lt;br /&gt;입구에서 반드시 필요한 행동만 남겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 아래 정도면 충분합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;규칙 읽기&lt;/li&gt;
&lt;li&gt;자기소개 작성&lt;/li&gt;
&lt;li&gt;질문 채널 또는 시작 채널 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 인증 흐름과 같이 붙이면 더 자연스럽다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 동의, 인증 역할, 체크리스트가 따로 놀면 유저가 무엇부터 해야 할지 헷갈립니다.&lt;br /&gt;&lt;br /&gt;규칙 동의가 끝난 뒤 &lt;code&gt;/온보딩&lt;/code&gt;을 안내하는 식으로 붙이면 입구 흐름이 부드럽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 구조를 먼저 정리하려면 &lt;a href=&quot;https://blog.dishost.kr/117&quot;&gt;규칙 동의 인증 디스코드 봇 만들기&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/118&quot;&gt;인증 디스코드 봇 만들기&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 운영 기준도 미리 정해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크리스트가 있다고 해서 유저가 실제로 다 읽는 것은 아닙니다.&lt;br /&gt;&lt;br /&gt;체크 항목은 &lt;code&gt;확인 여부&lt;/code&gt;를 보여 주는 참고 장치로 보고, 진짜 입구 구조는 채널 문구와 권한에서 잡아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 체크리스트를 만들더라도 &lt;a href=&quot;https://blog.dishost.kr/105&quot;&gt;디스코드 서버 규칙 채널 작성법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/106&quot;&gt;디스코드 공지 채널 운영법&lt;/a&gt;을 같이 손봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 상시 운영이라면 배포도 같이 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온보딩은 새 멤버가 들어올 때마다 바로 써야 하는 기능입니다.&lt;br /&gt;&lt;br /&gt;개발용 PC에서만 돌리면 입구 자동화가 쉽게 끊깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서버에서 계속 돌릴 생각이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 상시 실행 환경을 같이 준비해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 체크리스트는 입구를 보조할 뿐, 입구를 대체하지는 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크리스트가 돌아가기 시작하면 다음에는 입구 문구와 채널 배치를 더 짧고 분명하게 다듬을 차례입니다.&lt;br /&gt;&lt;br /&gt;입구 구조 자체를 더 읽히게 만들고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/103&quot;&gt;디스코드 서버 온보딩 채널 구성 예시&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/105&quot;&gt;디스코드 서버 규칙 채널 작성법&lt;/a&gt;을 먼저 다듬어야 합니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/119</guid>
      <comments>https://dishost.tistory.com/119#entry119comment</comments>
      <pubDate>Fri, 24 Apr 2026 16:51:37 +0900</pubDate>
    </item>
    <item>
      <title>인증 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/118</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;버튼 한 번으로 끝나는 인증은 편하지만, 공개 서버에서는 너무 가볍게 느껴질 때가 있습니다.&lt;br /&gt;&lt;br /&gt;새 멤버가 안내를 읽었는지 한 번 더 확인하고 싶거나, 봇 자동 가입 스팸을 조금이라도 걸러 내고 싶을 때가 바로 그렇습니다.&lt;br /&gt;&lt;br /&gt;이 글에서는 DM으로 인증 코드를 보내고, 유저가 서버로 돌아와 코드를 입력하면 역할을 여는 가장 작은 운영형 구조를 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 버튼 인증보다 한 단계 더 확인하고 싶을 때 쓴다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 같은 상황에서 잘 맞습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입구 안내를 한 번 더 읽게 하고 싶은 경우&lt;/li&gt;
&lt;li&gt;봇 자동 가입 스팸을 조금 줄이고 싶은 경우&lt;/li&gt;
&lt;li&gt;인증 버튼보다 한 단계 더 적극적인 행동을 요구하고 싶은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전한 보안 인증은 아니지만, 공개 서버 입구를 한 번 더 정리하는 용도로는 충분합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 코드 인증 전에 정할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬래시 명령어 구조와 역할 구조는 먼저 맞춰 둡니다.&lt;br /&gt;&lt;br /&gt;아직 이 단계가 없다면 &lt;a href=&quot;https://blog.dishost.kr/61&quot;&gt;슬래시 명령어 디스코드 봇 만드는 법, 파이썬 discord.py app_commands 입문&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/117&quot;&gt;규칙 동의 인증 디스코드 봇 만들기&lt;/a&gt;을 먼저 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 코드 인증에 필요한 값 넣기&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
GUILD_ID=테스트_서버_ID
VERIFY_ROLE_ID=여기에_인증역할_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;테스트 단계에서는 서버 단위 sync가 빠릅니다.&lt;br /&gt;글로벌 명령어로 올리면 반영 속도 때문에 디버깅이 느려집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 인증 코드 발급과 확인 흐름&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os
import random
import string

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))
VERIFY_ROLE_ID = int(os.getenv(&quot;VERIFY_ROLE_ID&quot;))

intents = discord.Intents.default()
intents.members = True
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)
verification_codes: dict[int, str] = {}


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)


@tree.command(name=&quot;인증코드발급&quot;, description=&quot;DM으로 인증 코드를 발급합니다.&quot;, guild=guild)
async def issue_code(interaction: discord.Interaction):
    code = &quot;&quot;.join(random.choices(string.digits, k=6))
    verification_codes[interaction.user.id] = code

    try:
        await interaction.user.send(f&quot;인증 코드: {code}\n서버로 돌아가 /인증 명령어에 입력해 주세요.&quot;)
        await interaction.response.send_message(&quot;DM으로 인증 코드를 보냈습니다.&quot;, ephemeral=True)
    except discord.Forbidden:
        await interaction.response.send_message(&quot;DM을 보낼 수 없습니다. 개인 메시지 허용 설정을 먼저 확인해 주세요.&quot;, ephemeral=True)


@tree.command(name=&quot;인증&quot;, description=&quot;발급받은 인증 코드를 입력합니다.&quot;, guild=guild)
@app_commands.describe(code=&quot;DM으로 받은 6자리 코드&quot;)
async def verify(interaction: discord.Interaction, code: str):
    expected_code = verification_codes.get(interaction.user.id)

    if expected_code is None:
        await interaction.response.send_message(&quot;먼저 /인증코드발급 명령어를 실행해 주세요.&quot;, ephemeral=True)
        return

    if code != expected_code:
        await interaction.response.send_message(&quot;인증 코드가 일치하지 않습니다.&quot;, ephemeral=True)
        return

    if interaction.guild is None:
        await interaction.response.send_message(&quot;서버 안에서만 사용할 수 있습니다.&quot;, ephemeral=True)
        return

    member = interaction.guild.get_member(interaction.user.id)
    role = interaction.guild.get_role(VERIFY_ROLE_ID)

    if member is None or role is None:
        await interaction.response.send_message(&quot;멤버 또는 인증 역할을 찾지 못했습니다.&quot;, ephemeral=True)
        return

    try:
        await member.add_roles(role, reason=&quot;인증 코드 확인 완료&quot;)
        verification_codes.pop(interaction.user.id, None)
        await interaction.response.send_message(&quot;인증이 완료되었습니다. 이제 일반 채널을 사용할 수 있습니다.&quot;, ephemeral=True)
    except discord.Forbidden:
        await interaction.response.send_message(&quot;봇 권한이 부족해 역할을 지급하지 못했습니다.&quot;, ephemeral=True)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;구조 자체는 단순합니다.&lt;br /&gt;DM으로 코드를 보내고, 서버 안에서 코드를 다시 입력해 맞으면 역할을 여는 방식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 재발급부터 만료까지 확인하기&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그다음 아래 순서로 확인합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;/인증코드발급&lt;/code&gt; 실행&lt;/li&gt;
&lt;li&gt;DM에서 6자리 코드 확인&lt;/li&gt;
&lt;li&gt;서버 안에서 &lt;code&gt;/인증 code:123456&lt;/code&gt; 실행&lt;/li&gt;
&lt;li&gt;인증 역할이 붙는지 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 코드 저장을 메모리 딕셔너리로 두면 구현이 빠릅니다.&lt;br /&gt;&lt;br /&gt;대신 봇이 재시작되면 기존 코드가 사라집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. DM 인증에서 자주 막히는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DM이 안 오는 경우 유저 개인 설정에서 서버 멤버 DM 차단을 켠 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;코드가 자꾸 틀리는 경우 이전 코드와 새 코드가 섞였을 수 있습니다.&lt;br /&gt;&lt;br /&gt;한 번 더 발급한 뒤 최신 코드만 입력해 봅니다.&lt;/li&gt;
&lt;li&gt;역할 지급이 실패하는 경우 여기서도 결국 역할 순서와 &lt;code&gt;Manage Roles&lt;/code&gt; 권한을 먼저 봐야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 운영 단계에서는 코드 수명과 재발급 기준을 같이 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실전에서는 발급 코드를 오래 살려 두면 안 됩니다.&lt;br /&gt;&lt;br /&gt;예를 들어 10분 뒤 자동 만료되게 하거나, 세 번 틀리면 재발급하게 만드는 식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 많이 막히는 지점은 코드 검증보다 DM 발송 실패와 봇 재시작입니다.&lt;br /&gt;&lt;br /&gt;유저가 DM 차단을 켜 둔 상태인지, 메모리 저장이라 재시작 뒤 코드가 날아가는 구조인지 먼저 설명해 둬야 운영상 훨씬 덜 꼬입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 규칙 동의 인증과 어떻게 다를까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 동의 인증은 버튼 한 번으로 역할을 여는 구조입니다.&lt;br /&gt;&lt;br /&gt;지금 글의 코드는 유저가 DM을 받고, 다시 서버로 돌아와 직접 코드를 입력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입구를 조금 더 단단하게 잡고 싶을 때는 지금 방식이 낫고, 초반 커뮤니티 서버처럼 진입 장벽을 낮추고 싶을 때는 &lt;a href=&quot;https://blog.dishost.kr/117&quot;&gt;규칙 동의 인증 디스코드 봇 만들기&lt;/a&gt; 쪽이 더 잘 맞습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 배포는 결국 상시 실행 환경이 필요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입구 인증은 새 멤버가 언제 들어올지 모르는 기능입니다.&lt;br /&gt;&lt;br /&gt;개발 PC를 끄면 인증 흐름도 같이 멈춥니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영용으로 옮길 때는 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 계속 켜져 있는 서버가 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 인증 강도를 높이려면 입구 구조도 같이 바꾼다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 구조를 붙였다면 다음에는 새 멤버가 서버 안에서 해야 할 행동을 더 짧고 명확하게 안내해야 합니다.&lt;br /&gt;&lt;br /&gt;입구 동선을 이어서 정리할 때는 &lt;a href=&quot;https://blog.dishost.kr/117&quot;&gt;규칙 동의 인증 디스코드 봇 만들기&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/119&quot;&gt;온보딩 체크리스트 디스코드 봇 만들기&lt;/a&gt;을 순서대로 보면 인증과 안내를 어떻게 나눌지 감이 빨리 잡힙니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/118</guid>
      <comments>https://dishost.tistory.com/118#entry118comment</comments>
      <pubDate>Thu, 23 Apr 2026 16:51:12 +0900</pubDate>
    </item>
    <item>
      <title>규칙 동의 인증 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/117</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 채널을 읽게만 두고 끝내면 실제로 동의했는지 확인하기 어렵습니다.&lt;br /&gt;&lt;br /&gt;서버 규모가 조금만 커져도 규칙 확인 여부와 채널 접근 권한을 분리할 필요가 생깁니다.&lt;br /&gt;&lt;br /&gt;이럴 때 가장 많이 쓰는 방식이 버튼을 눌러 규칙 동의를 받고 인증 역할을 여는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 복잡한 데이터베이스 없이 버튼 한 번으로 인증 역할을 붙이는 구조부터 만드는 게 안정적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 어떤 구조로 동작하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 흐름은 단순합니다.&lt;br /&gt;&lt;br /&gt;규칙 채널에 안내 메시지를 올리고, 유저가 &lt;code&gt;동의합니다&lt;/code&gt; 버튼을 누르면 인증 역할을 붙입니다.&lt;br /&gt;&lt;br /&gt;인증 역할이 붙은 뒤에만 일반 채널이 보이도록 권한을 나눕니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 서버 입구 정리에 먼저 도움이 됩니다.&lt;br /&gt;&lt;br /&gt;규칙 채널은 &lt;a href=&quot;https://blog.dishost.kr/105&quot;&gt;디스코드 서버 규칙 채널 작성법&lt;/a&gt;, 역할 계층은 &lt;a href=&quot;https://blog.dishost.kr/58&quot;&gt;디스코드 역할 설정 완벽 가이드, 관리자 운영진 멤버 봇 역할을 처음부터 정리하는 법&lt;/a&gt;에서 먼저 잡아 두면 흐름이 매끈합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 준비할 역할과 채널&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 정도를 먼저 만들어 둡니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;역할
- 미인증
- 인증완료

채널
- 규칙
- 공지
- 일반채팅&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;권한은 보통 아래처럼 나눕니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;미인증
- 규칙 채널 보기 허용
- 공지 채널 보기 허용
- 일반채팅 보기 차단

인증완료
- 규칙 채널 보기 허용
- 공지 채널 보기 허용
- 일반채팅 보기 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;미인증 역할을 기본 지급으로 두고, 버튼 인증 뒤 `인증완료` 역할을 붙이는 방식이 가장 흔합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 인증 채널과 역할 ID 넣기&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
VERIFY_ROLE_ID=여기에_인증완료_역할_ID
RULES_CHANNEL_ID=여기에_규칙채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;버튼 클릭만 처리할 때도 역할 ID는 이름보다 ID로 둬야 안정적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 버튼 인증 흐름을 붙이는 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
VERIFY_ROLE_ID = int(os.getenv(&quot;VERIFY_ROLE_ID&quot;))
RULES_CHANNEL_ID = int(os.getenv(&quot;RULES_CHANNEL_ID&quot;))

intents = discord.Intents.default()
intents.members = True
client = discord.Client(intents=intents)


class VerifyView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=None)

    @discord.ui.button(label=&quot;동의합니다&quot;, style=discord.ButtonStyle.success, custom_id=&quot;verify:agree&quot;)
    async def verify_button(self, interaction: discord.Interaction, button: discord.ui.Button):
        if interaction.guild is None:
            await interaction.response.send_message(&quot;서버 안에서만 사용할 수 있습니다.&quot;, ephemeral=True)
            return

        role = interaction.guild.get_role(VERIFY_ROLE_ID)
        if role is None:
            await interaction.response.send_message(&quot;인증 역할을 찾지 못했습니다.&quot;, ephemeral=True)
            return

        member = interaction.user
        if not isinstance(member, discord.Member):
            await interaction.response.send_message(&quot;멤버 정보를 읽지 못했습니다.&quot;, ephemeral=True)
            return

        if role in member.roles:
            await interaction.response.send_message(&quot;이미 인증이 완료된 상태입니다.&quot;, ephemeral=True)
            return

        try:
            await member.add_roles(role, reason=&quot;규칙 동의 인증 완료&quot;)
            await interaction.response.send_message(&quot;규칙 동의가 확인되었습니다. 이제 일반 채널을 볼 수 있습니다.&quot;, ephemeral=True)
        except discord.Forbidden:
            await interaction.response.send_message(&quot;봇 권한이 부족해 역할을 지급하지 못했습니다.&quot;, ephemeral=True)


@client.event
async def on_ready():
    client.add_view(VerifyView())
    print(f&quot;로그인 성공: {client.user}&quot;)


async def send_verify_message(channel: discord.TextChannel):
    embed = discord.Embed(
        title=&quot;서버 이용 전 확인&quot;,
        description=(
            &quot;규칙을 읽은 뒤 아래 버튼을 눌러 인증을 완료해 주세요.\n&quot;
            &quot;인증이 끝나야 일반 채널이 열립니다.&quot;
        ),
        color=discord.Color.green(),
    )
    await channel.send(embed=embed, view=VerifyView())


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;버튼이 재시작 뒤에도 동작하게 하려면 `custom_id`가 고정된 persistent view 구조를 써야 합니다.&lt;br /&gt;위 코드는 봇이 켜진 상태에서 버튼 클릭을 처리하는 기본형입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 인증 메시지 올리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 한 번은 규칙 채널에 안내 메시지를 보내야 합니다.&lt;br /&gt;&lt;br /&gt;임시로 아래 코드를 &lt;code&gt;on_ready()&lt;/code&gt; 안에서 한 번만 호출해도 되고, 직접 명령어를 붙여도 됩니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;channel = client.get_channel(RULES_CHANNEL_ID)
if isinstance(channel, discord.TextChannel):
    await send_verify_message(channel)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;메시지가 여러 번 중복으로 올라가지 않게, 실제 운영 전에는 한 번만 실행하고 다시 빼야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 입구에서 자주 끊기는 지점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버튼은 눌리는데 역할이 안 붙는 경우 봇 역할이 &lt;code&gt;인증완료&lt;/code&gt; 역할보다 아래에 있거나 &lt;code&gt;Manage Roles&lt;/code&gt; 권한이 없는 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;버튼이 회색으로 죽는 경우 재시작 후 persistent view 등록이 안 된 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;custom_id&lt;/code&gt;와 &lt;code&gt;client.add_view()&lt;/code&gt;를 다시 봅니다.&lt;/li&gt;
&lt;li&gt;이미 인증한 유저가 계속 눌리는 경우 중복 지급 자체는 문제 없지만, 안내 문구를 따로 분기해 둬야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 규칙 동의와 실제 인증은 분리해서 생각한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 엄밀한 본인 확인이 아니라 서버 규칙 동의 확인에 가깝습니다.&lt;br /&gt;&lt;br /&gt;이메일 인증이나 외부 계정 연동 같은 강한 인증이 필요한 경우와는 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 초반 커뮤니티 서버에는 충분하지만, 더 강한 인증이 필요하면 다음 단계 구조를 따로 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 입구 동선과 함께 써야 효과가 난다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 동의 버튼만 만들어 두고 나머지 채널 구조가 흐릿하면 체감 효과가 약합니다.&lt;br /&gt;&lt;br /&gt;규칙 읽기, 공지 확인, 자기소개 또는 질문 채널 진입이 자연스럽게 이어져야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입구 동선을 다시 정리하려면 &lt;a href=&quot;https://blog.dishost.kr/103&quot;&gt;디스코드 서버 온보딩 채널 구성 예시&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/114&quot;&gt;자동 환영 인사 디스코드 봇 만들기&lt;/a&gt;을 묶어서 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 24시간 켜져 있어야 의미가 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 버튼은 새 멤버가 들어올 때마다 언제든 눌릴 수 있습니다.&lt;br /&gt;&lt;br /&gt;운영용 서버라면 결국 상시 실행 환경이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포까지 같이 올릴 계획이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 별도 서버에서 돌리는 쪽이 현실적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 인증 뒤에 열릴 채널 구조를 같이 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 동의 기반 입구 인증까지 만들었다면 다음에는 코드 입력이나 추가 확인 절차가 있는 일반 인증 봇으로 확장할 수 있습니다.&lt;br /&gt;&lt;br /&gt;지금 단계에서 권한 문제를 먼저 확인하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/100&quot;&gt;디스코드 역할이 지급되지 않을 때 해결법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/98&quot;&gt;Missing Permissions(50013) 오류 해결법&lt;/a&gt;을 같이 점검하면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/117</guid>
      <comments>https://dishost.tistory.com/117#entry117comment</comments>
      <pubDate>Wed, 22 Apr 2026 16:50:46 +0900</pubDate>
    </item>
    <item>
      <title>반응 역할 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/116</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;반응 역할은 유저가 스스로 역할을 고를 수 있게 만드는 가장 흔한 구조입니다.&lt;br /&gt;&lt;br /&gt;게임 선택, 지역 선택, 알림 구독 역할처럼 운영에 바로 쓰기 좋습니다.&lt;br /&gt;&lt;br /&gt;수동으로 역할을 나눠 주는 것보다 훨씬 덜 번거롭고, 유저도 즉시 반응을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 이모지 하나에 역할 하나를 붙이는 단순한 구조부터 만드는 게 가장 안정적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 먼저 준비할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 봇 구조와 역할 설계는 이미 끝나 있어야 합니다.&lt;br /&gt;&lt;br /&gt;역할 이름과 계층이 정리되지 않은 상태에서 반응 역할부터 만들면 나중에 엉키기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 구조가 아직 애매하다면 &lt;a href=&quot;https://blog.dishost.kr/58&quot;&gt;디스코드 역할 설정 완벽 가이드, 관리자 운영진 멤버 봇 역할을 처음부터 정리하는 법&lt;/a&gt;을 먼저 봐야 합니다.&lt;br /&gt;&lt;br /&gt;자동 지급 구조와 함께 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/115&quot;&gt;자동 역할 부여 디스코드 봇 만들기&lt;/a&gt;도 바로 이어집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 반응 역할 메시지를 먼저 고정한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇이 아무 메시지에나 반응을 감지하게 두면 관리가 어려워집니다.&lt;br /&gt;&lt;br /&gt;역할 선택 전용 메시지 하나를 정해 두고, 그 메시지에 붙는 반응만 처리해야 덜 꼬입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 메시지는 아래 정도면 충분합니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;원하는 역할에 반응을 눌러 주세요.
  게임 알림
  공지 알림
  개발 질문&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 메시지를 올린 뒤 메시지 ID를 복사해 코드에 넣습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 역할 메시지와 ID 먼저 넣기&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
REACTION_MESSAGE_ID=여기에_역할선택_메시지_ID
GAME_ROLE_ID=여기에_게임역할_ID
NOTICE_ROLE_ID=여기에_공지역할_ID
DEV_ROLE_ID=여기에_개발역할_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;메시지 ID와 역할 ID는 개발자 모드에서 복사합니다.&lt;br /&gt;이모지와 역할 매핑은 코드 딕셔너리로 두면 읽기 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 반응 추가와 제거를 같이 묶는 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
REACTION_MESSAGE_ID = int(os.getenv(&quot;REACTION_MESSAGE_ID&quot;))

ROLE_MAP = {
    &quot; &quot;: int(os.getenv(&quot;GAME_ROLE_ID&quot;)),
    &quot; &quot;: int(os.getenv(&quot;NOTICE_ROLE_ID&quot;)),
    &quot; &quot;: int(os.getenv(&quot;DEV_ROLE_ID&quot;)),
}

intents = discord.Intents.default()
intents.guilds = True
intents.members = True
intents.reactions = True
client = discord.Client(intents=intents)


async def update_role(payload: discord.RawReactionActionEvent, should_add: bool):
    if payload.message_id != REACTION_MESSAGE_ID:
        return

    if payload.guild_id is None or payload.user_id == client.user.id:
        return

    emoji = str(payload.emoji)
    role_id = ROLE_MAP.get(emoji)

    if role_id is None:
        return

    guild = client.get_guild(payload.guild_id)
    if guild is None:
        return

    member = guild.get_member(payload.user_id)
    role = guild.get_role(role_id)

    if member is None or role is None:
        return

    try:
        if should_add:
            await member.add_roles(role, reason=&quot;반응 역할 지급&quot;)
            print(f&quot;역할 지급: {member} -&amp;gt; {role.name}&quot;)
        else:
            await member.remove_roles(role, reason=&quot;반응 역할 해제&quot;)
            print(f&quot;역할 해제: {member} -&amp;gt; {role.name}&quot;)
    except discord.Forbidden:
        print(&quot;권한 부족으로 역할 변경에 실패했습니다.&quot;)


@client.event
async def on_ready():
    print(f&quot;로그인 성공: {client.user}&quot;)


@client.event
async def on_raw_reaction_add(payload: discord.RawReactionActionEvent):
    await update_role(payload, should_add=True)


@client.event
async def on_raw_reaction_remove(payload: discord.RawReactionActionEvent):
    await update_role(payload, should_add=False)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`raw` 이벤트를 쓰는 이유는 메시지가 캐시에 없더라도 반응 변화를 받을 수 있기 때문입니다.&lt;br /&gt;초반에는 `on_reaction_add`로 시작하기 쉽지만, 실제 운영에서는 `raw` 쪽이 덜 불안합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 역할 선택 메시지 올리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 아무 채널에 안내 메시지를 올립니다.&lt;br /&gt;&lt;br /&gt;그다음 아래 순서로 준비합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지 ID 복사&lt;/li&gt;
&lt;li&gt;사용할 역할 ID 복사&lt;/li&gt;
&lt;li&gt;안내 이모지 추가&lt;/li&gt;
&lt;li&gt;봇 역할 순서 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇 역할이 대상 역할보다 낮으면 여기서도 똑같이 실패합니다.&lt;br /&gt;&lt;br /&gt;이 부분은 자동 역할 글과 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 반응 한 번으로 왕복 확인하기&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;실행 후 역할 선택 메시지에 반응을 달아 봅니다.&lt;br /&gt;반응을 누르면 역할이 붙고, 반응을 지우면 역할이 빠져야 정상입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 초반에 가장 자주 틀리는 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반응은 달리는데 역할이 안 붙는 경우 대부분 역할 순서 문제입니다.&lt;/li&gt;
&lt;li&gt;특정 이모지만 동작하지 않는 경우 코드의 이모지 문자와 실제 메시지 반응 이모지가 정확히 같은지 봅니다.&lt;br /&gt;&lt;br /&gt;커스텀 이모지는 문자열 형태가 달라서 처음에는 일반 이모지로 시작해야 합니다.&lt;/li&gt;
&lt;li&gt;메시지를 다시 올렸더니 안 되는 경우 새 메시지 ID를 &lt;code&gt;.env&lt;/code&gt;에 반영하지 않은 경우가 많습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 초반에는 역할 수를 적게 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 종류가 너무 많으면 선택 메시지 자체가 지저분해집니다.&lt;br /&gt;&lt;br /&gt;처음에는 3개 안팎으로 시작하면 보기 좋고 관리도 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 구독, 관심 분야, 지역처럼 실제로 자주 쓰는 역할만 남겨야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 반응 역할은 입구보다 중간 구간에 더 잘 맞는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 막 들어온 사람에게 역할을 너무 많이 고르게 하면 피로도가 생깁니다.&lt;br /&gt;&lt;br /&gt;기본 역할은 자동 지급하고, 선택형 역할은 적응이 끝난 뒤 고르게 두면 흐름이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입구 구조는 &lt;a href=&quot;https://blog.dishost.kr/114&quot;&gt;자동 환영 인사 디스코드 봇 만들기&lt;/a&gt;, 기본 권한은 &lt;a href=&quot;https://blog.dishost.kr/115&quot;&gt;자동 역할 부여 디스코드 봇 만들기&lt;/a&gt;, 선택형 분기는 지금 글을 함께 보면 역할 흐름이 정리됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 역할 선택은 입구보다 적응 구간에서 빛난다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반응 역할까지 만들었다면 다음에는 규칙 동의 여부에 따라 접근 권한을 여는 인증 구조로 넘어갈 수 있습니다.&lt;br /&gt;&lt;br /&gt;지금 단계에서 권한 오류를 먼저 점검하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/55&quot;&gt;디스코드 봇 권한 설정법, 50013과 50001을 막는 기본 구조 정리&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/100&quot;&gt;디스코드 역할이 지급되지 않을 때 해결법&lt;/a&gt;을 같이 읽어 두면 원인 구분이 빨라집니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/116</guid>
      <comments>https://dishost.tistory.com/116#entry116comment</comments>
      <pubDate>Tue, 21 Apr 2026 16:50:19 +0900</pubDate>
    </item>
    <item>
      <title>자동 역할 부여 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/115</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;새 멤버가 들어올 때 기본 역할을 자동으로 붙여 두면 운영이 훨씬 편해집니다.&lt;br /&gt;&lt;br /&gt;공지 읽기 전용 역할, 기본 멤버 역할, 신규 유저 역할처럼 초반에 자주 쓰는 구조가 있습니다.&lt;br /&gt;&lt;br /&gt;수동으로 역할을 주기 시작하면 인원이 조금만 늘어도 바로 번거로워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 역할 부여는 간단해 보이지만 실제로는 역할 순서와 권한 계층에서 자주 막힙니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 먼저 확인할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 역할 구조와 봇 권한 구조는 이미 정리돼 있어야 합니다.&lt;br /&gt;&lt;br /&gt;역할 이름만 만들고 순서를 안 맞추면 코드가 맞아도 실패합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 이 부분이 애매하다면 &lt;a href=&quot;https://blog.dishost.kr/58&quot;&gt;디스코드 역할 설정 완벽 가이드, 관리자 운영진 멤버 봇 역할을 처음부터 정리하는 법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/55&quot;&gt;디스코드 봇 권한 설정법, 50013과 50001을 막는 기본 구조 정리&lt;/a&gt;을 먼저 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Members Intent와 Manage Roles 권한을 맞춘다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입장 이벤트를 받으려면 &lt;code&gt;SERVER MEMBERS INTENT&lt;/code&gt;가 필요합니다.&lt;br /&gt;&lt;br /&gt;역할을 붙이려면 봇 역할에 &lt;code&gt;Manage Roles&lt;/code&gt; 권한도 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 중 하나라도 빠지면 아래 둘 중 하나로 끝나는 경우가 많습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트가 아예 안 돈다&lt;/li&gt;
&lt;li&gt;이벤트는 도는데 역할 지급만 실패한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 역할 순서가 더 중요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드에서는 봇 역할이 대상 역할보다 위에 있어야 역할을 줄 수 있습니다.&lt;br /&gt;&lt;br /&gt;이 부분을 놓치면 &lt;code&gt;Missing Permissions(50013)&lt;/code&gt;이 바로 뜹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 아래처럼 둡니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;운영진
봇
기본멤버
신규유저
@everyone&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;봇 역할보다 위에 있는 역할은 봇이 지급할 수 없습니다.&lt;br /&gt;이건 코드로 우회되지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 실행 전 변수부터 묶는다&lt;/h3&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
AUTO_ROLE_ID=여기에_자동지급_역할_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;역할 이름으로 찾는 방식도 가능하지만, 이름이 바뀌면 바로 깨집니다.&lt;br /&gt;처음부터 역할 ID를 써야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 입장 직후 역할을 붙이는 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
AUTO_ROLE_ID = int(os.getenv(&quot;AUTO_ROLE_ID&quot;))

intents = discord.Intents.default()
intents.members = True
client = discord.Client(intents=intents)


@client.event
async def on_ready():
    print(f&quot;로그인 성공: {client.user}&quot;)


@client.event
async def on_member_join(member: discord.Member):
    role = member.guild.get_role(AUTO_ROLE_ID)

    if role is None:
        print(&quot;자동 지급 역할을 찾지 못했습니다.&quot;)
        return

    try:
        await member.add_roles(role, reason=&quot;신규 멤버 자동 역할 지급&quot;)
        print(f&quot;역할 지급 완료: {member} -&amp;gt; {role.name}&quot;)
    except discord.Forbidden:
        print(&quot;권한 부족으로 역할 지급에 실패했습니다.&quot;)
    except discord.HTTPException as error:
        print(f&quot;역할 지급 중 HTTP 오류: {error}&quot;)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;핵심은 `member.add_roles()` 한 줄이지만, 실제 성공 여부는 Portal 설정과 역할 계층에서 갈립니다.&lt;br /&gt;그래서 코드보다 서버 안 역할 순서를 먼저 봐야 빠릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 역할 지급 흐름 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행은 아래처럼 합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그다음 테스트 계정으로 서버에 다시 들어와 역할이 붙는지 확인합니다.&lt;br /&gt;바로 안 붙으면 아래 순서로 보면 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;봇 역할에 &lt;code&gt;Manage Roles&lt;/code&gt;가 있는가&lt;/li&gt;
&lt;li&gt;봇 역할이 대상 역할보다 위에 있는가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AUTO_ROLE_ID&lt;/code&gt;가 맞는가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SERVER MEMBERS INTENT&lt;/code&gt;를 켰는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 실제로 자주 엉키는 지점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Missing Permissions(50013)&lt;/code&gt;가 뜨는 경우 대부분 봇 역할 순서가 낮습니다.&lt;/li&gt;
&lt;li&gt;역할 ID를 못 찾는 경우 개발자 모드를 켜지 않았거나 다른 서버 역할 ID를 넣은 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;특정 유저에게만 실패하는 경우 해당 유저가 이미 같은 역할을 갖고 있거나, 다른 인증 절차와 충돌할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;권한은 있는데 왜 안 되지&lt;/code&gt;에서 가장 많이 막히는 지점이 역할 순서입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 자동 역할은 한 개부터 시작해야 덜 꼬인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 입장 역할, 지역 역할, 게임 역할까지 한 번에 붙이면 관리가 복잡해집니다.&lt;br /&gt;&lt;br /&gt;초반에는 &lt;code&gt;신규유저&lt;/code&gt;나 &lt;code&gt;기본멤버&lt;/code&gt;처럼 한 개만 자동 지급하면 무난합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 선택형 역할은 반응 역할이나 버튼 역할로 분리해야 운영에 맞습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 환영 메시지와 같이 쓰면 흐름이 좋아진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입장 직후 환영 메시지와 기본 역할 지급이 같이 되면 온보딩이 훨씬 부드러워집니다.&lt;br /&gt;&lt;br /&gt;새 유저 입장에서는 규칙 채널이 보이고, 기본 안내가 보이고, 권한도 바로 맞춰지는 흐름이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.dishost.kr/114&quot;&gt;자동 환영 인사 디스코드 봇 만들기&lt;/a&gt;와 묶어 두면 입구 자동화가 거의 한 세트로 정리됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 자동 역할 다음에는 입구 문구를 같이 손본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 역할 자동 지급까지 끝났다면 이제 유저가 직접 역할을 고르는 구조로 넘어갈 수 있습니다.&lt;br /&gt;&lt;br /&gt;역할 체계를 먼저 다시 다듬고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/100&quot;&gt;디스코드 역할이 지급되지 않을 때 해결법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/98&quot;&gt;Missing Permissions(50013) 오류 해결법&lt;/a&gt;을 함께 점검해 둬야 안전합니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/115</guid>
      <comments>https://dishost.tistory.com/115#entry115comment</comments>
      <pubDate>Mon, 20 Apr 2026 16:49:54 +0900</pubDate>
    </item>
    <item>
      <title>자동 환영 인사 디스코드 봇 만들기</title>
      <link>https://dishost.tistory.com/114</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;새 멤버가 서버에 들어왔을 때 아무 반응이 없으면 입구가 비어 있는 느낌이 납니다.&lt;br /&gt;&lt;br /&gt;반대로 환영 문구가 너무 길거나 여러 채널로 동시에 날아오면 부담스럽습니다.&lt;br /&gt;&lt;br /&gt;자동 환영 인사 봇은 단순하지만 서버 첫인상을 크게 바꾸는 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 공개 환영 메시지 하나와 간단한 DM 안내 정도만 자동화해 두는 구성이 가장 안정적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 기본 뼈대부터 먼저 맞춘다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 파이썬 봇과 토큰 분리는 이미 끝난 상태를 기준으로 갑니다.&lt;br /&gt;&lt;br /&gt;아직 기본 구조가 없다면 &lt;a href=&quot;https://blog.dishost.kr/53&quot;&gt;파이썬 디스코드 봇 만들기 처음부터 끝까지, discord.py 입문 가이드&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/60&quot;&gt;.env 환경변수로 디스코드 토큰 숨기기, 하드코딩 없이 안전하게 관리하는 법&lt;/a&gt;부터 먼저 맞춰 둬야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입구 채널 구조를 아직 안 정했다면 &lt;a href=&quot;https://blog.dishost.kr/103&quot;&gt;디스코드 서버 온보딩 채널 구성 예시&lt;/a&gt;도 같이 열어 두면 흐름이 잘 맞습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Members Intent를 먼저 켠다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멤버 입장 이벤트를 받으려면 &lt;code&gt;Server Members Intent&lt;/code&gt;가 필요합니다.&lt;br /&gt;&lt;br /&gt;코드만 맞고 Portal 설정이 빠져 있으면 &lt;code&gt;on_member_join&lt;/code&gt;이 아예 실행되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Discord Developer Portal에서 아래 순서로 켭니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Your Application -&amp;gt; Bot -&amp;gt; Privileged Gateway Intents
- SERVER MEMBERS INTENT 활성화
- 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 설정을 놓치면 코드가 조용히 멈추는 것처럼 보여서 처음에 많이 막힙니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 입장 이벤트에 필요한 패키지 준비&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;pip install -U discord.py python-dotenv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이미 기본 봇을 만든 상태라면 설치가 끝나 있을 수 있습니다.&lt;br /&gt;그래도 버전이 애매하면 한 번 더 올려 두면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. &lt;code&gt;.env&lt;/code&gt; 파일 예시&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
WELCOME_CHANNEL_ID=여기에_환영채널_ID
RULES_CHANNEL_ID=여기에_규칙채널_ID
NOTICE_CHANNEL_ID=여기에_공지채널_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;채널 ID는 개발자 모드를 켠 뒤 채널 우클릭으로 복사하면 됩니다.&lt;br /&gt;문구 안에서 채널 멘션을 안정적으로 쓰려면 이름보다 ID가 낫습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 공개 환영과 DM 안내를 같이 보내는 코드&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

import discord
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
WELCOME_CHANNEL_ID = int(os.getenv(&quot;WELCOME_CHANNEL_ID&quot;))
RULES_CHANNEL_ID = int(os.getenv(&quot;RULES_CHANNEL_ID&quot;))
NOTICE_CHANNEL_ID = int(os.getenv(&quot;NOTICE_CHANNEL_ID&quot;))

intents = discord.Intents.default()
intents.members = True

auto_reply_color = discord.Color.blue()
client = discord.Client(intents=intents)


@client.event
async def on_ready():
    print(f&quot;로그인 성공: {client.user}&quot;)


@client.event
async def on_member_join(member: discord.Member):
    welcome_channel = client.get_channel(WELCOME_CHANNEL_ID)
    rules_channel = client.get_channel(RULES_CHANNEL_ID)
    notice_channel = client.get_channel(NOTICE_CHANNEL_ID)

    if welcome_channel is not None:
        embed = discord.Embed(
            title=&quot;새 멤버가 들어왔습니다&quot;,
            description=(
                f&quot;{member.mention} 님, 환영합니다.\n&quot;
                f&quot;먼저 {rules_channel.mention if rules_channel else '#규칙'} 채널을 확인하고,\n&quot;
                f&quot;공지 사항은 {notice_channel.mention if notice_channel else '#공지'} 채널에서 볼 수 있습니다.&quot;
            ),
            color=auto_reply_color,
        )
        embed.set_thumbnail(url=member.display_avatar.url)
        await welcome_channel.send(embed=embed)

    try:
        await member.send(
            &quot;서버에 오신 것을 환영합니다.\n&quot;
            &quot;먼저 규칙과 공지를 확인한 뒤 질문 채널을 이용해 주세요.&quot;
        )
    except discord.Forbidden:
        print(f&quot;DM 전송 실패: {member}&quot;)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;핵심은 `intents.members = True`와 `on_member_join` 이벤트입니다.&lt;br /&gt;입장 직후 공개 채널 메시지와 DM을 같이 보내되, DM 실패는 자주 나오는 문제라 예외 처리를 넣어 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 입장 이벤트부터 순서대로 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 순서로 보면 빠릅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;봇을 서버에 초대했는가&lt;/li&gt;
&lt;li&gt;봇 역할에 환영 채널 보기와 메시지 보내기 권한이 있는가&lt;/li&gt;
&lt;li&gt;Portal에서 &lt;code&gt;SERVER MEMBERS INTENT&lt;/code&gt;를 켰는가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; 채널 ID가 실제 채널과 맞는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행은 아래처럼 합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;테스트할 때는 부계정이나 테스트 서버를 쓰는 쪽이 훨씬 수월합니다.&lt;br /&gt;실서버에서 계속 들락날락하면 기존 멤버 알림이 섞이기 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 환영 흐름에서 자주 끊기는 지점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환영 채널에 아무 메시지도 안 오는 경우 &lt;code&gt;WELCOME_CHANNEL_ID&lt;/code&gt;가 틀렸거나 봇 권한이 부족한 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;on_member_join&lt;/code&gt; 자체가 안 도는 경우 대부분 &lt;code&gt;SERVER MEMBERS INTENT&lt;/code&gt; 설정 누락입니다.&lt;/li&gt;
&lt;li&gt;DM만 실패하는 경우 유저가 서버 멤버 DM 차단을 켠 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;이 경우는 공개 환영 메시지만 보내도 충분합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 문구는 짧게 유지해야 읽힌다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환영 메시지에 규칙, 공지, 자기소개, 이벤트, 질문 채널까지 한 번에 다 넣으면 오히려 안 읽힙니다.&lt;br /&gt;&lt;br /&gt;처음에는 아래 세 가지 정도만 넣으면 충분합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환영 인사&lt;/li&gt;
&lt;li&gt;규칙 채널 안내&lt;/li&gt;
&lt;li&gt;공지 또는 질문 채널 안내&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온보딩 세부 구조는 사람이 읽는 채널 문구에서 정리하고, 봇은 첫 진입 한 번만 도와주면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 24시간 돌릴 계획이라면 서버가 필요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환영 봇은 멤버가 언제 들어올지 모르기 때문에 노트북을 끄면 바로 의미가 없어집니다.&lt;br /&gt;&lt;br /&gt;상시 운영용 기능은 결국 계속 켜져 있는 서버 위에 올려 두어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 단계까지 바로 올릴 생각이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 별도 서버에 두는 쪽이 현실적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 환영 메시지 뒤에는 역할과 입구를 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환영 메시지까지 자동화했다면 다음에는 입장 직후 역할을 자동으로 붙이는 흐름으로 이어갈 수 있습니다.&lt;br /&gt;&lt;br /&gt;지금 단계에서 역할 구조를 먼저 다시 확인하고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/58&quot;&gt;디스코드 역할 설정 완벽 가이드, 관리자 운영진 멤버 봇 역할을 처음부터 정리하는 법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/55&quot;&gt;디스코드 봇 권한 설정법, 50013과 50001을 막는 기본 구조 정리&lt;/a&gt;을 먼저 맞춰 둬야 합니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/114</guid>
      <comments>https://dishost.tistory.com/114#entry114comment</comments>
      <pubDate>Sun, 19 Apr 2026 16:49:28 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 서버 홍보 방법</title>
      <link>https://dishost.tistory.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 서버는 만든 뒤가 더 중요합니다.&lt;br /&gt;&lt;br /&gt;채널과 규칙을 잘 깔아 두어도 처음 들어온 사람이 바로 나가면 홍보가 거의 의미가 없습니다.&lt;br /&gt;&lt;br /&gt;서버 홍보는 링크를 많이 뿌리는 작업이 아니라, 들어온 사람이 왜 남아야 하는지 먼저 정리하는 작업에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유입은 홍보글에서 시작되지만 잔류는 서버 구조에서 결정되는 경우가 많습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 홍보 전에 먼저 점검할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홍보부터 시작하면 초반 유입이 그대로 이탈로 바뀌기 쉽습니다.&lt;br /&gt;&lt;br /&gt;아래 네 가지는 최소한 맞춰 둔 뒤 링크를 열어야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 소개문이 한눈에 이해되는가&lt;/li&gt;
&lt;li&gt;규칙 채널과 공지 채널이 위쪽에서 바로 보이는가&lt;/li&gt;
&lt;li&gt;처음 들어온 사람이 질문할 채널이 보이는가&lt;/li&gt;
&lt;li&gt;초대 링크가 만료되지 않도록 설정했는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소개문 문구가 아직 애매하다면 &lt;a href=&quot;https://blog.dishost.kr/111&quot;&gt;디스코드 서버 소개문 작성법&lt;/a&gt;부터 먼저 손보면 됩니다.&lt;br /&gt;&lt;br /&gt;입구 채널 구조가 흐릿하다면 &lt;a href=&quot;https://blog.dishost.kr/103&quot;&gt;디스코드 서버 온보딩 채널 구성 예시&lt;/a&gt;도 같이 보면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 초대 링크는 용도별로 나눠 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홍보용 링크를 하나만 쓰면 어디서 유입됐는지 파악하기 어렵습니다.&lt;br /&gt;&lt;br /&gt;디스코드는 초대 링크마다 이름과 만료 정책을 따로 둘 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 아래처럼 구분할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;커뮤니티 사이트 홍보용
SNS 프로필 고정용
이벤트 공지용
파트너 서버 교환용
지인 공유용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게 나눠 두면 어느 채널에서 유입이 실제로 들어오는지 보기 쉽습니다.&lt;br /&gt;짧게 끝나는 홍보와 오래 살아남는 홍보를 구분할 때 특히 도움이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 홍보 문구는 분위기보다 용도를 먼저 쓴다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;재미있는 사람들 많은 서버&lt;/code&gt;, &lt;code&gt;함께 성장하는 공간&lt;/code&gt; 같은 문구는 실제 정보가 부족합니다.&lt;br /&gt;&lt;br /&gt;누구를 위한 서버인지와 들어오면 무엇을 할 수 있는지가 먼저 보여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반 홍보 문구는 아래 정도가 무난합니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;디스코드 봇 개발과 서버 운영 정보를 다루는 서버입니다.
파이썬, discord.js, 권한 설정, 에러 해결 질문을 받을 수 있습니다.
처음 들어오면 규칙과 공지를 확인한 뒤 질문 채널을 이용해 주세요.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 문장은 길지 않지만 대상, 주제, 첫 행동이 모두 들어 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 홍보 채널마다 문구를 조금씩 바꾼다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 링크와 같은 문구를 모든 곳에 붙여 넣으면 반응이 약합니다.&lt;br /&gt;&lt;br /&gt;홍보 위치에 따라 강조점이 달라지는 게 자연스럽습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 커뮤니티에서는 질문 가능한 주제를 앞에 둡니다.&lt;/li&gt;
&lt;li&gt;게임 커뮤니티에서는 이벤트나 참여 요소를 앞에 둡니다.&lt;/li&gt;
&lt;li&gt;SNS 프로필에서는 소개문을 두 줄 안으로 줄여야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홍보 위치가 바뀌면 문구도 같이 바뀌어야 클릭 이후 이탈이 줄어듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 이벤트가 없다면 유입이 금방 식는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홍보만 하고 서버 안에서 새 사람이 움직일 이유가 없으면 며칠 안에 조용해집니다.&lt;br /&gt;&lt;br /&gt;질문 채널, 자료 공유, 주간 이벤트 중 하나는 있어야 들어온 사람이 머무를 이유가 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 서버가 아직 작다면 정기 이벤트 공지 한 줄이 생각보다 크게 작동합니다.&lt;br /&gt;&lt;br /&gt;이벤트 구조는 나중에 따로 다듬더라도, 지금 단계에서도 최소한 일정 안내 채널 하나는 준비해 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 파트너 홍보는 교환보다 맥락이 중요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 교환 홍보를 할 때는 숫자만 맞추는 방식보다 주제가 맞는지가 더 중요합니다.&lt;br /&gt;&lt;br /&gt;완전히 다른 성격의 서버에 링크를 올리면 유입은 들어와도 바로 나가는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 기준으로 봐야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 주제가 겹치거나 인접한가&lt;/li&gt;
&lt;li&gt;초보 유저와 고급 유저 비율이 비슷한가&lt;/li&gt;
&lt;li&gt;공지 톤과 규칙 수준이 크게 다르지 않은가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홍보는 클릭 수보다 남는 사람 비율을 기준으로 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 홍보 후에는 온보딩 동선을 바로 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유입이 들어왔는데 &lt;code&gt;어디부터 봐야 하지&lt;/code&gt;에서 멈추면 홍보 효율이 바로 떨어집니다.&lt;br /&gt;&lt;br /&gt;새 유저 입장에서는 공지보다 첫 행동이 더 중요할 때가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목은 실제로 자주 놓칩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입장 직후 읽을 채널이 너무 많지 않은가&lt;/li&gt;
&lt;li&gt;질문 채널 이름이 바로 이해되는가&lt;/li&gt;
&lt;li&gt;인증이나 규칙 동의 절차가 지나치게 길지 않은가&lt;/li&gt;
&lt;li&gt;모바일에서도 공지와 규칙이 잘 보이는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 입구 흐름을 다시 맞추려면 &lt;a href=&quot;https://blog.dishost.kr/102&quot;&gt;디스코드 서버 꾸미기, 채널 역할 배치 가이드&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/105&quot;&gt;디스코드 서버 규칙 채널 작성법&lt;/a&gt;을 같이 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 홍보 성과는 간단한 표로 기록한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 복잡한 분석 도구보다 간단한 기록만 있어도 충분합니다.&lt;br /&gt;&lt;br /&gt;주 1회 정도 아래처럼 정리해 두면 방향을 잡기 쉽습니다.&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;날짜 | 홍보 위치 | 사용한 초대 링크 | 유입 수 | 일주일 뒤 잔류 수
03/20 | 개발 커뮤니티 A | invite-dev | 18 | 7
03/20 | SNS 프로필 | invite-profile | 9 | 5
03/21 | 파트너 서버 B | invite-partner | 14 | 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이런 기록이 쌓이면 유입이 많은 채널보다 잔류가 높은 채널에 힘을 주게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 너무 이른 대규모 홍보는 오히려 손해다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입구 구조가 정리되기 전에 대규모 홍보를 하면 첫인상이 그대로 고정됩니다.&lt;br /&gt;&lt;br /&gt;한 번 실망하고 나간 유저가 다시 돌아오는 경우는 많지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 소규모로 열고, 공지와 온보딩 문구를 다듬고, 질문 흐름이 안정되면 점차 넓혀야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 홍보를 열기 전에 입구부터 고친다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 홍보를 시작했다면 유입 이후 머무를 이유를 만드는 구조까지 같이 봐야 합니다.&lt;br /&gt;&lt;br /&gt;홍보 유입 뒤 멤버가 머무를 채널 구조를 먼저 다듬고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/102&quot;&gt;디스코드 서버 꾸미기, 채널 역할 배치 가이드&lt;/a&gt;로 이어가면 됩니다.&lt;br /&gt;&lt;br /&gt;입구 구조를 먼저 다시 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/103&quot;&gt;디스코드 서버 온보딩 채널 구성 예시&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/106&quot;&gt;디스코드 공지 채널 운영법&lt;/a&gt;을 같이 점검해 둬야 합니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/112</guid>
      <comments>https://dishost.tistory.com/112#entry112comment</comments>
      <pubDate>Fri, 17 Apr 2026 16:48:37 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 서버 보안 설정 체크리스트</title>
      <link>https://dishost.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 어느 정도 꾸며 놓고 나면 그다음은 보안입니다.&lt;br /&gt;&lt;br /&gt;역할 구조가 애매하거나 &lt;code&gt;@everyone&lt;/code&gt; 권한이 넓으면 스팸, 광고, 채널 오염이 금방 들어옵니다.&lt;br /&gt;&lt;br /&gt;초반 서버일수록 큰 사고 한 번으로 운영 의욕이 꺾이는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 점검은 사고가 난 뒤보다 채널 구조를 다듬는 시점에 같이 해 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &lt;code&gt;@everyone&lt;/code&gt; 권한부터 줄인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 볼 항목입니다.&lt;br /&gt;&lt;br /&gt;초반 서버에서 &lt;code&gt;@everyone&lt;/code&gt;에 너무 많은 권한이 열려 있는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 아래 정도부터 제한하는 쪽이 안전합니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Manage Channels
Manage Roles
Mention @everyone, @here, and All Roles
Create Instant Invite
Manage Webhooks&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;기본 역할은 최대한 좁게 두고 필요한 역할에만 풀어야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 공지 채널과 운영 채널을 분리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안은 기술 옵션만의 문제가 아닙니다.&lt;br /&gt;&lt;br /&gt;누가 어디서 무엇을 할 수 있는지 구조가 분명해야 사고가 줄어듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공지 채널은 읽기 전용, 운영 채널은 관리자만, 질문 채널은 일반 멤버 사용 가능처럼 목적을 분리해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 관리자 권한은 최소한으로 준다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 편의 때문에 관리자 권한을 넓게 주는 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;이 방식은 실수 한 번의 영향이 너무 큽니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;관리자&lt;/code&gt;와 &lt;code&gt;운영진&lt;/code&gt;을 분리하고, 운영진에는 필요한 기능 권한만 줘야 더 안전합니다.&lt;br /&gt;&lt;br /&gt;역할 설계 자체는 &lt;a href=&quot;https://blog.dishost.kr/58&quot;&gt;디스코드 역할 설정 완벽 가이드, 관리자 운영진 멤버 봇 역할을 처음부터 정리하는 법&lt;/a&gt;과 같이 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 인증 전 유저 노출 범위를 제한한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개 서버라면 인증 전 유저가 볼 수 있는 채널을 줄여야 합니다.&lt;br /&gt;&lt;br /&gt;규칙, 공지, 인증 채널 정도만 먼저 보여 주고, 인증 후 커뮤니티 채널을 여는 구조가 흔합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 스팸 계정 유입 때 피해 범위를 줄이는 데 도움이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 초대 링크 관리도 보안이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만료 없는 링크를 아무 데나 뿌리면 유입 경로 추적이 어려워집니다.&lt;br /&gt;&lt;br /&gt;기본 링크와 이벤트 링크를 분리하고, 필요 없는 링크는 정리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 중인 공개 링크가 몇 개인지 정도는 주기적으로 확인해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 봇 권한도 최소한만 준다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇은 편하다고 &lt;code&gt;Administrator&lt;/code&gt;를 주기 쉽습니다.&lt;br /&gt;&lt;br /&gt;하지만 실제로는 필요한 기능 권한만 열어야 안전하고 문제 추적도 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 삭제 봇이면 &lt;code&gt;Manage Messages&lt;/code&gt;, 역할 지급 봇이면 &lt;code&gt;Manage Roles&lt;/code&gt;처럼 기능 단위로 줘야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 운영 체크리스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목은 주기적으로 확인해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;- @everyone 권한이 과하게 열려 있지 않은가
- 관리자 역할이 너무 많은 사람에게 부여되지 않았는가
- 인증 전 채널 노출 범위가 적절한가
- 공지 채널이 읽기 전용으로 유지되는가
- 초대 링크가 필요 이상으로 남아 있지 않은가
- 봇 권한이 최소 범위를 넘어서지 않는가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 정도만 봐도 초반 사고 대부분은 줄일 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 사고는 복잡한 해킹보다 설정 실수에서 더 자주 난다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서버에서는 외부 공격보다 운영진 실수, 과한 권한, 방치된 링크 문제를 더 자주 봅니다.&lt;br /&gt;&lt;br /&gt;보안 체크리스트는 어려운 기술보다 이런 실수를 줄이는 데 초점이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 보안 점검 뒤에는 입구 운영까지 연결한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 구조까지 맞췄다면 인증 전 동선과 공지 운영도 같이 점검해야 합니다.&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://blog.dishost.kr/103&quot;&gt;디스코드 서버 온보딩 채널 구성 예시&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/106&quot;&gt;디스코드 공지 채널 운영법&lt;/a&gt;을 같이 열어 두면 보안 설정이 실제 동선과 맞는지 확인하기 쉽습니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/110</guid>
      <comments>https://dishost.tistory.com/110#entry110comment</comments>
      <pubDate>Wed, 15 Apr 2026 16:47:47 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 카테고리 채널 정리하는 법</title>
      <link>https://dishost.tistory.com/109</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 오래 굴리다 보면 채널이 점점 늘어납니다.&lt;br /&gt;&lt;br /&gt;공지, 질문, 이벤트, 봇 테스트, 운영 로그가 쌓이면서 처음의 단순한 구조가 금방 흐트러집니다.&lt;br /&gt;&lt;br /&gt;이 시점부터는 채널을 추가하는 것보다 정리하는 능력이 더 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리는 채널을 지우는 작업보다 유저가 덜 헤매게 만드는 작업에 더 가깝습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 채널 수보다 기능이 기준이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 목적의 채널이 여러 개 생기면 유저는 어느 곳을 써야 할지 헷갈립니다.&lt;br /&gt;&lt;br /&gt;그래서 채널 정리는 채널 수를 줄이는 작업이 아니라 기능을 분명하게 만드는 작업에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;잡담1&lt;/code&gt;, &lt;code&gt;잡담2&lt;/code&gt;, &lt;code&gt;잡담3&lt;/code&gt;보다 &lt;code&gt;잡담&lt;/code&gt;, &lt;code&gt;질문&lt;/code&gt;, &lt;code&gt;자료공유&lt;/code&gt;가 낫습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 카테고리는 큰 기능 단위만 남긴다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반 서버라면 아래 정도가 가장 많이 버팁니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;  시작하기
  커뮤니티
  봇
  이벤트
  운영&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;카테고리를 더 쪼개고 싶어질 때도 있지만, 사람이 바로 이해할 수 있는 수를 넘기면 안 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 안 쓰는 채널은 과감히 숨기거나 지운다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 달 이상 거의 안 쓰는 채널은 정리 대상으로 봐도 되는 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;특히 이벤트 종료 채널, 테스트용 채널, 중복 질문 채널이 자주 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지우기 부담스럽다면 먼저 아카이브용 카테고리로 옮겨 두는 방식도 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 채널 이름 규칙을 통일한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;공지&lt;/code&gt;, &lt;code&gt;general-chat&lt;/code&gt;, &lt;code&gt;질문방&lt;/code&gt;, &lt;code&gt;bot-command&lt;/code&gt;처럼 언어와 형식이 섞이면 구조가 더 어수선해 보입니다.&lt;br /&gt;&lt;br /&gt;한글 위주인지, 영어 위주인지, 이모지를 붙일지 먼저 정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통일 예시는 아래처럼 둘 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;공지
잡담
질문
자료공유
봇명령어
관리자전용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;규칙 하나만 맞아도 서버 인상이 많이 정리됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 질문 채널과 잡담 채널은 구분해 둬야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘을 한 채널에 섞으면 질문이 금방 묻힙니다.&lt;br /&gt;&lt;br /&gt;디스코드 봇 개발 서버나 정보 공유 서버라면 특히 분리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널 수를 줄이더라도 이 둘은 구분해야 운영상 이점이 큽니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 봇 채널도 로그와 명령어를 나눌지 판단한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇 수가 적으면 &lt;code&gt;봇명령어&lt;/code&gt; 하나로 버틸 수 있습니다.&lt;br /&gt;&lt;br /&gt;AI 봇, 로그 봇, 티켓 봇까지 늘어나면 &lt;code&gt;봇명령어&lt;/code&gt;와 &lt;code&gt;봇로그&lt;/code&gt;를 나눠야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 분리는 운영 채널을 깔끔하게 유지하는 데 꽤 도움이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 정리 기준을 운영진끼리 합의해 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누가 새 채널을 만들 수 있는지, 어떤 목적이면 새 카테고리를 추가하는지 기준이 없으면 다시 늘어납니다.&lt;br /&gt;&lt;br /&gt;정리보다 중요한 것은 다시 복잡해지지 않게 막는 기준입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 정도만 정해 둬도 좋습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새 채널 추가 전 기존 채널 재활용 검토&lt;/li&gt;
&lt;li&gt;이벤트 종료 채널 정리 시점 지정&lt;/li&gt;
&lt;li&gt;테스트 채널은 운영 카테고리 안에서만 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 정리 후 반드시 보는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하고 나면 일반 계정으로 실제 동선을 봅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음 들어온 유저가 어디를 먼저 보는가&lt;/li&gt;
&lt;li&gt;질문은 어디에 올려야 하는가&lt;/li&gt;
&lt;li&gt;공지는 잘 보이는가&lt;/li&gt;
&lt;li&gt;운영 채널은 숨겨져 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 항목이 선명하면 정리가 잘 된 상태입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 다음 단계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 정리했다면 공지 위치와 운영 채널 노출 범위도 같이 다시 봐야 합니다.&lt;br /&gt;&lt;br /&gt;채널 배치 기준이 흔들리면 &lt;a href=&quot;https://blog.dishost.kr/102&quot;&gt;디스코드 서버 꾸미기, 채널 역할 배치 가이드&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/106&quot;&gt;디스코드 공지 채널 운영법&lt;/a&gt;을 같이 점검하면 됩니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/109</guid>
      <comments>https://dishost.tistory.com/109#entry109comment</comments>
      <pubDate>Tue, 14 Apr 2026 16:47:21 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 서버 템플릿 적용하는 법</title>
      <link>https://dishost.tistory.com/108</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서버 템플릿을 만들어 두었어도 실제로 새 서버에 적용해 보는 과정에서 다시 막히는 경우가 있습니다.&lt;br /&gt;&lt;br /&gt;어떤 구조가 복제되는지, 적용 후 무엇을 먼저 손봐야 하는지 감이 없으면 템플릿 장점이 줄어듭니다.&lt;br /&gt;&lt;br /&gt;적용은 단순하지만 후속 정리가 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿 적용은 1분이면 끝나도 운영용 서버로 다듬는 후처리는 따로 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 템플릿 링크 또는 템플릿 선택으로 시작한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 템플릿 기능을 열어 두었다면 템플릿 생성 시 만든 링크를 쓰거나, 디스코드에서 관련 메뉴를 통해 새 서버 생성 흐름으로 들어갑니다.&lt;br /&gt;&lt;br /&gt;이 단계에서는 채널과 카테고리 골격을 빠르게 가져오는 것이 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 템플릿을 안 만들었다면 &lt;a href=&quot;https://blog.dishost.kr/107&quot;&gt;디스코드 서버 템플릿 만드는 법&lt;/a&gt;부터 먼저 맞춰야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 적용 후 가장 먼저 볼 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿을 적용했다고 바로 끝나지 않습니다.&lt;br /&gt;&lt;br /&gt;새 서버 이름, 아이콘, 역할 구조, 초대 링크는 다시 점검해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크 항목은 아래 정도면 충분합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 서버 이름과 아이콘 변경
2. 공지 / 규칙 채널 내용 수정
3. 역할 이름과 순서 확인
4. 테스트용 채널 제거 여부 확인
5. 초대 링크 재생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;구조 복제 뒤 이 단계에서 실제 운영용 서버로 바뀝니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 공지와 규칙은 그대로 두면 안 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿 안에 넣어 둔 기본 문구는 새 서버 목적에 맞게 바꿔야 합니다.&lt;br /&gt;&lt;br /&gt;스터디 서버, 게임 서버, 정보 공유 서버는 분위기와 운영 기준이 다를 수밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 규칙과 공지는 복제 후 바로 수정하는 습관이 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 역할 구조도 목적에 맞게 다듬는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿의 역할 구조가 항상 정답은 아닙니다.&lt;br /&gt;&lt;br /&gt;예를 들어 스터디 서버는 &lt;code&gt;출석&lt;/code&gt;, &lt;code&gt;멘토&lt;/code&gt;, &lt;code&gt;기수&lt;/code&gt; 역할이 필요할 수 있고, 커뮤니티 서버는 &lt;code&gt;알림수신&lt;/code&gt;, &lt;code&gt;이벤트참여&lt;/code&gt;가 더 중요할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿은 출발점이고, 최종 구조는 새 서버 성격에 맞춰야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 봇 채널과 운영 채널은 특히 확인한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿 복제 후 가장 빨리 손봐야 하는 채널이 이 두 가지입니다.&lt;br /&gt;&lt;br /&gt;봇 테스트용 로그가 남아 있거나, 운영 채널 이름이 모호하면 바로 정리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 채널은 노출 범위도 같이 확인해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 템플릿 적용 후 테스트 계정으로 들어가 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 계정만 보고 끝내면 권한 문제를 놓치기 쉽습니다.&lt;br /&gt;&lt;br /&gt;가능하면 일반 계정으로 들어가 아래를 확인합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;규칙과 공지가 바로 보이는가&lt;/li&gt;
&lt;li&gt;질문 채널과 잡담 채널 동선이 자연스러운가&lt;/li&gt;
&lt;li&gt;관리자 채널이 숨겨져 있는가&lt;/li&gt;
&lt;li&gt;봇 명령어 채널이 분리되어 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정이 빠지면 구조 복제는 했는데 실제 동선이 꼬일 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 템플릿 적용이 특히 편한 상황&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새 기수 서버를 반복해서 만드는 스터디 운영&lt;/li&gt;
&lt;li&gt;클라이언트 서버 기본형 세팅&lt;/li&gt;
&lt;li&gt;이벤트 서버 단기 오픈&lt;/li&gt;
&lt;li&gt;봇 테스트 서버 반복 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때는 템플릿 적용 속도가 꽤 크게 체감됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 적용 후 남는 흔한 문제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공지 문구가 이전 서버 내용 그대로 남아 있는 경우&lt;/li&gt;
&lt;li&gt;역할 순서가 새 서버 목적과 안 맞는 경우&lt;/li&gt;
&lt;li&gt;테스트용 카테고리까지 같이 복제된 경우&lt;/li&gt;
&lt;li&gt;초대 링크를 새로 안 만들어 외부 공유가 꼬이는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿을 썼다고 후처리가 줄어드는 것은 아닙니다.&lt;br /&gt;&lt;br /&gt;반복 작업만 줄여 주는 쪽입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 다음 단계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿까지 적용했다면 규칙, 공지, 질문 채널이 실제 동선에 맞는지 바로 점검해야 합니다.&lt;br /&gt;&lt;br /&gt;적용 직후 구조를 다듬으려면 &lt;a href=&quot;https://blog.dishost.kr/102&quot;&gt;디스코드 서버 꾸미기, 채널 역할 배치 가이드&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/106&quot;&gt;디스코드 공지 채널 운영법&lt;/a&gt;을 같이 열어 두면 됩니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/108</guid>
      <comments>https://dishost.tistory.com/108#entry108comment</comments>
      <pubDate>Mon, 13 Apr 2026 16:46:55 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 서버 템플릿 만드는 법</title>
      <link>https://dishost.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서버 구조를 어느 정도 잡고 나면 같은 골격을 다시 만들고 싶어질 때가 있습니다.&lt;br /&gt;&lt;br /&gt;스터디 서버를 여러 개 운영하거나, 테스트 서버를 따로 만들거나, 클라이언트 서버 세팅을 반복할 때 특히 그렇습니다.&lt;br /&gt;&lt;br /&gt;이때 디스코드 서버 템플릿 기능을 써 두면 구조 복제가 훨씬 빨라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿은 서버를 새로 만들 때 시간을 줄여 주지만, 지저분한 구조까지 같이 복제한다는 점도 같이 따라옵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 템플릿은 구조 복사용 도구다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널, 카테고리, 일부 기본 골격을 빠르게 복제하는 용도입니다.&lt;br /&gt;&lt;br /&gt;운영 문장, 실제 멤버, 개별 권한 이력까지 그대로 옮겨 가는 만능 백업 개념은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 템플릿을 만들기 전에 먼저 구조를 정리해야 합니다.&lt;br /&gt;&lt;br /&gt;지금 서버 골격이 흔들린 상태라면 &lt;a href=&quot;https://blog.dishost.kr/102&quot;&gt;디스코드 서버 꾸미기, 채널 역할 배치 가이드&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/106&quot;&gt;디스코드 공지 채널 운영법&lt;/a&gt;을 먼저 다듬어야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 템플릿 만들기 전에 정리할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목은 한번 보고 들어가야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안 쓰는 채널 정리&lt;/li&gt;
&lt;li&gt;채널 이름 규칙 통일&lt;/li&gt;
&lt;li&gt;공지 / 규칙 / 질문 / 운영 채널 위치 점검&lt;/li&gt;
&lt;li&gt;역할 구조 최소화&lt;/li&gt;
&lt;li&gt;테스트용 채널 제거 여부 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지저분한 서버를 그대로 템플릿으로 저장하면 그 상태가 반복됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 기본 골격 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿용 서버라면 아래 정도 구성이 무난합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;  시작하기
├─ 규칙
├─ 공지
├─ 역할선택

  커뮤니티
├─ 잡담
├─ 질문
└─ 자료공유

  봇
├─ 봇명령어
└─ 봇로그

  운영
├─ 관리자전용
└─ 문의관리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 정도면 입문 서버, 스터디 서버, 커뮤니티 서버 대부분에 응용이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 템플릿 생성 위치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 설정으로 들어가서 &lt;code&gt;서버 템플릿&lt;/code&gt; 관련 메뉴를 찾습니다.&lt;br /&gt;&lt;br /&gt;설명 문구와 템플릿 이름을 정한 뒤 생성하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿 이름은 목적이 보이게 지어야 합니다.&lt;br /&gt;&lt;br /&gt;예를 들어 &lt;code&gt;스터디 서버 기본형&lt;/code&gt;, &lt;code&gt;봇 테스트 서버 기본형&lt;/code&gt;처럼 두면 나중에 헷갈리지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 템플릿 설명 문구도 짧게 써 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명 문구를 비워 두면 나중에 뭐였는지 기억이 잘 안 날 수 있습니다.&lt;br /&gt;&lt;br /&gt;아래 정도만 적어도 충분합니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;규칙 / 공지 / 질문 / 봇명령어 / 운영 채널이 포함된 기본 커뮤니티 서버 템플릿&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;실제 사용 시 누가 봐도 용도를 이해할 수 있어야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 템플릿을 자주 갱신할 필요도 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 구조는 운영하면서 바뀝니다.&lt;br /&gt;&lt;br /&gt;이벤트 채널이 추가되거나 온보딩 구조가 달라질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 템플릿도 한 번 만들고 끝내지 말고, 구조가 안정화될 때마다 업데이트해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 템플릿과 백업은 다르다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 헷갈리는 경우가 있습니다.&lt;br /&gt;&lt;br /&gt;템플릿은 새 서버 골격 복사에 가깝습니다.&lt;br /&gt;&lt;br /&gt;기존 운영 이력이나 로그를 그대로 보존하는 백업 도구로 생각하면 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 복구 관점은 별도로 챙겨야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 어떤 서버에 특히 유용한가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스터디 서버를 여러 기수로 운영하는 경우&lt;/li&gt;
&lt;li&gt;봇 테스트 서버를 자주 다시 만드는 경우&lt;/li&gt;
&lt;li&gt;고객용 커뮤니티 서버 기본형이 필요한 경우&lt;/li&gt;
&lt;li&gt;이벤트용 임시 서버를 빠르게 열어야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서는 템플릿 하나만 잘 만들어 둬도 시간이 꽤 절약됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 다음 단계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿을 만들었다면 공지와 역할 구조가 복제해 둘 만큼 안정적인지도 다시 봐야 합니다.&lt;br /&gt;&lt;br /&gt;골격부터 다시 다듬으려면 &lt;a href=&quot;https://blog.dishost.kr/102&quot;&gt;디스코드 서버 꾸미기, 채널 역할 배치 가이드&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/106&quot;&gt;디스코드 공지 채널 운영법&lt;/a&gt;을 같이 점검하면 됩니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/107</guid>
      <comments>https://dishost.tistory.com/107#entry107comment</comments>
      <pubDate>Sun, 12 Apr 2026 16:46:29 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 서버 규칙 채널 작성법</title>
      <link>https://dishost.tistory.com/105</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 채널이 길고 무거우면 유저는 안 읽고, 짧기만 하면 운영진이 나중에 설명하다 지칩니다.&lt;br /&gt;&lt;br /&gt;좋은 규칙 채널은 많아 보이는 규칙보다 &quot;뭐가 금지인지, 어디까지 제재하는지, 어디에 문의하는지&quot;가 바로 보이는 문안입니다.&lt;br /&gt;&lt;br /&gt;이 글은 규칙을 많이 쓰는 법이 아니라, 읽히는 규칙 카피를 어떻게 쓰는지에 집중하는 글입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 규칙 채널은 법전이 아니라 행동 기준 요약본이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 채널은 운영진 기록용 문서가 아닙니다.&lt;br /&gt;&lt;br /&gt;신규 유저가 서버 분위기와 금지 행동을 짧은 시간 안에 파악하게 만드는 용도입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길게 설명하는 것보다 아래 세 가지가 바로 보여야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하면 안 되는 행동&lt;/li&gt;
&lt;li&gt;제재 기준&lt;/li&gt;
&lt;li&gt;문의 위치&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 초반 규칙은 짧아야 읽힌다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 규칙 20개를 쓰면 거의 안 읽힙니다.&lt;br /&gt;&lt;br /&gt;실제 운영 초반에는 핵심 규칙 5~7개 정도면 충분한 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 아래처럼 시작할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 욕설, 비방, 분쟁 유도 금지
2. 광고, 도배, 스팸 금지
3. 개인정보 공유 금지
4. 불법 자료 공유 금지
5. 운영진 안내 우선 적용
6. 문의는 문의 채널 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 정도만 있어도 기본 방향은 충분히 전달됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 규칙 문장은 추상적이면 안 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;서로 예의를 지켜 주세요&lt;/code&gt;처럼 너무 넓은 문장만 있으면 실제 제재 기준으로 쓰기 어렵습니다.&lt;br /&gt;&lt;br /&gt;어떤 행동이 금지되는지 조금 더 명확히 써야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;과도한 욕설, 타 유저 조롱, 싸움 유도 발언 금지&lt;/code&gt;처럼 쓰면 운영 기준이 선명해집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 제재 기준을 아주 짧게라도 적어 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙을 어겼을 때 어떤 조치가 있는지 완전히 비워 두면 운영 일관성이 흔들립니다.&lt;br /&gt;&lt;br /&gt;길게 적을 필요는 없지만 기본 원칙은 남겨 둬야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 아래처럼 둘 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;경고 없이 메시지 삭제가 진행될 수 있습니다.
반복 위반은 타임아웃 또는 퇴장으로 이어질 수 있습니다.
악성 광고, 불법 자료 공유는 즉시 제재 대상입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 정도면 운영진도 기준을 설명하기 쉬워집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 문의 위치를 반드시 적는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 해석에 이견이 생기거나 제재 문의가 들어올 수 있습니다.&lt;br /&gt;&lt;br /&gt;문의 채널이나 운영진 호출 위치가 없으면 잡담 채널에서 바로 분쟁이 시작됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 채널 하단에 문의 동선을 적어 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 너무 딱딱한 문체만 고집할 필요는 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 채널이라고 해서 법률 문서처럼 쓰면 읽기 어려워집니다.&lt;br /&gt;&lt;br /&gt;건조한 문체는 유지하되, 문장을 짧게 자르고 항목형으로 정리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽히는 규칙 채널은 권위적인 문장보다 구조가 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 인증 서버라면 연결 문구도 넣는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증을 거쳐야 활동이 가능한 서버라면 규칙 채널 마지막에 다음 동작을 적어 둬야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 아래처럼 둘 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;규칙을 확인했다면 역할선택 또는 인증 채널로 이동해 주세요.
인증이 끝나면 잡담과 질문 채널이 열립니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 문장 하나가 온보딩 이탈을 줄이는 경우가 많습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 운영자가 주기적으로 고칠 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 커지면 초반 규칙만으로 부족해질 수 있습니다.&lt;br /&gt;&lt;br /&gt;광고, 티켓 남용, 특정 이벤트 스팸처럼 실제로 자주 생기는 문제를 반영해서 규칙을 다듬어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 쓴 문장을 오래 방치하면 운영 현실과 안 맞아집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 규칙을 썼다면 이제 읽히는 위치와 전달 방식까지 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 문안을 정리했다면 다음은 그 문구가 실제로 눈에 들어오는 위치에 놓여 있는지 봐야 합니다.&lt;br /&gt;&lt;br /&gt;입구 배치까지 같이 손보려면 &lt;a href=&quot;https://blog.dishost.kr/103&quot;&gt;디스코드 서버 온보딩 채널 구성 예시&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/102&quot;&gt;디스코드 서버 꾸미기, 채널 역할 배치 가이드&lt;/a&gt;를 같이 놓고 봐야 흐름이 맞습니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/105</guid>
      <comments>https://dishost.tistory.com/105#entry105comment</comments>
      <pubDate>Fri, 10 Apr 2026 16:45:38 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 서버 꾸미기, 채널 역할 배치 가이드</title>
      <link>https://dishost.tistory.com/102</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 서버를 꾸민다고 하면 보통 배너, 이모지, 역할 색상부터 떠올립니다.&lt;br /&gt;&lt;br /&gt;실제로 서버 인상을 좌우하는 것은 채널 순서와 역할 배치입니다.&lt;br /&gt;&lt;br /&gt;처음 들어온 유저가 어디로 이동하는지, 운영진이 어디서 관리하는지, 봇이 어느 채널에서 일하는지가 한눈에 보여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치는 보기 좋은 것보다 운영 동선이 오래 버티는 쪽으로 잡아야 합니다.&lt;br /&gt;&lt;br /&gt;기본 서버 골격이 아직 없다면 &lt;a href=&quot;https://blog.dishost.kr/54&quot;&gt;디스코드 서버 만드는 법 처음부터 끝까지, 채널 역할 기본 세팅 가이드&lt;/a&gt;부터 먼저 맞춰 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 꾸미기 전에 동선부터 정한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 들어온 유저가 밟는 순서를 먼저 정해야 합니다.&lt;br /&gt;&lt;br /&gt;서버 입구가 복잡하면 채널이 많아도 체감은 오히려 나빠집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반 동선은 보통 아래처럼 잡습니다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;규칙 확인
-&amp;gt; 공지 확인
-&amp;gt; 역할 선택 또는 인증
-&amp;gt; 잡담 / 질문 / 봇 명령어 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 흐름이 선명하면 채널 수가 좀 있어도 덜 복잡해 보입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 카테고리는 기능 단위로 나눈다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장식 기준으로 나누기보다 기능 기준으로 나눠야 관리가 쉽습니다.&lt;br /&gt;&lt;br /&gt;초반 서버라면 아래 정도가 가장 무난합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;  시작하기
  커뮤니티
  봇
  이벤트
  운영&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 다섯 묶음만 잘 잡아도 대부분의 중소형 서버는 충분히 굴러갑니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 채널 배치 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 아래처럼 두면 정리가 쉽습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;  시작하기
├─ 규칙
├─ 공지
├─ 역할선택
└─ 서버소개

  커뮤니티
├─ 잡담
├─ 질문
└─ 자료공유

  봇
├─ 봇명령어
└─ 봇로그

  이벤트
├─ 이벤트공지
└─ 참여인증

  운영
├─ 관리자전용
├─ 문의관리
└─ 내부로그&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;꾸민다는 이유로 잡담 채널만 여러 개 늘리는 방식은 초반에 잘 맞지 않습니다.&lt;br /&gt;활동량이 적을 때는 채널 수보다 대화 밀도가 더 중요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 역할도 장식보다 기능이 먼저다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 색상부터 정하기 시작하면 구조가 쉽게 흐트러집니다.&lt;br /&gt;&lt;br /&gt;먼저 기능 역할을 두고, 장식 역할은 그다음에 붙여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반 역할 예시는 아래 정도면 충분합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;관리자
운영진
봇
인증됨
멤버
알림수신&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`알림수신`처럼 목적이 분명한 역할은 실제 운영에 도움이 됩니다.&lt;br /&gt;반대로 이름만 멋있는 역할은 나중에 정리 대상이 되기 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 채널 이름 톤도 맞춘다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널 이름 규칙이 제각각이면 구조가 더 복잡해 보입니다.&lt;br /&gt;&lt;br /&gt;한글 위주로 갈지, 영어 위주로 갈지, 이모지를 붙일지 먼저 정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아래처럼 통일감을 주면 읽기 편합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;공지
잡담
질문
봇명령어
관리자전용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;또는 아래처럼 이모지를 붙여도 됩니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt; -공지
 -잡담
❓-질문
 -봇명령어
 -관리자전용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;둘 다 괜찮지만 한 서버 안에서는 규칙을 섞지 말아야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 공지와 잡담은 최대한 멀리 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공지 채널이 잡담 채널 아래에 묻히면 실제로 안 읽힙니다.&lt;br /&gt;&lt;br /&gt;유저가 들어왔을 때 가장 먼저 규칙과 공지가 보여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 꾸밀 때 시각적인 예쁨보다 위쪽 배치 우선순위를 먼저 정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 봇 채널은 따로 분리해야 덜 엉킨다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇 명령어와 일반 대화를 섞어 두면 채널이 금방 지저분해집니다.&lt;br /&gt;&lt;br /&gt;기본 봇만 있어도 &lt;code&gt;!핑&lt;/code&gt;, &lt;code&gt;/핑&lt;/code&gt;, 각종 테스트 로그가 쌓이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반부터 &lt;code&gt;봇명령어&lt;/code&gt; 하나를 따로 두면 이후 AI 봇, 티켓 봇, 로그 봇을 붙일 때도 편합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 운영 관점에서 꼭 봐야 할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목이 설명 가능해야 서버 구조가 잘 잡힌 상태입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새 유저는 어디부터 보는가&lt;/li&gt;
&lt;li&gt;공지는 누가 쓰는가&lt;/li&gt;
&lt;li&gt;봇은 어느 채널에서 동작하는가&lt;/li&gt;
&lt;li&gt;운영진은 어디서 내부 논의를 하는가&lt;/li&gt;
&lt;li&gt;문의가 들어오면 어디로 모이는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에 바로 답이 안 나오면 구조를 다시 다듬어야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 배치가 끝났다면 동선으로 다시 검증한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널과 역할 배치를 정리했다면 다음에는 신규 유저가 처음 들어왔을 때 어떤 화면을 보게 할지 온보딩 구조를 구체화해야 합니다.&lt;br /&gt;&lt;br /&gt;역할 구조를 더 손보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/58&quot;&gt;디스코드 역할 설정 완벽 가이드, 관리자 운영진 멤버 봇 역할을 처음부터 정리하는 법&lt;/a&gt;을 같이 보면 됩니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/102</guid>
      <comments>https://dishost.tistory.com/102#entry102comment</comments>
      <pubDate>Tue, 7 Apr 2026 18:33:53 +0900</pubDate>
    </item>
    <item>
      <title>discord.js v14 디스코드 봇 만들기 처음부터 끝까지</title>
      <link>https://dishost.tistory.com/63</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 쪽 흐름을 한 번 익힌 뒤에는 Node.js 기반으로도 같은 구조를 만들 수 있어야 선택지가 넓어집니다.&lt;br /&gt;&lt;br /&gt;디스코드 봇 생태계에서는 &lt;code&gt;discord.js&lt;/code&gt; 수요가 여전히 큽니다.&lt;br /&gt;&lt;br /&gt;특히 버튼, 모달, 슬래시 명령어 예제를 찾다 보면 대부분 &lt;code&gt;discord.js v14&lt;/code&gt; 기준으로 만나는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 쪽에서 가장 먼저 손에 익혀 둘 기본 골격은 &lt;code&gt;discord.js v14&lt;/code&gt;입니다.&lt;br /&gt;&lt;br /&gt;토큰 분리, 패키지 설치, 기본 명령어, 서버 초대, 실행 테스트까지 한 번에 정리합니다.&lt;br /&gt;&lt;br /&gt;파이썬 쪽 흐름과 비교해 보고 싶다면 &lt;a href=&quot;https://blog.dishost.kr/53&quot;&gt;파이썬 디스코드 봇 만들기 처음부터 끝까지, discord.py 입문 가이드&lt;/a&gt;를 같이 열어 두면 구조 차이를 보기에 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Node.js 버전부터 맞춘다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;discord.js v14&lt;/code&gt;는 너무 오래된 Node 버전에서는 문제를 일으킬 수 있습니다.&lt;br /&gt;&lt;br /&gt;처음에는 코드보다 실행 환경에서 더 자주 막힙니다.&lt;br /&gt;&lt;br /&gt;Node 18 이상을 맞춰 둬야 안전합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 아래 명령어로 버전을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;node -v
npm -v&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Node가 없거나 버전이 너무 낮다면 먼저 설치를 마칩니다.&lt;br /&gt;이 단계가 어긋나면 패키지 설치는 되는데 실행 시점에서 경고가 뜨는 경우가 나옵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 프로젝트 폴더 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 폴더를 하나 만들고 터미널에서 이동합니다.&lt;br /&gt;&lt;br /&gt;그다음 &lt;code&gt;npm init&lt;/code&gt;으로 기본 프로젝트를 시작합니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;mkdir my-discordjs-bot
cd my-discordjs-bot
npm init -y
npm install discord.js dotenv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`discord.js`는 디스코드 API 통신용 패키지입니다.&lt;br /&gt;`dotenv`는 토큰을 `.env`에 분리해서 읽어오는 용도입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Developer Portal에서 봇 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 Discord Developer Portal로 들어갑니다.&lt;br /&gt;&lt;br /&gt;새 애플리케이션을 만든 뒤 &lt;code&gt;Bot&lt;/code&gt; 메뉴에서 봇을 추가합니다.&lt;br /&gt;&lt;br /&gt;토큰을 복사할 수 있으면 일단 안전한 곳에 잠깐 보관합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;Message Content Intent&lt;/code&gt;도 같이 켜 둡니다.&lt;br /&gt;&lt;br /&gt;지금 단계에서는 접두사 명령어를 테스트할 예정이라 이 설정이 빠지면 &lt;code&gt;!핑&lt;/code&gt;이 반응하지 않습니다.&lt;br /&gt;&lt;br /&gt;이 문제는 &lt;a href=&quot;https://blog.dishost.kr/56&quot;&gt;Message Content Intent 설정법, 디스코드 봇이 명령어를 읽지 못할 때 먼저 볼 것&lt;/a&gt;에서 더 자세히 다뤘습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. .env 파일 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트에 &lt;code&gt;.env&lt;/code&gt; 파일을 만듭니다.&lt;br /&gt;&lt;br /&gt;내용은 아래처럼 두면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_디스코드_봇_토큰
PREFIX=!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;토큰을 코드에 직접 적어 두면 화면 공유나 깃 업로드 때 그대로 노출될 수 있습니다.&lt;br /&gt;파이썬과 마찬가지로 JS 쪽도 처음부터 분리해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. main.js 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트에 &lt;code&gt;main.js&lt;/code&gt; 파일을 만들고 아래 코드를 넣습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const { Client, GatewayIntentBits, EmbedBuilder } = require(&quot;discord.js&quot;);
const dotenv = require(&quot;dotenv&quot;);

dotenv.config();

const token = process.env.DISCORD_TOKEN;
const prefix = process.env.PREFIX || &quot;!&quot;;

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

client.once(&quot;ready&quot;, () =&amp;gt; {
  console.log(`로그인 성공: ${client.user.tag}`);
});

client.on(&quot;messageCreate&quot;, async (message) =&amp;gt; {
  if (message.author.bot) {
    return;
  }

  if (!message.content.startsWith(prefix)) {
    return;
  }

  const args = message.content.slice(prefix.length).trim().split(/\s+/);
  const command = args.shift()?.toLowerCase();

  if (command === &quot;핑&quot;) {
    await message.reply(`퐁, 현재 지연 시간은 ${client.ws.ping}ms 입니다.`);
    return;
  }

  if (command === &quot;도움&quot;) {
    await message.reply([
      &quot;사용 가능한 명령어&quot;,
      `${prefix}핑 - 응답 속도 확인`,
      `${prefix}도움 - 명령어 목록 확인`,
      `${prefix}서버정보 - 현재 서버 정보 확인`,
    ].join(&quot;\n&quot;));
    return;
  }

  if (command === &quot;서버정보&quot;) {
    if (!message.guild) {
      await message.reply(&quot;이 명령어는 서버에서만 사용할 수 있습니다.&quot;);
      return;
    }

    const embed = new EmbedBuilder()
      .setTitle(&quot;서버 정보&quot;)
      .addFields(
        { name: &quot;서버 이름&quot;, value: message.guild.name, inline: false },
        { name: &quot;멤버 수&quot;, value: String(message.guild.memberCount), inline: true },
        { name: &quot;채널 수&quot;, value: String(message.guild.channels.cache.size), inline: true }
      );

    await message.reply({ embeds: [embed] });
  }
});

if (!token) {
  throw new Error(&quot;DISCORD_TOKEN 값이 없습니다.&quot;);
}

client.login(token);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;파이썬의 `commands.Bot`처럼 편하게 명령어를 다루는 구조도 만들 수 있지만, 처음에는 `messageCreate` 이벤트를 직접 보면 흐름 이해에 유리합니다.&lt;br /&gt;메시지가 들어오고, 접두사를 자르고, 명령어를 분기하는 방식이 눈에 바로 들어오기 때문입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 서버 초대 링크 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;OAuth2&lt;/code&gt; -&amp;gt; &lt;code&gt;URL Generator&lt;/code&gt;로 들어갑니다.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;bot&lt;/code&gt; 스코프를 고르고, 최소한 아래 권한을 켭니다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;View Channels
Send Messages
Read Message History
Embed Links&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`!핑` 정도만 테스트할 때는 이 정도면 충분합니다.&lt;br /&gt;초대 링크 생성 흐름이 익숙하지 않다면 [디스코드 봇 초대 링크 생성법, 권한이 꼬이지 않게 링크 만드는 방법](https://blog.dishost.kr/59)을 먼저 봐야 빠릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 실행하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 아래 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;node main.js&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;정상 실행되면 `로그인 성공` 로그가 뜹니다.&lt;br /&gt;그다음 서버 채널에서 아래 명령어를 넣어 봅니다.&lt;/p&gt;
&lt;pre class=&quot;diff&quot;&gt;&lt;code&gt;!핑
!도움
!서버정보&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;초반 테스트는 봇 명령어 전용 채널에서 해야 합니다.&lt;br /&gt;잡담 채널에서 바로 실험하면 로그가 섞여서 확인이 느려집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 여기서 자주 막히는 문제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;messageCreate&lt;/code&gt;가 아예 안 들어오는 경우: Developer Portal의 &lt;code&gt;Message Content Intent&lt;/code&gt; 누락이 가장 흔합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Used disallowed intents&lt;/code&gt;나 관련 경고가 뜨는 경우: 코드에서는 &lt;code&gt;MessageContent&lt;/code&gt;를 켰는데 Portal에서는 저장하지 않은 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;토큰 오류가 나는 경우: &lt;code&gt;.env&lt;/code&gt; 공백, 잘못 복사한 토큰, 재발급 후 예전 값 사용을 먼저 봅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS 쪽은 코드 문법보다 환경값과 인텐트에서 더 자주 멈춥니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 다음 단계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 JS 봇이 켜졌다면 다음에는 슬래시 명령어 구조로 넘어가거나 AI 기능을 붙이면 됩니다.&lt;br /&gt;&lt;br /&gt;지금 순서에서는 먼저 AI 챗봇 확장으로 이어지는 흐름이 자연스럽습니다.&lt;br /&gt;&lt;br /&gt;24시간 운영까지 바로 올릴 계획이라면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt;처럼 별도 서버에 올려 두는 단계가 필요합니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.js</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/63</guid>
      <comments>https://dishost.tistory.com/63#entry63comment</comments>
      <pubDate>Sun, 29 Mar 2026 16:23:08 +0900</pubDate>
    </item>
    <item>
      <title>슬래시 명령어 디스코드 봇 만드는 법, 파이썬 discord.py app_commands 입문</title>
      <link>https://dishost.tistory.com/61</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;discord.py&lt;/code&gt;로 슬래시 명령어를 붙이려는데 &lt;code&gt;/핑&lt;/code&gt;조차 안 뜨거나 &lt;code&gt;app_commands&lt;/code&gt; 구조가 헷갈리면 여기서 많이 막힙니다.&lt;br /&gt;&lt;br /&gt;이 글에서는 &lt;code&gt;discord.py&lt;/code&gt;의 &lt;code&gt;app_commands&lt;/code&gt; 기준으로 &lt;code&gt;/핑&lt;/code&gt;, &lt;code&gt;/서버정보&lt;/code&gt;, &lt;code&gt;/청소&lt;/code&gt; 세 개를 바로 테스트할 수 있는 형태까지 한 번에 정리합니다.&lt;br /&gt;&lt;br /&gt;즉 이 글 하나로 슬래시 명령어 등록, 서버 단위 sync, 기본 권한 체크 흐름까지 바로 잡을 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 먼저 준비할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 파이썬 봇 구조와 토큰 분리는 이미 끝났다고 가정합니다.&lt;br /&gt;&lt;br /&gt;아직 이 단계가 안 됐다면 &lt;a href=&quot;https://blog.dishost.kr/53&quot;&gt;파이썬 디스코드 봇 만들기 처음부터 끝까지&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/60&quot;&gt;.env 환경변수로 디스코드 토큰 숨기기, 하드코딩 없이 안전하게 관리하는 법&lt;/a&gt;부터 먼저 봐야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 초대 링크에서 &lt;code&gt;applications.commands&lt;/code&gt;를 체크한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬래시 명령어는 일반 &lt;code&gt;bot&lt;/code&gt; 스코프만으로는 부족할 수 있습니다.&lt;br /&gt;&lt;br /&gt;초대 링크를 만들 때 &lt;code&gt;applications.commands&lt;/code&gt;도 같이 체크합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 아래 스코프 조합을 씁니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;bot
applications.commands&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 단계가 빠지면 코드가 맞아도 슬래시 명령어가 보이지 않을 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 폴더에서 아래 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;pip install -U discord.py python-dotenv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;기본 봇을 만든 상태라면 이미 설치되어 있을 가능성이 큽니다.&lt;br /&gt;버전이 애매하면 `-U` 옵션으로 한 번 더 올려야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. &lt;code&gt;.env&lt;/code&gt; 파일 예시&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_봇_토큰
GUILD_ID=테스트할_서버_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;처음에는 글로벌 명령어보다 특정 서버에만 등록하는 방식이 훨씬 빠릅니다.&lt;br /&gt;`GUILD_ID`를 넣어 두면 명령어 반영 속도가 빠릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. main.py 전체 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 그대로 넣으면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
GUILD_ID = int(os.getenv(&quot;GUILD_ID&quot;))

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
guild = discord.Object(id=GUILD_ID)


@client.event
async def on_ready():
    await tree.sync(guild=guild)
    print(f&quot;로그인 성공: {client.user}&quot;)
    print(&quot;슬래시 명령어 동기화 완료&quot;)


@tree.command(name=&quot;핑&quot;, description=&quot;봇 응답 속도를 확인합니다.&quot;, guild=guild)
async def ping(interaction: discord.Interaction):
    latency_ms = round(client.latency * 1000)
    await interaction.response.send_message(f&quot;퐁, 현재 지연 시간은 {latency_ms}ms 입니다.&quot;)


@tree.command(name=&quot;서버정보&quot;, description=&quot;현재 서버 정보를 보여줍니다.&quot;, guild=guild)
async def server_info(interaction: discord.Interaction):
    current_guild = interaction.guild

    if current_guild is None:
        await interaction.response.send_message(&quot;이 명령어는 서버에서만 사용할 수 있습니다.&quot;, ephemeral=True)
        return

    embed = discord.Embed(title=&quot;서버 정보&quot;, color=discord.Color.green())
    embed.add_field(name=&quot;서버 이름&quot;, value=current_guild.name, inline=False)
    embed.add_field(name=&quot;멤버 수&quot;, value=str(current_guild.member_count), inline=True)
    embed.add_field(name=&quot;채널 수&quot;, value=str(len(current_guild.channels)), inline=True)

    if current_guild.icon:
        embed.set_thumbnail(url=current_guild.icon.url)

    await interaction.response.send_message(embed=embed)


@tree.command(name=&quot;청소&quot;, description=&quot;최근 메시지를 정리합니다.&quot;, guild=guild)
@app_commands.describe(amount=&quot;삭제할 메시지 개수&quot;)
async def clear_messages(interaction: discord.Interaction, amount: int):
    if not interaction.user.guild_permissions.manage_messages:
        await interaction.response.send_message(
            &quot;메시지 관리 권한이 있는 사용자만 사용할 수 있습니다.&quot;,
            ephemeral=True,
        )
        return

    if amount &amp;lt; 1 or amount &amp;gt; 100:
        await interaction.response.send_message(
            &quot;삭제 개수는 1 이상 100 이하로 입력하세요.&quot;,
            ephemeral=True,
        )
        return

    await interaction.response.defer(ephemeral=True)
    deleted = await interaction.channel.purge(limit=amount)
    await interaction.followup.send(f&quot;메시지 {len(deleted)}개를 삭제했습니다.&quot;, ephemeral=True)


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 없습니다.&quot;)

client.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;슬래시 명령어 구조에서는 `commands.Bot` 대신 `CommandTree`를 중심으로 생각하면 됩니다.&lt;br /&gt;명령어를 등록한 뒤 `tree.sync()`를 호출해야 실제 서버에 반영됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 서버 ID 확인 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 앱에서 &lt;code&gt;사용자 설정&lt;/code&gt; -&amp;gt; &lt;code&gt;고급&lt;/code&gt; -&amp;gt; &lt;code&gt;개발자 모드&lt;/code&gt;를 켭니다.&lt;br /&gt;&lt;br /&gt;그다음 서버 아이콘을 우클릭해서 &lt;code&gt;ID 복사&lt;/code&gt;를 누르면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 글로벌 배포보다 서버 단위 테스트가 훨씬 빠릅니다.&lt;br /&gt;&lt;br /&gt;명령어가 1시간씩 안 보이는 상황을 피하기 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 테스트하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 아래 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;디스코드 채팅창에서 `/`를 입력하면 명령어 목록에 `/핑`, `/서버정보`, `/청소`가 보여야 합니다.&lt;br /&gt;안 보이면 아래 구간을 먼저 봅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초대 링크에 &lt;code&gt;applications.commands&lt;/code&gt;가 들어갔는가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tree.sync(guild=guild)&lt;/code&gt;가 실제로 호출되는가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GUILD_ID&lt;/code&gt;가 맞는가&lt;/li&gt;
&lt;li&gt;봇을 재시작했는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 자주 막히는 문제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명령어가 안 보이는 경우: 서버 ID가 틀렸거나 sync가 안 된 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;This interaction failed&lt;/code&gt;가 뜨는 경우: 응답 시간을 넘겼거나, &lt;code&gt;interaction.response&lt;/code&gt;를 제대로 처리하지 않은 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/청소&lt;/code&gt;가 실패하는 경우: 사용자 권한이나 봇 권한 둘 중 하나가 부족할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬래시 명령어 쪽은 코드보다 sync와 권한에서 많이 막힙니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 메시지 명령어와 같이 써도 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 완전히 갈아탈 필요는 없습니다.&lt;br /&gt;&lt;br /&gt;메시지 명령어 봇을 먼저 만든 뒤 슬래시 명령어를 일부 기능부터 옮겨 가는 방식도 흔합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입문 단계에서는 이 흐름이 더 편합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 다음 단계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬래시 명령어 기본형이 붙었다면 다음에는 초대 링크와 토큰 관리 쪽을 같이 맞춰야 막힘이 적습니다.&lt;br /&gt;&lt;br /&gt;바로 이어서 볼 글은 &lt;a href=&quot;https://blog.dishost.kr/55&quot;&gt;디스코드 봇 권한 설정법&lt;/a&gt;, &lt;a href=&quot;https://blog.dishost.kr/59&quot;&gt;디스코드 봇 초대 링크 생성법, 권한이 꼬이지 않게 링크 만드는 방법&lt;/a&gt;, &lt;a href=&quot;https://blog.dishost.kr/60&quot;&gt;.env 환경변수로 디스코드 토큰 숨기기, 하드코딩 없이 안전하게 관리하는 법&lt;/a&gt;입니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/61</guid>
      <comments>https://dishost.tistory.com/61#entry61comment</comments>
      <pubDate>Sat, 28 Mar 2026 16:19:12 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 봇 초대 링크 생성법, 권한이 꼬이지 않게 링크 만드는 방법</title>
      <link>https://dishost.tistory.com/59</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 봇을 만들고도 서버에 못 넣는 경우가 자주 나옵니다.&lt;br /&gt;&lt;br /&gt;봇 코드보다 초대 링크 생성 과정에서 막히는 쪽이 더 흔할 때도 있습니다.&lt;br /&gt;&lt;br /&gt;특히 권한을 너무 적게 고르거나, 아예 잘못된 스코프를 선택해서 다시 초대하는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 링크는 한 번만 대충 만들고 끝내는 작업이 아닙니다.&lt;br /&gt;&lt;br /&gt;기능이 늘어날수록 필요한 권한도 달라지기 때문에 구조를 이해해 둬야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 초대 링크를 만드는 위치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 &lt;a href=&quot;https://discord.com/developers/applications&quot;&gt;Discord Developer Portal&lt;/a&gt;에 접속합니다.&lt;br /&gt;&lt;br /&gt;봇 애플리케이션을 선택한 뒤 왼쪽 메뉴에서 &lt;code&gt;OAuth2&lt;/code&gt; -&amp;gt; &lt;code&gt;URL Generator&lt;/code&gt;로 이동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 두 가지를 고릅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 스코프를 쓸지&lt;/li&gt;
&lt;li&gt;어떤 권한을 봇에 줄지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 스코프는 보통 두 개를 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 많이 쓰는 것은 아래 두 개입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;bot&lt;/code&gt;: 일반 봇 계정을 서버에 초대할 때 필요합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;applications.commands&lt;/code&gt;: 슬래시 명령어를 사용할 때 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 명령어만 쓰는 아주 기초적인 봇이라면 &lt;code&gt;bot&lt;/code&gt;만으로도 시작할 수 있습니다.&lt;br /&gt;&lt;br /&gt;슬래시 명령어를 바로 쓸 계획이라면 둘 다 체크하는 경우가 보통입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 초반에 자주 쓰는 권한&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 권한은 대부분의 기본 봇에서 자주 씁니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;View Channels
Send Messages
Read Message History
Embed Links
Attach Files
Manage Messages
Use Slash Commands
Manage Roles&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;권한을 너무 적게 주면 서버에 들어온 뒤 기능이 바로 막힙니다.&lt;br /&gt;반대로 너무 넓게 주면 관리가 불안해집니다.&lt;br /&gt;현재 넣을 기능 기준으로만 줘야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 기본 응답 봇과 관리 봇은 링크를 나눠 생각하면 편하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;!핑&lt;/code&gt;, &lt;code&gt;!도움&lt;/code&gt; 정도만 쓰는 봇이라면 읽기와 전송 권한 위주면 충분합니다.&lt;br /&gt;&lt;br /&gt;메시지 삭제, 역할 지급, 로그 기록까지 들어가면 &lt;code&gt;Manage Messages&lt;/code&gt;, &lt;code&gt;Manage Roles&lt;/code&gt;가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 아래 정도로 생각하면 편합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답형 봇: View Channels, Send Messages, Read Message History, Embed Links&lt;/li&gt;
&lt;li&gt;관리형 봇: 응답형 권한 + Manage Messages + Manage Roles + Use Slash Commands&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 실제로 생성되는 링크 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 링크는 대략 이런 형태입니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;https://discord.com/oauth2/authorize?client_id=애플리케이션_ID&amp;amp;scope=bot%20applications.commands&amp;amp;permissions=권한값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기서 `client_id`는 애플리케이션 ID입니다.&lt;br /&gt;`permissions`는 체크한 권한 조합에 따라 숫자로 자동 계산됩니다.&lt;br /&gt;직접 숫자를 외울 필요는 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 초대가 안 되거나 서버 목록이 안 뜰 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 아래 경우가 많습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 서버에서 &lt;code&gt;서버 관리&lt;/code&gt; 권한이 없는 계정으로 시도한 경우&lt;/li&gt;
&lt;li&gt;잘못된 애플리케이션으로 링크를 만든 경우&lt;/li&gt;
&lt;li&gt;브라우저에 다른 디스코드 계정이 로그인되어 있는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 페이지에서 서버 선택 목록이 비어 있으면 계정 권한부터 확인해야 빠릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 초대 후에도 기능이 안 될 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 들어왔다고 끝난 것은 아닙니다.&lt;br /&gt;&lt;br /&gt;권한이 충분하지 않거나 채널에서 막히면 기능은 여전히 안 돌아갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때는 아래를 같이 봅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초대 링크에 필요한 권한이 포함되어 있었는가&lt;/li&gt;
&lt;li&gt;봇 역할이 채널에서 차단되지 않았는가&lt;/li&gt;
&lt;li&gt;봇 역할이 대상 역할보다 위에 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대는 성공했는데 역할 지급이 안 되면 대부분 세 번째 문제입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 나중에 권한이 바뀌면 어떻게 하나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇 기능을 늘리다가 새 권한이 필요해질 수 있습니다.&lt;br /&gt;&lt;br /&gt;그럴 때는 초대 링크를 다시 만들어서 서버에 다시 넣거나 권한을 재승인하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 링크를 너무 대충 만들면 안 되는 이유가 여기 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 운영에서 안전한 습관&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 링크는 아무 데나 뿌리면 안 됩니다.&lt;br /&gt;&lt;br /&gt;테스트 봇과 운영 봇이 섞이면 엉뚱한 봇을 서버에 넣는 경우도 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 이름, 아이콘, 설명을 구분해 두면 실수를 줄이기 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 다음 단계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 봇이 이미 있다면 초대 링크를 만들고 바로 서버에서 테스트해 보면 됩니다.&lt;br /&gt;&lt;br /&gt;권한 구조를 아직 정리하지 않았다면 &lt;a href=&quot;https://blog.dishost.kr/55&quot;&gt;디스코드 봇 권한 설정법&lt;/a&gt;과 &lt;a href=&quot;https://blog.dishost.kr/58&quot;&gt;디스코드 역할 설정 완벽 가이드&lt;/a&gt;를 같이 보면서 맞춰야 안전합니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/59</guid>
      <comments>https://dishost.tistory.com/59#entry59comment</comments>
      <pubDate>Thu, 26 Mar 2026 16:16:36 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 역할 설정 완벽 가이드, 관리자 운영진 멤버 봇 역할을 처음부터 정리하는 법</title>
      <link>https://dishost.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 서버에서 권한 문제는 결국 역할 구조로 돌아옵니다.&lt;br /&gt;&lt;br /&gt;역할 이름만 그럴듯하게 만들고 순서와 권한을 대충 두면 나중에 봇도 막히고 운영진도 헷갈립니다.&lt;br /&gt;&lt;br /&gt;초반 역할 설계는 적게, 명확하게, 위계가 보이게 잡아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반 서버는 화려한 역할보다 설명 가능한 역할 구조가 먼저입니다.&lt;br /&gt;&lt;br /&gt;서버 규모가 커져도 기본 뼈대는 거의 그대로 가져갈 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 역할은 많을수록 좋은 게 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 역할을 열 개 넘게 만드는 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;대부분은 멋있어 보이기만 하고 실제 운영에서는 오히려 혼란만 커집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 역할은 아래 정도면 충분합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;관리자
운영진
봇
인증됨
멤버&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 다섯 개만으로도 대부분의 입문 서버는 충분히 굴러갑니다.&lt;br /&gt;중요한 건 숫자가 아니라 역할별 책임이 겹치지 않는 구조입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &lt;code&gt;관리자&lt;/code&gt;와 &lt;code&gt;운영진&lt;/code&gt;은 나눠야 안전하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 운영 인원에게 &lt;code&gt;Administrator&lt;/code&gt; 권한을 주면 관리가 편해 보입니다.&lt;br /&gt;&lt;br /&gt;실제로는 실수 한 번의 영향 범위가 너무 커집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 아래처럼 나눕니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;관리자&lt;/code&gt;:&lt;br /&gt;&lt;br /&gt;전체 설정 변경, 역할 수정, 채널 구조 변경까지 가능한 역할&lt;/li&gt;
&lt;li&gt;&lt;code&gt;운영진&lt;/code&gt;:&lt;br /&gt;&lt;br /&gt;메시지 관리, 제재, 문의 대응처럼 일상 운영에 필요한 권한만 가진 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할을 나누면 누가 어디까지 건드릴 수 있는지가 훨씬 선명해집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 봇 역할은 항상 대상 역할보다 위에 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 지급, 닉네임 변경, 채널 관리 같은 작업을 봇에게 시킬 계획이라면 봇 역할 위치가 중요합니다.&lt;br /&gt;&lt;br /&gt;봇 역할이 대상 역할보다 아래에 있으면 기능이 막힙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안전한 기본 구조는 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;관리자
운영진
봇
인증됨
멤버&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;봇이 `멤버`나 `인증됨` 역할을 주게 만들 계획이라면 이 순서가 편합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 역할 이름보다 더 중요한 것은 역할 목적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할은 장식이 아니라 기능 단위로 봐야 합니다.&lt;br /&gt;&lt;br /&gt;예를 들면 아래처럼 나눌 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;접근 역할:&lt;br /&gt;&lt;br /&gt;인증됨, 멤버, 신규유저&lt;/li&gt;
&lt;li&gt;운영 역할:&lt;br /&gt;&lt;br /&gt;운영진, 관리자&lt;/li&gt;
&lt;li&gt;기능 역할:&lt;br /&gt;&lt;br /&gt;봇, 알림수신, 이벤트참여&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 목적별로 나누면 어떤 역할을 왜 만들었는지 설명하기 쉬워집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 색상 역할은 초반에는 최소화한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 색상은 서버를 보기 좋게 만들 수 있습니다.&lt;br /&gt;&lt;br /&gt;그런데 색상 역할을 너무 많이 만들면 이름만 다르고 기능은 없는 역할이 늘어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 운영진 계열 색상만 구분하고, 일반 유저 역할은 기능 중심으로 두면 됩니다.&lt;br /&gt;&lt;br /&gt;유저 꾸미기 역할은 활동량이 어느 정도 붙은 뒤 추가해도 늦지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 역할 권한은 필요 최소한만 준다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할을 만들 때 가장 위험한 습관은 일단 많이 켜 두는 것입니다.&lt;br /&gt;&lt;br /&gt;초반에는 아래처럼 필요한 것만 주면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;운영진
- Manage Messages
- Timeout Members
- Kick Members

관리자
- Administrator 또는 세부 관리 권한

봇
- Send Messages
- Manage Messages
- Manage Roles
- Read Message History&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;특히 `Administrator` 권한은 정말 필요한 역할에만 줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 역할 배치 체크리스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할을 만든 뒤에는 아래만 확인해도 큰 사고를 많이 줄일 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;봇 역할이 지급 대상 역할보다 위에 있는가&lt;/li&gt;
&lt;li&gt;운영진 역할에 불필요한 관리자 권한이 들어가 있지 않은가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@everyone&lt;/code&gt; 권한이 너무 넓지 않은가&lt;/li&gt;
&lt;li&gt;신규 유저와 인증된 유저의 차이가 실제로 채널 노출에 반영되는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 화면에서 체크만 하지 말고 실제 일반 계정으로 들어가 직접 확인해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 자주 하는 실수&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;역할 이름만 그럴듯하고 실제 목적이 없는 경우&lt;/li&gt;
&lt;li&gt;봇 역할이 아래에 있어서 역할 지급이 안 되는 경우&lt;/li&gt;
&lt;li&gt;운영진에게 관리자 권한을 너무 넓게 준 경우&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@everyone&lt;/code&gt; 권한을 방치해서 사실상 모든 역할이 무의미한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 서버는 구조보다 운영 속도가 빠르게 바뀝니다.&lt;br /&gt;&lt;br /&gt;그래서 역할은 화려함보다 수정하기 쉬운 구조가 더 중요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 역할 구조가 정리된 뒤 좋은 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 구조가 안정되면 자동 역할, 반응 역할, 인증 봇 같은 기능을 붙이기 쉬워집니다.&lt;br /&gt;&lt;br /&gt;반대로 역할 구조가 엉킨 상태에서는 자동화 기능을 붙일수록 더 헷갈립니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 역할표를 짰다면 권한 테스트로 끝낸다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 구조를 정리했다면 봇 초대 링크와 권한을 맞춰서 실제 서버에 투입할 차례입니다.&lt;br /&gt;&lt;br /&gt;기본 봇이 없다면 &lt;a href=&quot;https://blog.dishost.kr/53&quot;&gt;파이썬 디스코드 봇 만들기 처음부터 끝까지&lt;/a&gt;부터, 초대 링크 구조를 먼저 정리할 거라면 다음 글 흐름으로 넘어가면 됩니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/58</guid>
      <comments>https://dishost.tistory.com/58#entry58comment</comments>
      <pubDate>Wed, 25 Mar 2026 16:15:57 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 채널 권한 설정법, 공지 채널과 관리자 채널이 꼬이지 않게 만드는 법</title>
      <link>https://dishost.tistory.com/57</link>
      <description>&lt;p&gt;디스코드 서버를 만들고 나면 채널은 금방 늘어납니다.&lt;br&gt;&lt;br&gt;문제는 채널 수보다 권한입니다.&lt;br&gt;&lt;br&gt;공지 채널에 일반 유저가 글을 쓰거나, 관리자 채널이 보이면 서버 구조가 바로 흐트러집니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;채널 권한은 한 번에 복잡하게 잡기보다 기준 패턴 몇 개를 정하면 훨씬 쉽습니다.&lt;br&gt;&lt;br&gt;공지 채널, 관리자 채널, 문의 채널, 봇 명령어 채널을 기준으로 가장 덜 꼬이는 배치를 먼저 잡아 봅니다.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;1. 채널 권한은 서버 권한 위에 덮어쓴다&lt;/h3&gt;
&lt;p&gt;서버 역할에서 권한을 켜 두어도 채널에서 막으면 접근이 제한됩니다.&lt;br&gt;&lt;br&gt;반대로 서버 역할에서 아예 없는 권한은 채널에서 살릴 수 없습니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;정리하면 아래 순서로 보면 됩니다.&lt;br&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;서버 역할 권한 설정
-&amp;gt; 채널별 권한 덮어쓰기
-&amp;gt; 역할 순서와 실제 노출 결과 확인&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
채널 권한이 꼬일 때는 채널 설정만 보지 말고 서버 역할 권한도 같이 확인해야 빠릅니다.&lt;br&gt;

&lt;h3&gt;2. 공지 채널은 읽기 전용으로 두는 게 보통&lt;/h3&gt;
&lt;p&gt;공지 채널은 대부분 아래 구조로 잡습니다.&lt;br&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;@everyone
- View Channel: 허용
- Send Messages: 차단

운영진
- View Channel: 허용
- Send Messages: 허용

봇
- View Channel: 허용
- Send Messages: 허용&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
여기서 봇 권한을 빼먹는 경우가 생각보다 많습니다.&lt;br&gt;
자동 공지나 로그를 보내는 봇이 있다면 봇 역할도 같이 허용해야 합니다.&lt;br&gt;

&lt;h3&gt;3. 관리자 채널은 &lt;code&gt;@everyone&lt;/code&gt;부터 막는다&lt;/h3&gt;
&lt;p&gt;관리자 채널은 특정 역할만 보게 만드는 구조가 핵심입니다.&lt;br&gt;&lt;br&gt;가장 안전한 방식은 &lt;code&gt;@everyone&lt;/code&gt;에서 &lt;code&gt;View Channel&lt;/code&gt;을 끄고, 필요한 역할만 다시 허용하는 방식입니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;예시는 아래처럼 잡습니다.&lt;br&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;@everyone
- View Channel: 차단

관리자
- View Channel: 허용
- Send Messages: 허용

운영진
- View Channel: 허용
- Send Messages: 허용&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
이 구조면 일반 유저는 채널 존재 자체를 보지 못합니다.&lt;br&gt;
운영 채널은 보이기만 해도 구조가 노출되므로 이 방식이 가장 깔끔합니다.&lt;br&gt;

&lt;h3&gt;4. 문의 채널과 봇 명령어 채널은 목적이 다르다&lt;/h3&gt;
&lt;p&gt;문의 채널은 유저가 글을 쓸 수 있어야 하고, 봇 명령어 채널은 봇 응답이 정상적으로 보여야 합니다.&lt;br&gt;&lt;br&gt;둘을 같은 패턴으로 잡으면 불편해집니다.&lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;문의 채널:&lt;br&gt;&lt;br&gt;유저 메시지 허용, 운영진 보기 허용, 봇 메시지 허용&lt;br&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;봇 명령어 채널:&lt;br&gt;&lt;br&gt;일반 유저 메시지 허용, 봇 응답 허용, 필요하면 파일 첨부와 링크 임베드 허용&lt;br&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;특히 임베드 메시지를 쓰는 봇은 &lt;code&gt;Embed Links&lt;/code&gt; 권한이 빠지면 메시지가 빈약하게 보일 수 있습니다.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;5. 카테고리 권한을 먼저 잡고 채널은 예외만 둔다&lt;/h3&gt;
&lt;p&gt;채널마다 권한을 전부 따로 잡기 시작하면 곧 관리가 어려워집니다.&lt;br&gt;&lt;br&gt;카테고리 권한을 먼저 잡고, 채널에서는 정말 필요한 예외만 둬야 합니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;운영&lt;/code&gt; 카테고리를 아래처럼 두면 됩니다.&lt;br&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;운영 카테고리
@everyone: View Channel 차단
운영진: 허용
관리자: 허용
봇: 허용&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
그 아래 `로그`, `문의관리`, `내부공지` 채널을 만들면 기본값이 깔려 있으니 수정량이 줄어듭니다.&lt;br&gt;

&lt;h3&gt;6. 권한 테스트는 관리자 계정이 아니라 일반 계정으로 한다&lt;/h3&gt;
&lt;p&gt;운영자는 대부분 강한 권한을 갖고 있어서 잘못 설정해도 문제를 놓치기 쉽습니다.&lt;br&gt;&lt;br&gt;가능하면 테스트용 일반 계정이나 친구 계정으로 확인해야 합니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;최소한 아래는 직접 봅니다.&lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;공지 채널에 일반 멤버가 글을 쓸 수 없는가&lt;br&gt;&lt;/li&gt;
&lt;li&gt;관리자 채널이 일반 멤버에게 아예 안 보이는가&lt;/li&gt;
&lt;li&gt;문의 채널과 봇 명령어 채널은 정상적으로 보이는가&lt;/li&gt;
&lt;li&gt;봇이 공지 채널이나 로그 채널에 메시지를 보낼 수 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7. 자주 생기는 문제&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;채널은 보이는데 메시지를 못 쓰는 경우:&lt;br&gt;&lt;br&gt;&lt;code&gt;Send Messages&lt;/code&gt;가 꺼져 있는 상태입니다.&lt;br&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;채널 자체가 안 보이는 경우:&lt;br&gt;&lt;br&gt;&lt;code&gt;View Channel&lt;/code&gt;이 막혀 있거나 상위 카테고리 권한이 덮여 있을 가능성이 큽니다.&lt;br&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;봇만 특정 채널에서 반응이 없는 경우:&lt;br&gt;&lt;br&gt;봇 역할 채널 권한이 빠졌거나, 봇 초대 링크 권한이 처음부터 부족했을 가능성이 큽니다.&lt;br&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8. 권한을 너무 세밀하게 쪼개지 말 것&lt;/h3&gt;
&lt;p&gt;초반 서버에서 역할과 채널 권한을 지나치게 세밀하게 나누면 운영자도 헷갈립니다.&lt;br&gt;&lt;br&gt;처음에는 공지, 커뮤니티, 운영 정도만 확실히 나누고, 실제 활동이 쌓일 때 세분화해야 합니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;권한은 정교함보다 예측 가능성이 중요합니다.&lt;br&gt;&lt;br&gt;누가 어디를 볼 수 있는지 운영진이 바로 설명할 수 있어야 합니다.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;9. 다음 단계&lt;/h3&gt;
&lt;p&gt;채널 권한을 정리했다면 역할 구조를 같이 손봐야 꼬이지 않습니다.&lt;br&gt;&lt;br&gt;역할 이름은 괜찮은데 우선순위와 실제 기능이 뒤죽박죽인 경우가 흔합니다.&lt;br&gt;&lt;br&gt;서버 역할 구조부터 다시 점검하려면 역할 순서와 목적을 먼저 표로 적어 봐야 빠릅니다.&lt;br&gt;&lt;br&gt;기본 봇을 이미 만들어 둔 상태라면 &lt;a href=&quot;https://blog.dishost.kr/53&quot;&gt;파이썬 디스코드 봇 만들기 처음부터 끝까지&lt;/a&gt;와 &lt;a href=&quot;https://blog.dishost.kr/55&quot;&gt;디스코드 봇 권한 설정법&lt;/a&gt;을 같이 열어 두고 채널별로 하나씩 테스트하면 됩니다.&lt;br&gt;&lt;br&gt;이 작업을 해 두면 뒤에서 티켓 봇이나 로그 봇을 붙일 때 훨씬 수월합니다.&lt;br&gt;&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/57</guid>
      <comments>https://dishost.tistory.com/57#entry57comment</comments>
      <pubDate>Tue, 24 Mar 2026 16:15:28 +0900</pubDate>
    </item>
    <item>
      <title>Message Content Intent 설정법, 디스코드 봇이 명령어를 읽지 못할 때 먼저 볼 것</title>
      <link>https://dishost.tistory.com/56</link>
      <description>&lt;p&gt;봇이 온라인으로 보이는데 &lt;code&gt;!핑&lt;/code&gt;에 아무 반응도 없으면 코드부터 의심하게 됩니다.&lt;br&gt;&lt;br&gt;그런데 초반에는 코드보다 &lt;code&gt;Message Content Intent&lt;/code&gt; 설정이 빠진 경우가 훨씬 많습니다.&lt;br&gt;&lt;br&gt;이 옵션 하나 때문에 명령어가 통째로 먹히지 않는 경우가 자주 나옵니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;접두사 명령어 기반 봇이라면 거의 반드시 확인해야 하는 설정입니다.&lt;br&gt;&lt;br&gt;한 번만 구조를 이해해 두면 같은 문제로 다시 멈출 일이 크게 줄어듭니다.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;1. Message Content Intent가 하는 일&lt;/h3&gt;
&lt;p&gt;봇이 일반 메시지 내용을 읽을 수 있게 허용하는 옵션입니다.&lt;br&gt;&lt;br&gt;예를 들어 &lt;code&gt;!핑&lt;/code&gt;, &lt;code&gt;!청소&lt;/code&gt;, &lt;code&gt;!도움&lt;/code&gt;처럼 채팅창에 직접 입력하는 메시지 명령어는 이 권한이 있어야 읽을 수 있습니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;반대로 슬래시 명령어는 디스코드가 별도 인터랙션 형태로 전달하므로 항상 이 설정이 필요한 것은 아닙니다.&lt;br&gt;&lt;br&gt;그래도 메시지 기반 기능이 하나라도 들어가면 먼저 켜 둬야 안전합니다.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;2. 어떤 경우에 꼭 필요한가&lt;/h3&gt;
&lt;p&gt;아래 유형이면 거의 필요합니다.&lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;!핑&lt;/code&gt;, &lt;code&gt;?help&lt;/code&gt; 같은 접두사 명령어 사용&lt;br&gt;&lt;/li&gt;
&lt;li&gt;특정 단어 감지 후 자동 답장&lt;br&gt;&lt;/li&gt;
&lt;li&gt;욕설 필터, 키워드 감지, 자동 응답 기능&lt;br&gt;&lt;/li&gt;
&lt;li&gt;유저가 직접 입력한 일반 메시지를 분석하는 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;봇이 단순히 슬래시 명령어만 받고 일반 채팅 내용을 읽지 않는 구조라면 없어도 될 수 있습니다.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;3. Discord Developer Portal에서 켜는 위치&lt;/h3&gt;
&lt;p&gt;브라우저에서 &lt;a href=&quot;https://discord.com/developers/applications&quot;&gt;Discord Developer Portal&lt;/a&gt;에 접속합니다.&lt;br&gt;&lt;br&gt;애플리케이션을 선택한 뒤 왼쪽의 &lt;code&gt;Bot&lt;/code&gt; 메뉴로 들어갑니다.&lt;br&gt;&lt;br&gt;아래로 내리면 &lt;code&gt;Privileged Gateway Intents&lt;/code&gt; 구간이 보입니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;Message Content Intent&lt;/code&gt;를 켭니다.&lt;br&gt;&lt;br&gt;저장 버튼이 보이면 저장까지 눌러야 합니다.&lt;br&gt;&lt;br&gt;생각보다 여기서 저장을 안 누르고 닫는 경우가 많습니다.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;4. 코드 쪽에서도 같이 켜야 한다&lt;/h3&gt;
&lt;p&gt;Portal에서만 켜고 코드에서 인텐트를 열지 않으면 여전히 명령어가 안 읽힐 수 있습니다.&lt;br&gt;&lt;br&gt;둘 다 맞아야 합니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;discord.py&lt;/code&gt; 예시는 아래처럼 잡습니다.&lt;br&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import discord
from discord.ext import commands

intents = discord.Intents.default()
intents.message_content = True

bot = commands.Bot(command_prefix=&amp;quot;!&amp;quot;, intents=intents)&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
핵심은 `intents.message_content = True` 한 줄입니다.&lt;br&gt;
이 줄이 없으면 Portal에서 옵션을 켜도 코드에서 메시지 내용을 받지 못합니다.&lt;br&gt;

&lt;h3&gt;5. 빠르게 테스트하는 방법&lt;/h3&gt;
&lt;p&gt;가장 단순한 핑 명령어로 먼저 확인해야 합니다.&lt;br&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@bot.command(name=&amp;quot;핑&amp;quot;)
async def ping(ctx):
    await ctx.send(&amp;quot;퐁&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
이 코드를 넣고 실행한 뒤 채널에서 `!핑`을 입력합니다.&lt;br&gt;
반응이 없으면 아래 순서대로 봅니다.&lt;br&gt;

&lt;ul&gt;
&lt;li&gt;Portal에서 &lt;code&gt;Message Content Intent&lt;/code&gt;를 켰는가&lt;br&gt;&lt;/li&gt;
&lt;li&gt;코드에서 &lt;code&gt;intents.message_content = True&lt;/code&gt;를 넣었는가&lt;br&gt;&lt;/li&gt;
&lt;li&gt;봇을 재시작했는가&lt;br&gt;&lt;/li&gt;
&lt;li&gt;접두사 문자를 실제 입력과 똑같이 맞췄는가&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. 자주 헷갈리는 포인트&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Portal만 켜고 코드에서 안 켠 경우&lt;br&gt;&lt;/li&gt;
&lt;li&gt;코드만 켜고 Portal에서 안 켠 경우&lt;br&gt;&lt;/li&gt;
&lt;li&gt;설정을 바꾼 뒤 봇을 재시작하지 않은 경우&lt;br&gt;&lt;/li&gt;
&lt;li&gt;슬래시 명령어와 메시지 명령어 차이를 섞어서 이해한 경우&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;가장 흔한 패턴은 첫 번째와 두 번째입니다.&lt;br&gt;&lt;br&gt;둘 중 하나만 맞으면 될 것 같지만 실제로는 둘 다 맞아야 합니다.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;7. 권한 문제와 혼동하지 말 것&lt;/h3&gt;
&lt;p&gt;Message Content Intent가 빠지면 봇은 메시지를 읽지 못합니다.&lt;br&gt;&lt;br&gt;채널 권한이 막힌 경우는 메시지를 읽거나 보내는 범위 자체가 제한됩니다.&lt;br&gt;&lt;br&gt;겉으로 보기에는 둘 다 &lt;code&gt;봇이 조용하다&lt;/code&gt;로 보이기 때문에 헷갈리기 쉽습니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;권한 구조까지 같이 확인하려면 &lt;a href=&quot;https://blog.dishost.kr/55&quot;&gt;디스코드 봇 권한 설정법&lt;/a&gt;을 같이 보면 됩니다.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;8. 슬래시 명령어만 쓸 거라면&lt;/h3&gt;
&lt;p&gt;슬래시 명령어 구조만 쓴다면 &lt;code&gt;Message Content Intent&lt;/code&gt; 의존도는 낮아집니다.&lt;br&gt;&lt;br&gt;다만 입문 단계에서는 메시지 명령어와 슬래시 명령어를 같이 테스트하는 경우가 많아서 미리 이해해 둬야 합니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;실제로 처음 봇을 만들 때는 &lt;code&gt;!핑&lt;/code&gt;으로 확인하고, 나중에 슬래시 명령어로 넘어가는 흐름이 가장 편합니다.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;9. 이런 로그가 보이면 다시 확인&lt;/h3&gt;
&lt;p&gt;터미널에 에러가 아예 없는데도 반응이 없는 경우가 있습니다.&lt;br&gt;&lt;br&gt;이럴 때는 오히려 Intent 설정 문제일 가능성이 큽니다.&lt;br&gt;&lt;br&gt;코드는 돌아가지만 이벤트가 안 들어오는 상태라고 보면 됩니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;이 문제를 먼저 정리해 두면 뒤에서 ChatGPT 기능이나 자동 응답 기능을 붙일 때 덜 막힙니다.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;10. 다음 단계&lt;/h3&gt;
&lt;p&gt;기본 봇 코드가 아직 없다면 &lt;a href=&quot;https://blog.dishost.kr/53&quot;&gt;파이썬 디스코드 봇 만들기 처음부터 끝까지&lt;/a&gt;부터 먼저 맞춰 둬야 합니다.&lt;br&gt;&lt;br&gt;슬래시 명령어 구조로 바꿀 계획이라면 그다음 글에서 바로 연결하면 됩니다.&lt;br&gt;&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/56</guid>
      <comments>https://dishost.tistory.com/56#entry56comment</comments>
      <pubDate>Mon, 23 Mar 2026 16:15:01 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 서버 만드는 법 처음부터 끝까지, 채널 역할 기본 세팅 가이드</title>
      <link>https://dishost.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 서버를 처음 만들면 채널부터 늘어놓기 쉽습니다.&lt;br /&gt;&lt;br /&gt;그렇게 시작하면 며칠 뒤 역할 권한이 꼬이고, 공지 채널과 잡담 채널이 섞이고, 봇을 넣은 뒤에도 구조를 다시 뜯어고치게 됩니다.&lt;br /&gt;&lt;br /&gt;처음 세팅에서 큰 틀만 제대로 잡아도 이후 운영 난이도가 꽤 내려갑니다.&lt;br /&gt;&lt;br /&gt;봇까지 바로 붙일 계획이라면 &lt;a href=&quot;https://blog.dishost.kr/53&quot;&gt;파이썬 디스코드 봇 만들기 처음부터 끝까지&lt;/a&gt;를 이어서 보면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 서버부터 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 앱이나 웹에서 왼쪽 사이드바의 &lt;code&gt;+&lt;/code&gt; 버튼을 누릅니다.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;서버 만들기&lt;/code&gt;를 선택한 뒤 서버 이름과 아이콘을 설정합니다.&lt;br /&gt;&lt;br /&gt;아이콘은 나중에 바꿔도 되지만, 이름은 초반에 방향을 어느 정도 정해 둬야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 &lt;code&gt;커뮤니티 서버&lt;/code&gt; 기능을 켜는 것은 서두르지 않아도 됩니다.&lt;br /&gt;&lt;br /&gt;일반 서버 상태로 구조를 먼저 잡고, 공지 채널과 규칙 채널이 준비된 뒤 커뮤니티 기능을 켜야 덜 꼬입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 채널을 만들기 전에 카테고리부터 나누기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널을 바로 여러 개 만드는 방식보다 카테고리부터 먼저 정하는 방식이 관리가 쉽습니다.&lt;br /&gt;&lt;br /&gt;초반 서버라면 아래 정도만 있어도 충분합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;  시작하기
├─ 규칙
├─ 공지
└─ 역할선택

  커뮤니티
├─ 잡담
├─ 질문
└─ 봇명령어

  운영
├─ 관리자전용
├─ 로그
└─ 문의&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;핵심은 유저가 처음 들어왔을 때 어디를 먼저 봐야 하는지가 바로 보이게 만드는 것입니다.&lt;br /&gt;규칙, 공지, 역할선택이 맨 위에 고정되어 있으면 서버 첫인상이 훨씬 정돈되어 보입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 처음부터 만들어 두면 좋은 채널&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 채널은 대부분의 서버에서 초반부터 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;규칙&lt;/code&gt;:&lt;br /&gt;&lt;br /&gt;서버 규칙, 금지 항목, 제재 기준을 적는 채널입니다.&lt;br /&gt;&lt;br /&gt;나중에 인증 봇이나 규칙 동의 시스템을 붙일 때 기준 채널로 쓰이기도 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;공지&lt;/code&gt;:&lt;br /&gt;&lt;br /&gt;업데이트, 이벤트, 운영 안내를 올리는 채널입니다.&lt;br /&gt;&lt;br /&gt;일반 멤버가 채팅을 치지 못하게 읽기 전용으로 두는 경우가 많습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;잡담&lt;/code&gt;:&lt;br /&gt;&lt;br /&gt;가장 많이 쓰는 기본 채널입니다.&lt;br /&gt;&lt;br /&gt;초반 서버 활동량은 거의 여기서 결정됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;봇명령어&lt;/code&gt;:&lt;br /&gt;&lt;br /&gt;봇 테스트나 명령어 실행을 따로 모으는 채널입니다.&lt;br /&gt;&lt;br /&gt;잡담 채널이 봇 로그로 도배되는 상황을 막기 쉽습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;관리자전용&lt;/code&gt;:&lt;br /&gt;&lt;br /&gt;운영진끼리만 보는 채널입니다.&lt;br /&gt;&lt;br /&gt;권한 테스트, 공지 초안, 제재 기록 메모를 남길 때 편합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 역할은 적게 시작해야 덜 꼬인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 역할을 열 개 넘게 만드는 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;대부분은 초반 활동량보다 역할 수가 더 많아지고, 권한 구조도 복잡해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 역할은 아래 정도로 시작하면 충분합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;관리자
운영진
봇
인증됨
멤버&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`관리자`와 `운영진`은 분리해야 합니다.&lt;br /&gt;모든 운영진에게 `Administrator` 권한을 주면 실수 한 번에 채널 전체 설정이 바뀔 수 있습니다.&lt;br /&gt;웬만하면 필요한 권한만 개별적으로 체크하는 방식이 안전합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 권한 구조는 이렇게 잡으면 덜 꼬인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한은 &lt;code&gt;@everyone&lt;/code&gt;을 기준으로 막고, 필요한 역할에만 푸는 식으로 설계해야 관리가 쉽습니다.&lt;br /&gt;&lt;br /&gt;예를 들어 &lt;code&gt;관리자전용&lt;/code&gt; 채널은 &lt;code&gt;@everyone&lt;/code&gt;에서 보기 권한을 끄고, &lt;code&gt;관리자&lt;/code&gt;와 &lt;code&gt;운영진&lt;/code&gt; 역할만 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공지 채널은 아래처럼 잡으면 무난합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@everyone: 보기 허용 / 메시지 전송 차단
운영진: 보기 허용 / 메시지 전송 허용
관리자: 보기 허용 / 메시지 전송 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;봇을 넣을 계획이라면 봇 역할도 공지 채널과 로그 채널을 볼 수 있게 풀어 둬야 합니다.&lt;br /&gt;이 부분이 빠지면 봇은 온라인인데도 특정 채널에서만 조용해집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 초대 링크는 무기한 하나, 임시 링크는 따로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 만들고 나면 초대 링크부터 여기저기 뿌리게 됩니다.&lt;br /&gt;&lt;br /&gt;무기한 기본 링크 하나와, 이벤트용 임시 링크를 분리해 두면 나중에 추적하기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 링크를 만들 때는 아래처럼 설정하면 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 링크:&lt;br /&gt;&lt;br /&gt;만료 없음, 사용 횟수 제한 없음&lt;/li&gt;
&lt;li&gt;이벤트 링크:&lt;br /&gt;&lt;br /&gt;7일 만료 또는 100회 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나눠 두면 어떤 경로로 유저가 들어오는지 구분하기 쉽습니다.&lt;br /&gt;&lt;br /&gt;초대 링크 관리가 필요해지면 뒤에서 초대 추적 봇을 붙이는 흐름으로 이어가면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 서버를 만든 직후 바로 점검할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 구조를 대충 만든 뒤 바로 사람을 받기 시작하면 꼭 수정이 생깁니다.&lt;br /&gt;&lt;br /&gt;초반 점검은 아래 정도만 해도 충분합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새 계정으로 들어왔을 때 &lt;code&gt;규칙&lt;/code&gt;, &lt;code&gt;공지&lt;/code&gt;, &lt;code&gt;잡담&lt;/code&gt;이 정상적으로 보이는가&lt;/li&gt;
&lt;li&gt;일반 멤버가 공지 채널에 글을 쓰지 못하는가&lt;/li&gt;
&lt;li&gt;운영진만 관리자전용 채널을 볼 수 있는가&lt;/li&gt;
&lt;li&gt;봇을 넣었을 때 &lt;code&gt;봇명령어&lt;/code&gt; 채널에 메시지를 보낼 수 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사용자 시점으로 한 번 들어가 보는 게 가장 빠릅니다.&lt;br /&gt;&lt;br /&gt;운영자 계정으로만 보면 권한 문제를 놓치기 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 처음 많이 하는 실수&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채널을 너무 많이 만드는 실수:&lt;br /&gt;&lt;br /&gt;활동량이 적은 초반에는 채널 수보다 대화 밀도가 더 중요합니다.&lt;br /&gt;&lt;br /&gt;사람이 적은데 채널만 많으면 서버가 비어 보입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Administrator&lt;/code&gt; 권한을 남발하는 실수:&lt;br /&gt;&lt;br /&gt;관리 편하다고 다 열어 두면 나중에 누가 뭘 바꿨는지 추적하기도 어렵습니다.&lt;br /&gt;&lt;br /&gt;필요 권한만 따로 줘야 안전합니다.&lt;/li&gt;
&lt;li&gt;규칙과 공지가 아래로 묻히는 실수:&lt;br /&gt;&lt;br /&gt;새로 들어온 유저는 맨 위에서 서버 분위기를 판단합니다.&lt;br /&gt;&lt;br /&gt;필수 채널을 위쪽에 고정해 두는 게 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 여기서 한 번 더 점검할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 구조를 만들었다면 다음에는 봇을 붙이거나 권한 구조를 더 정교하게 다듬는 쪽으로 넘어가면 됩니다.&lt;br /&gt;&lt;br /&gt;특히 봇을 넣을 생각이라면 권한 구조부터 먼저 이해해야 안전합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 24시간 운영까지 생각한다면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 채널만 정리한다고 운영이 끝나는 것은 아닙니다.&lt;br /&gt;&lt;br /&gt;자동 공지, 인증, 환영 메시지, 티켓 같은 기능은 결국 봇이 맡게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇을 직접 만들 계획이라면 &lt;a href=&quot;https://blog.dishost.kr/53&quot;&gt;파이썬 디스코드 봇 만들기 처음부터 끝까지&lt;/a&gt;에서 바로 이어가면 됩니다.&lt;br /&gt;&lt;br /&gt;완성한 봇을 24시간 돌릴 때는 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt; 글처럼 별도 서버에 올려 두는 흐름이 필요합니다.&lt;/p&gt;</description>
      <category>디스코드 서버 운영</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/54</guid>
      <comments>https://dishost.tistory.com/54#entry54comment</comments>
      <pubDate>Sat, 21 Mar 2026 16:12:48 +0900</pubDate>
    </item>
    <item>
      <title>파이썬 디스코드 봇 만들기 처음부터 끝까지, discord.py 입문 가이드</title>
      <link>https://dishost.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬으로 디스코드 봇을 처음 만들 때 가장 많이 막히는 구간은 코드보다 세팅 쪽입니다.&lt;br /&gt;&lt;br /&gt;Developer Portal에서 봇 생성, 토큰 보관, 권한과 인텐트 설정, 서버 초대까지 흐름을 한 번에 잡아야 곧바로 실행으로 넘어갈 수 있습니다.&lt;br /&gt;&lt;br /&gt;디스코드 봇 개발의 전 과정을 한 번에 끝내는 입문 가이드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;discord.py&lt;/code&gt; 라이브러리로 기본 명령어가 동작하는 파이썬 디스코드 봇 하나를 바로 만들어 보겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;!핑&lt;/code&gt;, &lt;code&gt;!서버정보&lt;/code&gt;, &lt;code&gt;!청소&lt;/code&gt; 명령어까지 넣어 두었기 때문에 파일만 만들고 실행하면 즉시 테스트 가능합니다.&lt;br /&gt;&lt;br /&gt;기본 구조를 익힌 뒤에는 AI 기능 붙이기, 권한 구조 정리, 슬래시 명령어 전환 순서로 넓혀 가면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 먼저 준비할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 전에 파이썬 3.10 이상, 디스코드 계정, 코드를 작성할 폴더 하나를 준비합니다.&lt;br /&gt;&lt;br /&gt;운영체제가 macOS든 Windows든 흐름은 거의 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과물 구조는 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;my-discord-bot/
├─ .env
└─ main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`.env`는 토큰을 숨기는 파일, `main.py`는 실제 봇 코드가 들어가는 파일입니다.&lt;br /&gt;토큰을 코드에 그대로 적으면 나중에 깃허브 업로드나 화면 공유 중에 유출될 수 있으니 처음부터 분리하는 편이 안전합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Discord Developer Portal에서 봇 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 &lt;a href=&quot;https://discord.com/developers/applications&quot;&gt;Discord Developer Portal&lt;/a&gt;에 접속합니다.&lt;br /&gt;&lt;br /&gt;오른쪽 위의 &lt;code&gt;New Application&lt;/code&gt; 버튼을 누르고 봇 이름을 입력한 뒤 애플리케이션을 생성합니다.&lt;br /&gt;&lt;br /&gt;생성을 마쳤다면 왼쪽 메뉴에서 &lt;code&gt;Bot&lt;/code&gt; 탭으로 이동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;Reset Token&lt;/code&gt; 또는 &lt;code&gt;Copy&lt;/code&gt; 버튼으로 봇 토큰을 발급받을 수 있습니다.&lt;br /&gt;&lt;br /&gt;토큰은 봇의 통제권을 가진 비밀번호입니다.&lt;br /&gt;&lt;br /&gt;깃허브 업로드, 화면 공유, 메신저 전송 과정에서 실수로 노출되지 않게 주의해야 합니다.&lt;br /&gt;&lt;br /&gt;유출이 의심되면 바로 &lt;code&gt;Reset Token&lt;/code&gt;을 눌러 기존 토큰을 폐기합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 화면에서 &lt;code&gt;Message Content Intent&lt;/code&gt;를 켜 둡니다.&lt;br /&gt;&lt;br /&gt;지금 단계에서는 &lt;code&gt;!핑&lt;/code&gt;, &lt;code&gt;!청소&lt;/code&gt; 같은 메시지 기반 명령어를 사용하므로 이 설정이 꺼져 있으면 봇이 명령어를 읽지 못합니다.&lt;br /&gt;&lt;br /&gt;이 단계에서 빠뜨리는 경우가 매우 많습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 봇을 서버에 초대할 링크 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 메뉴에서 &lt;code&gt;OAuth2&lt;/code&gt; -&amp;gt; &lt;code&gt;URL Generator&lt;/code&gt;로 이동합니다.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;SCOPES&lt;/code&gt;에서는 &lt;code&gt;bot&lt;/code&gt;을 체크합니다.&lt;br /&gt;&lt;br /&gt;지금 단계에서는 슬래시 명령어를 사용하지 않으므로 &lt;code&gt;applications.commands&lt;/code&gt;는 필수가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 &lt;code&gt;BOT PERMISSIONS&lt;/code&gt;에서는 최소한 다음 권한을 체크합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;View Channels&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Send Messages&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Read Message History&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Manage Messages&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Manage Messages&lt;/code&gt;는 &lt;code&gt;!청소&lt;/code&gt; 명령어를 테스트할 때 필요합니다.&lt;br /&gt;&lt;br /&gt;이 권한이 없으면 봇이 온라인 상태여도 메시지 삭제는 수행하지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면 아래쪽에 생성된 URL을 복사해서 브라우저에 붙여넣고, 봇을 추가할 서버를 선택합니다.&lt;br /&gt;&lt;br /&gt;초대까지 마치면 서버 멤버 목록에서 봇을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 프로젝트 폴더 만들고 패키지 설치하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS Code에서 새 폴더를 열고 터미널을 실행합니다.&lt;br /&gt;&lt;br /&gt;가상환경까지 같이 만드는 흐름으로 잡아 두면 나중에 패키지 충돌이 적습니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;mkdir my-discord-bot
cd my-discord-bot
python3 -m venv .venv
source .venv/bin/activate
pip install -U discord.py python-dotenv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Windows에서는 가상환경 활성화 명령어만 다릅니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;python -m venv .venv
.venv\Scripts\activate
pip install -U discord.py python-dotenv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`discord.py`는 디스코드 API와 통신하는 핵심 라이브러리입니다.&lt;br /&gt;`python-dotenv`는 `.env` 파일에 저장한 토큰을 코드에서 읽어오는 용도입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. .env 파일에 토큰 보관하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 폴더 안에 &lt;code&gt;.env&lt;/code&gt; 파일을 만들고 아래처럼 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DISCORD_TOKEN=여기에_Developer_Portal_토큰_붙여넣기
PREFIX=!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`PREFIX`는 명령어 앞에 붙는 문자입니다.&lt;br /&gt;`!핑`, `!청소`처럼 느낌표 기반 명령어를 사용할 계획이므로 `!`로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.env&lt;/code&gt;를 제대로 읽지 못하면 &lt;code&gt;Improper token has been passed&lt;/code&gt;나 로그인 실패가 발생할 수 있습니다.&lt;br /&gt;&lt;br /&gt;공백이 같이 들어갔는지, 큰따옴표를 썼는지, 토큰 앞뒤에 줄바꿈이 붙었는지부터 확인하면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. main.py 전체 코드 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;main.py&lt;/code&gt; 파일을 만들고 아래 코드를 그대로 넣습니다.&lt;br /&gt;&lt;br /&gt;주석까지 포함한 완성본이라 바로 실행해도 됩니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os

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

# .env 파일에서 토큰과 접두사를 읽어옵니다.
load_dotenv()
TOKEN = os.getenv(&quot;DISCORD_TOKEN&quot;)
PREFIX = os.getenv(&quot;PREFIX&quot;, &quot;!&quot;)

# 메시지 기반 명령어를 읽으려면 message_content 인텐트가 필요합니다.
intents = discord.Intents.default()
intents.message_content = True

# commands.Bot은 명령어 처리 기능이 포함된 봇 클래스입니다.
bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None)


@bot.event
async def on_ready():
    print(f&quot;로그인 성공: {bot.user} (ID: {bot.user.id})&quot;)
    print(&quot;봇이 정상적으로 켜졌습니다.&quot;)


@bot.command(name=&quot;핑&quot;)
async def ping(ctx: commands.Context):
    latency_ms = round(bot.latency * 1000)
    await ctx.send(f&quot;퐁, 현재 지연 시간은 {latency_ms}ms 입니다.&quot;)


@bot.command(name=&quot;도움&quot;)
async def help_command(ctx: commands.Context):
    help_text = (
        &quot;사용 가능한 명령어\n&quot;
        f&quot;{PREFIX}핑 - 봇 응답 속도 확인\n&quot;
        f&quot;{PREFIX}서버정보 - 현재 서버 정보 확인\n&quot;
        f&quot;{PREFIX}청소 숫자 - 최근 메시지 정리\n&quot;
    )
    await ctx.send(f&quot;```\n{help_text}```&quot;)


@bot.command(name=&quot;서버정보&quot;)
async def server_info(ctx: commands.Context):
    guild = ctx.guild

    if guild is None:
        await ctx.send(&quot;이 명령어는 서버 채널에서만 사용할 수 있습니다.&quot;)
        return

    embed = discord.Embed(
        title=&quot;서버 정보&quot;,
        color=discord.Color.blue(),
    )
    embed.add_field(name=&quot;서버 이름&quot;, value=guild.name, inline=False)
    embed.add_field(name=&quot;멤버 수&quot;, value=str(guild.member_count), inline=True)
    embed.add_field(name=&quot;채널 수&quot;, value=str(len(guild.channels)), inline=True)
    embed.add_field(name=&quot;서버장&quot;, value=str(guild.owner), inline=False)

    if guild.icon:
        embed.set_thumbnail(url=guild.icon.url)

    await ctx.send(embed=embed)


@bot.command(name=&quot;청소&quot;)
@commands.has_permissions(manage_messages=True)
async def clear_messages(ctx: commands.Context, amount: int = 5):
    if amount &amp;lt; 1 or amount &amp;gt; 100:
        await ctx.send(&quot;청소 개수는 1 이상 100 이하로 입력하세요.&quot;)
        return

    deleted = await ctx.channel.purge(limit=amount + 1)
    notice = await ctx.send(f&quot;메시지 {len(deleted) - 1}개를 삭제했습니다.&quot;)
    await notice.delete(delay=3)


@clear_messages.error
async def clear_messages_error(ctx: commands.Context, error: Exception):
    if isinstance(error, commands.MissingPermissions):
        await ctx.send(&quot;이 명령어는 메시지 관리 권한이 있는 사용자만 사용할 수 있습니다.&quot;)
        return

    if isinstance(error, commands.BadArgument):
        await ctx.send(f&quot;사용 예시: {PREFIX}청소 10&quot;)
        return

    raise error


if not TOKEN:
    raise ValueError(&quot;DISCORD_TOKEN 값이 .env 파일에 없습니다.&quot;)

bot.run(TOKEN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;핵심은 `commands.Bot`입니다.&lt;br /&gt;일반 `discord.Client`보다 명령어를 다루기 쉬워 입문 단계에서는 이 구조가 더 직관적입니다.&lt;br /&gt;`@bot.command()` 데코레이터를 붙이면 `!핑`, `!서버정보` 같은 명령어를 빠르게 추가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;!청소&lt;/code&gt; 명령어에는 &lt;code&gt;@commands.has_permissions(manage_messages=True)&lt;/code&gt;를 붙였습니다.&lt;br /&gt;&lt;br /&gt;아무나 메시지를 지우지 못하게 막는 권한 조건입니다.&lt;br /&gt;&lt;br /&gt;봇 기능을 만들 때는 동작 자체보다 권한 조건을 같이 설계하는 습관이 중요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 봇 실행하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 아래 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;macOS나 일부 환경에서는 아래 명령어가 더 잘 맞을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python3 main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;정상 실행되면 터미널에 `로그인 성공` 문구가 출력됩니다.&lt;br /&gt;디스코드 서버 채널로 이동해서 아래 명령어를 하나씩 입력해 봅니다.&lt;/p&gt;
&lt;pre class=&quot;diff&quot;&gt;&lt;code&gt;!핑
!도움
!서버정보
!청소 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`!청소 3`은 최근 메시지 3개를 삭제합니다.&lt;br /&gt;테스트는 일반 채널보다 별도 테스트 채널에서 진행하는 편이 안전합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 자주 막히는 구간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 만들 때는 아래 구간에서 많이 멈춥니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Message Content Intent&lt;/code&gt; 활성화 누락:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;Bot&lt;/code&gt; 탭에서 이 설정을 켜지 않으면 봇이 온라인으로 보여도 &lt;code&gt;!핑&lt;/code&gt; 같은 메시지 명령어에는 반응하지 않습니다.&lt;br /&gt;&lt;br /&gt;코드는 멀쩡한데 봇이 조용하면 가장 먼저 여기부터 확인하면 됩니다.&lt;/li&gt;
&lt;li&gt;봇 권한 부족:&lt;br /&gt;&lt;br /&gt;특히 &lt;code&gt;!청소&lt;/code&gt; 명령어는 봇에게 &lt;code&gt;Manage Messages&lt;/code&gt; 권한이 있어야 하고, 명령어를 실행하는 사용자도 메시지 관리 권한이 있어야 합니다.&lt;br /&gt;&lt;br /&gt;권한 구조가 헷갈리면 봇 역할보다 위에 관리자 역할을 올려 두지 않았는지도 같이 확인합니다.&lt;/li&gt;
&lt;li&gt;토큰 복사 오류:&lt;br /&gt;&lt;br /&gt;토큰 앞뒤 공백, 잘못 복사한 문자열, 재발급 후 예전 토큰을 그대로 둔 경우가 많습니다.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;.env&lt;/code&gt; 값을 다시 붙여넣고 필요하면 포털에서 토큰을 재발급한 뒤 다시 테스트합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 여기서 다음으로 이어갈 만한 글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 봇 세팅은 끝났습니다.&lt;br /&gt;&lt;br /&gt;다음 단계에서는 AI 답변 기능을 붙이거나, 슬래시 명령어 구조로 바꾸거나, 서버 권한 구조를 정리하는 쪽으로 확장하면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 24시간 호스팅으로 실제 운영하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 만든 봇은 내 컴퓨터에서 터미널을 켠 동안만 돌아갑니다.&lt;br /&gt;&lt;br /&gt;노트북을 닫거나 전원을 끄면 봇도 같이 멈춥니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 유저들이 하루 종일 봇을 쓰게 하려면 별도 서버에 올려서 24시간 실행해야 합니다.&lt;br /&gt;&lt;br /&gt;배포 단계까지 바로 이어서 진행하려면 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 디스코드 봇 무료 호스팅, 디스호스트&lt;/a&gt; 글을 참고해서 방금 만든 &lt;code&gt;main.py&lt;/code&gt;와 &lt;code&gt;.env&lt;/code&gt;를 서버에 올리면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/53</guid>
      <comments>https://dishost.tistory.com/53#entry53comment</comments>
      <pubDate>Fri, 20 Mar 2026 11:37:17 +0900</pubDate>
    </item>
    <item>
      <title>디스코드 AI 챗봇 만들기, 파이썬(discord.py)과 OpenAI 연결하기</title>
      <link>https://dishost.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 AI 챗봇 만들기, 파이썬(discord.py)과 OpenAI 연결하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 디스코드 서버에 대화 기능이 있는 AI 봇을 추가하는 서버가 많아졌습니다.&lt;br /&gt;&lt;br /&gt;정해진 명령어에만 답하는 봇보다, 유저의 질문을 파악하고 대답하는 챗봇이 인기가 많기 때문입니다.&lt;br /&gt;&lt;br /&gt;챗봇을 만들기 위해서는 보통 OpenAI의 API를 활용하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 파이썬의 discord.py 라이브러리를 사용해서 디스코드 메신저에 챗봇을 만드는 전체 과정을 순서대로 설명합니다.&lt;br /&gt;&lt;br /&gt;파이썬 기초 문법을 알고 있다면 누구나 직관적으로 따라 해볼 수 있도록 작성했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. OpenAI 플랫폼에서 API 키 발급하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드 봇이 AI 모델과 통신하려면 OpenAI 서버의 접근 권한이 필요합니다.&lt;br /&gt;&lt;br /&gt;우선 인터넷 창을 열고 &lt;a href=&quot;https://platform.openai.com&quot;&gt;OpenAI 플랫폼 페이지&lt;/a&gt;에 로그인합니다.&lt;br /&gt;&lt;br /&gt;화면 왼쪽 메뉴에서 'API keys' 탭을 찾아서 클릭합니다.&lt;br /&gt;&lt;br /&gt;가운데 있는 [Create new secret key] 버튼을 누르면 영어와 숫자로 이루어진 긴 비밀 키(API 키)가 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 키의 문자열은 타인에게 절대 노출되면 안 됩니다.&lt;br /&gt;&lt;br /&gt;만약 외부에 노출되어 타인이 이 키를 악용하면 요금이 과다 청구될 수 있기 때문입니다.&lt;br /&gt;&lt;br /&gt;화면에 뜬 키를 복사해서 본인의 텍스트 편집기에 임시로 붙여넣습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 파이썬 프로젝트 폴더 만들고 라이브러리 설치하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터에 봇 관련 파일들을 모아둘 빈 폴더를 하나 만듭니다.&lt;br /&gt;&lt;br /&gt;VS Code 같은 코딩 웹 에디터나 프로그램으로 방금 만든 폴더를 열고 터미널을 실행합니다.&lt;br /&gt;&lt;br /&gt;디스코드 API와 OpenAI API를 코드에서 사용할 수 있도록 아래 명령어를 터미널 창에 적고 엔터를 누르세요.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;pip install discord.py openai python-dotenv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 디스코드 통신용 &lt;code&gt;discord.py&lt;/code&gt;, OpenAI 통신용 &lt;code&gt;openai&lt;/code&gt;, 그리고 아까 발급받은 비밀 키를 보관할 &lt;code&gt;python-dotenv&lt;/code&gt; 라이브러리를 설치합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 환경변수(.env) 보안 코드 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에 API 키를 그대로 적으면 나중에 깃허브 등에 코드를 올릴 때 키가 같이 유출될 위험이 큽니다.&lt;br /&gt;&lt;br /&gt;이 문제를 방지하기 위해 &lt;code&gt;.env&lt;/code&gt; 파일을 사용합니다. 폴더 안에 새 파일을 만들고 이름을 &lt;code&gt;.env&lt;/code&gt;로 설정합니다.&lt;br /&gt;&lt;br /&gt;파일을 열고 아래처럼 코드를 적어 넣습니다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;DISCORD_TOKEN=디스코드_포털에서_받은_토큰
OPENAI_API_KEY=sk-복사해둔_오픈에이아이_키&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 메인 파이썬 코드 (main.py) 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 봇을 실행할 &lt;code&gt;main.py&lt;/code&gt; 파일을 만들어 줍니다.&lt;br /&gt;&lt;br /&gt;빈 파일 안에 아래 파이썬 코드를 복사해서 붙여넣으세요.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import discord
from openai import AsyncOpenAI
import os
from dotenv import load_dotenv

# .env 파일에 저장해둔 비공개 키값들을 불러옵니다.
load_dotenv()

# 디스코드 봇이 메시지를 읽어올 수 있도록 설정(인텐트)을 변경합니다.
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)

# OpenAI 비동기 클라이언트를 실행합니다.
openai_client = AsyncOpenAI(api_key=os.getenv(&quot;OPENAI_API_KEY&quot;))

# 봇이 켜지고 접속을 마쳤을 때 터미널에 메시지를 출력합니다.
@client.event
async def on_ready():
    print(f&quot;로그인 성공: {client.user.name}&quot;)

# 서버에 새로운 메시지가 올라왔을 때 아래 코드가 실행됩니다.
@client.event
async def on_message(message):

    # 봇이 스스로 작성한 메시지는 무시합니다.
    if message.author.bot:
        return

    # 누군가 채팅창에서 봇을 태그했을 때만 반응합니다.
    if client.user in message.mentions:

        # 챗봇이 생각하는 동안 '입력 중...'이라는 표시를 띄워줍니다.
        async with message.channel.typing():

            # 유저의 메시지 내용 중에서 봇을 태그하는 코드를 지웁니다.
            user_text = message.content.replace(f'&amp;lt;@{client.user.id}&amp;gt;', '').strip()

            try:
                # OpenAI 서버로 질문 대화를 전송합니다.
                response = await openai_client.chat.completions.create(
                    model=&quot;gpt-4o-mini&quot;,
                    messages=[
                        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;너는 이 디스코드 서버의 매니저야. 친절하게 답변해줘.&quot;},
                        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_text}
                    ]
                )

                # 받아온 답변을 디스코드 채널에 답장으로 전송합니다.
                answer = response.choices[0].message.content
                await message.reply(answer)

            except Exception as e:
                # 에러 로그를 출력합니다.
                print(f&quot;오류 발생: {e}&quot;)
                await message.reply(&quot;에러가 발생해서 답변을 할 수 없습니다.&quot;)

# 설정해둔 디스코드 토큰으로 봇을 켭니다.
client.run(os.getenv(&quot;DISCORD_TOKEN&quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 &lt;code&gt;system&lt;/code&gt; 역할을 맡은 안내 메시지를 수정하면 봇의 성격을 바꿀 수 있습니다.&lt;br /&gt;&lt;br /&gt;원하는 말투나 설정으로 문장을 수정하면 그에 맞게 봇의 반응이 즉각적으로 변합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 에러 확인 및 Message Content 권한 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 &lt;code&gt;python main.py&lt;/code&gt;를 누르면 코드가 실행됩니다.&lt;br /&gt;&lt;br /&gt;디스코드 서버로 넘어가서 봇을 태그하고 질문을 하면 로딩 표시 이후에 봇이 대답을 전송합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 코드를 다 적었는데도 봇이 메시지에 반응을 하지 않고 오류가 나타난다면, 디스코드 개발자 포털(Discord Developer Portal)에서 서버 권한을 확인해야 합니다.&lt;br /&gt;&lt;br /&gt;내 봇을 선택한 뒤 Bot 메뉴로 들어가서 'Message Content Intent' 항목의 스위치가 파란색으로 활성화되어 있는지 확인하고 변경 사항을 저장하세요.&lt;br /&gt;&lt;br /&gt;이 기능 커져 있어야 봇이 유저의 대화 텍스트 데이터를 정상적으로 읽어올 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 디스코드 봇을 24시간 호스팅하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 모두 작성하고 작동까지 확인했다면 내 컴퓨터를 켤 때만 작업 환경에서 봇이 작동하는 상태일 것입니다.&lt;br /&gt;&lt;br /&gt;컴퓨터 전원을 끄거나 에디터 프로그램을 닫으면 파이썬 프로그램도 중단되기 때문에 봇도 멈춥니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 유저들이 새벽 시간대라도 봇을 사용하게 하려면 24시간 내내 작동할 별도의 호스팅 서버를 이용해야 합니다.&lt;br /&gt;&lt;br /&gt;관련된 무료 호스팅 배포 방법은 &lt;a href=&quot;https://blog.dishost.kr/47&quot;&gt;24시간 무료 봇 호스팅, 디스호스트&lt;/a&gt; 글을 참고하여, 방금 작성한 &lt;code&gt;main.py&lt;/code&gt;와 &lt;code&gt;.env&lt;/code&gt; 파일 등을 서버에 올려두고 작동시키면 됩니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/52</guid>
      <comments>https://dishost.tistory.com/52#entry52comment</comments>
      <pubDate>Thu, 19 Mar 2026 17:22:25 +0900</pubDate>
    </item>
    <item>
      <title>웹소설 표지 생성 AI, PYOZI AI 장단점, 기능, 가격 정리</title>
      <link>https://dishost.tistory.com/51</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹소설에서 표지의 중요성은 매우 중요합니다. 독자가 작품을 고를 때 가장 먼저 보는 요소이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 표지 작업은 비용과 시간이 많이 드는 과정이기도 합니다. 그래서 AI를 활용한 표지 생성 서비스가 점점 관심을 받고 있는데, 그중에서도 PYOZI AI는 웹소설 작가들에게 특히 맞춤형으로 설계된 표지 생성 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 PYOZI AI가 어떤 서비스인지, 장점과 단점은 무엇인지, 기능과 가격은 어떻게 구성돼 있는지, 실제로 어떻게 쓰면 되는지를 한 번에 정리해 보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PYOZI AI&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PYOZI AI는 웹소설 표지와 삽화를 만드는 데 초점을 맞춘 AI 생성 서비스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 장르에 맞는 표지를 빠르게 뽑거나, 표지에 타이포그래피를 얹거나, 플랫폼 규격에 맞춰진 파일을 받거나, 상업적 라이선스 등의 번거로움을 해결해 줍니다. 즉 웹소설 표지 제작 프로세스를 한번에 해결해주는 서비스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pyozi.com/&quot;&gt;PYOZI AI : https://pyozi.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774259348204&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;PYOZI AI | 웹소설 표지 AI 생성 솔루션&quot; data-og-description=&quot;AI가 웹소설 표지와 삽화를 자동으로 생성합니다. 로맨스판타지, BL, 무협, 현대판타지 등 다양한 장르 지원. 전문 일러스트레이터 수준의 퀄리티를 몇 분 만에!&quot; data-og-host=&quot;pyozi.com&quot; data-og-source-url=&quot;https://pyozi.com/&quot; data-og-url=&quot;https://pyozi.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dIurfK/dJMb82eMr40/2hHkAsJdkMDGSuYikERrkk/img.png?width=1026&amp;amp;height=379&amp;amp;face=0_0_1026_379,https://scrap.kakaocdn.net/dn/bAIes8/dJMb9g5aAtj/o36QMz9RrzVxCkc9z4BPB0/img.png?width=1026&amp;amp;height=379&amp;amp;face=0_0_1026_379&quot;&gt;&lt;a href=&quot;https://pyozi.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pyozi.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dIurfK/dJMb82eMr40/2hHkAsJdkMDGSuYikERrkk/img.png?width=1026&amp;amp;height=379&amp;amp;face=0_0_1026_379,https://scrap.kakaocdn.net/dn/bAIes8/dJMb9g5aAtj/o36QMz9RrzVxCkc9z4BPB0/img.png?width=1026&amp;amp;height=379&amp;amp;face=0_0_1026_379');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;PYOZI AI | 웹소설 표지 AI 생성 솔루션&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AI가 웹소설 표지와 삽화를 자동으로 생성합니다. 로맨스판타지, BL, 무협, 현대판타지 등 다양한 장르 지원. 전문 일러스트레이터 수준의 퀄리티를 몇 분 만에!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pyozi.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 웹소설 전용 이미지 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범용 AI 이미지 생성기는 결과물이 웹소설 표지로 바로 이어지지 않는 경우가 많습니다. 원하는 그림체를 찾는 것이 어렵고, 캐릭터 구도, 제목 배치 등 단순 이미지 생성기로는 해결하기 힘든 부분이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 PYOZI AI는 처음부터 웹소설 표지와 삽화에 맞춰 설계된 서비스이기 때문에, 장르별 분위기와 표지용 결과물에 초점이 맞춰져 있다는 점이 첫 번째 장점입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 타이포그래피 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타 그림 AI는 타이포그래피를 생성할 수 없기 때문에, 결국 타이포그래피는 포토샵 등의 프로그램을 이용하여 직접 추가해야하는 단점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PYOZI AI는 타이포그래피 생성 기능이 포함돼 있어서, 그림 생성 이후 실제 표지를 바로 완성할 수 있습니다. 디자인 툴을 다루지 않는 작가에게 이 차이는 큽니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 저렴한 가격&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외주 표지는 한 번 맡기면 &lt;a href=&quot;https://pyozi.com/blog/cover-commission-prices&quot;&gt;적지 않은 비용&lt;/a&gt;이 들어갑니다. 일러스트 외주를 통해 웹소설 표지 외주를 맡기면 최소 30만원에서 많게는 150만원까지 소모되는데 비해, PYOZI AI는 9달러 정도로 훨씬 더 많은 표지를 생성할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 상업 사용 및 라이센스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외주나 커미션으로 표지 제작을 맡기게 되면, 외주 작가와 복잡한 라이센스 과정과 저작권 협의 과정을 거쳐야하고, 이 과정에서 많은 시간과 돈이 소모되게 됩니다. PYOZI AI는 유료 생성 결과물에 대한 모든 저작권을 행사하지 않기 때문에, 이러한 문제에서 안전하다고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 각 플랫폼마다 AI 이미지와 관련된 정책이 다르니, 이를 따로 확인하는 편이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;단점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 아직은 못미치는 퀄리티&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 아직은 이미지 생성 AI가 완벽하지 않기 때문에, 일러스트레이터에게 고액의 외주를 맡겨 나오는 수준의 결과물을 기대하기는 어렵습니다. 가끔은 경이로운 퀄리티를 보여주기도 하지만, 눈동자의 초점과 동공 묘사가 일그러지거나 복잡한 옷감의 문양이나 머리카락과 옷의 경계선이 뭉개지는 등, 퀄리티가 완벽하지 않을 때가 있습니다. 배경에서도 물체가 서로 겹쳐 보이거나 중력을 거스르는 형태의 소품이 나타나는 AI 노이즈가 발생할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 아쉬운 디테일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 특정 인물의 복장 디테일, 아주 미묘한 시대감, 소설 속 핵심 상징을 정확히 넣는 등의 픽셀 단위의 정교한 결과물을 위해서는 많은 시도가 필요합니다. 따라서 표지 내에 매우 디테일한 표현을 원하는 분들에게는 고액 일러스트가 더 나은 선택일 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PYOZI AI 주요 기능 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장르 맞춤형 표지 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로판, BL, 판타지, 무협, 현대물 등 웹소설 장르에 맞는 분위기의 결과물을 빠르게 뽑는 데 초점이 맞춰져 있습니다. 범용 이미지 툴보다 웹소설 표지에 더 적합한 이미지를 생성할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자동 타이포그래피 배치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표지 이미지 위에 제목을 올리는 기능이 따로 있습니다. 직접 텍스트 레이어를 생성할 필요없이, 스타일만 정하면 알아서 타이포그래피가 배치됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변형 생성과 부분 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 처음부터 다시 뽑는 것만이 아니라, 변형 생성과 수정 기능도 들어가 있습니다. 마음에 드는 시안을 중심으로 조금씩 조정해 가는 방식이 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다양한 그림체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;21개 이상의 그림체가 있어서, 다양한 장르와 대상 독자에 맞는 그림체를 골라 표지를 제작할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐릭터 기억 기능 / 프로젝트 기반 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소설 캐릭터를 생성 후 저장하여, 표지나 삽화에 동일한 캐릭터를 넣을 수 있습니다. 또한 내 웹소설들의 캐릭터, 삽화, 표지들을 프로젝트 기반으로 편리하게 관리할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;플랜&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단건 패키지: Premium Cover&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;9.9달러&lt;/li&gt;
&lt;li&gt;시안 생성 40장&lt;/li&gt;
&lt;li&gt;타이포그래피 생성 8회&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;정기 구독은 부담스럽고 표지를 하나만 만들고 싶다&amp;rdquo;는 작가님께 가장 적합합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Basic 플랜&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;월 9달러&lt;/li&gt;
&lt;li&gt;월 크레딧 800 (표지 80개 or 타이포 20개 생성)&lt;/li&gt;
&lt;li&gt;프로젝트 슬롯 3개&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 유료로 넘어가서 테스트해 보기 가장 무난한 단계입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pro 플랜&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;월 19달러&lt;/li&gt;
&lt;li&gt;월 크레딧 2400 (표지 240개 or 타이포 60개 생성)&lt;/li&gt;
&lt;li&gt;프로젝트 슬롯 10개&lt;/li&gt;
&lt;li&gt;타이포그래피 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 대중적으로 쓰기 쉬운 구간입니다. 여러 작품을 동시에 만지거나, 한 작품에서 시안을 많이 비교해 보고 싶다면 이쪽이 더 현실적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Studio 플랜&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;월 35달러&lt;/li&gt;
&lt;li&gt;월 크레딧 4500 (표지 450개 or 타이포 112개 생성)&lt;/li&gt;
&lt;li&gt;프로젝트 슬롯 사실상 무제한 수준&lt;/li&gt;
&lt;li&gt;타이포그래피 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 프로젝트를 동시에 돌리거나, 작업량이 꾸준히 많은 작가님께 맞는 플랜입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;크레딧 충전 상품&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;15달러: 1200 크레딧&lt;/li&gt;
&lt;li&gt;30달러: 2500 크레딧&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월 구독보다 &amp;ldquo;필요할 때만 충전해서 쓰고 싶다&amp;rdquo;는 작가님께 적합합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추천 대상&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좋아!&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연재용 표지를 빠르게 만들고 싶은 웹소설 작가&lt;/li&gt;
&lt;li&gt;저예산으로 준수한 퀄리티의 표지를 뽑고 싶은 작가&lt;/li&gt;
&lt;li&gt;복잡한 외주 계약 등을 신경쓰고 싶지 않은 경우&lt;/li&gt;
&lt;li&gt;포토샵 없이 제목까지 한 번에 제작하고 싶은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;나빠..&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음부터 100% 수작업 외주 퀄리티를 기대하는 경우&lt;/li&gt;
&lt;li&gt;AI 이미지 자체를 플랫폼 정책상 쓰기 어려운 연재처만 노리는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PYOZI AI는 단순 그림 AI라기보다는, 웹소설 표지에 맞춰진 표지 생성 AI입니다. 웹소설 표지를 빠르고 저렴하게, 준수한 퀄리티로 뽑고 싶은 작가님이시라면, 한 번 써볼 이유는 충분합니다. 특히 정기 구독이 부담되면 단건 패키지부터 테스트해 보는 방식도 괜찮습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pyozi.com/&quot;&gt;https://pyozi.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774259398994&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;PYOZI AI | 웹소설 표지 AI 생성 솔루션&quot; data-og-description=&quot;AI가 웹소설 표지와 삽화를 자동으로 생성합니다. 로맨스판타지, BL, 무협, 현대판타지 등 다양한 장르 지원. 전문 일러스트레이터 수준의 퀄리티를 몇 분 만에!&quot; data-og-host=&quot;pyozi.com&quot; data-og-source-url=&quot;https://pyozi.com/&quot; data-og-url=&quot;https://pyozi.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dIurfK/dJMb82eMr40/2hHkAsJdkMDGSuYikERrkk/img.png?width=1026&amp;amp;height=379&amp;amp;face=0_0_1026_379,https://scrap.kakaocdn.net/dn/bAIes8/dJMb9g5aAtj/o36QMz9RrzVxCkc9z4BPB0/img.png?width=1026&amp;amp;height=379&amp;amp;face=0_0_1026_379&quot;&gt;&lt;a href=&quot;https://pyozi.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pyozi.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dIurfK/dJMb82eMr40/2hHkAsJdkMDGSuYikERrkk/img.png?width=1026&amp;amp;height=379&amp;amp;face=0_0_1026_379,https://scrap.kakaocdn.net/dn/bAIes8/dJMb9g5aAtj/o36QMz9RrzVxCkc9z4BPB0/img.png?width=1026&amp;amp;height=379&amp;amp;face=0_0_1026_379');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;PYOZI AI | 웹소설 표지 AI 생성 솔루션&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AI가 웹소설 표지와 삽화를 자동으로 생성합니다. 로맨스판타지, BL, 무협, 현대판타지 등 다양한 장르 지원. 전문 일러스트레이터 수준의 퀄리티를 몇 분 만에!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pyozi.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/51</guid>
      <comments>https://dishost.tistory.com/51#entry51comment</comments>
      <pubDate>Fri, 6 Feb 2026 19:06:34 +0900</pubDate>
    </item>
    <item>
      <title>Discord.py Components V2 사용 가이드 (신형 임베드, 줄 나누기, 임베드 내에 버튼 등)</title>
      <link>https://dishost.tistory.com/50</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Components V2 아키텍처 및 LayoutView 시스템&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Discord API의 Components V2는 기존의 자동 레이아웃 방식에서 개발자가 직접 UI를 배치하는 수동 레이아웃 방식으로 바뀌었습니다.&lt;br /&gt;discord.py 2.6부터 추가된 discord.ui.LayoutView가 핵심이며, 기존 discord.ui.View의 자동 행 관리 방식을 완전히 바꿉니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 LayoutView 기본 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LayoutView는 Components V2에서 UI를 만들 때 사용하는 기본 컨테이너입니다. 기존 View는 컴포넌트를 추가하면 자동으로 5개의 Action Row에 배치했지만, LayoutView에서는 개발자가 직접 계층 구조와 배치를 정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, UI 구성의 제어권이 라이브러리에서 개발자에게 넘어온 것입니다. 가장 중요한 제약은 컴포넌트 개수 제한인데, LayoutView 하나당 최대 40개까지 배치할 수 있습니다(중첩 포함). 기존 25개(5&amp;times;5)보다 늘어났지만, Container나 Section 같은 구조 컴포넌트도 카운트되므로 실제 사용 가능한 슬롯은 생각보다 적을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;b&gt;특성&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;discord.ui.View (V1)&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;discord.ui.LayoutView (V2)&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;기본 레이아웃&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;자동&lt;/td&gt;
&lt;td&gt;수동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;행(Row) 관리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;자동 처리&lt;/td&gt;
&lt;td&gt;ActionRow 명시 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;최대 컴포넌트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;25개 (5x5)&lt;/td&gt;
&lt;td&gt;40개 (중첩 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;계층 구조&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;평면적&lt;/td&gt;
&lt;td&gt;중첩 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Top-Level 요소&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Button, Select&lt;/td&gt;
&lt;td&gt;TextDisplay, Container, Section, ActionRow 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 클래스 기반 UI 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LayoutView는 클래스 속성으로 UI를 정의하는 방식을 사용합니다. 코드 가독성이 좋고 UI 구조를 한눈에 파악하기 쉽습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;import discord
from discord import ui

class TechnicalSpecLayout(ui.LayoutView):
    # Top-Level Component 1: TextDisplay
    header = ui.TextDisplay(
        label=&quot;시스템 상태 모니터링&quot;,
        style=discord.TextStyle.heading
    )

    # Top-Level Component 2: Separator
    separator = ui.Separator(spacing=discord.SeparatorSpacing.small)

    # Top-Level Component 3: Container (Nested)
    status_box = ui.Container(
        ui.TextDisplay(&quot;정상 작동 중: 99.9% Uptime&quot;)
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TextDisplay, Separator, Container 같은 V2 컴포넌트는 LayoutView의 최상위에 직접 배치할 수 있습니다. 하지만 Button이나 Select Menu는 최상위에 둘 수 없고, 반드시 ActionRow 안에 넣어야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 동적 아이템 관리와 ID 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Components V2의 모든 컴포넌트는 고유한 숫자형 id를 가집니다(custom_id와는 별개). 중첩된 구조에서 특정 컴포넌트를 찾으려면 find_item(id) 메서드를 사용하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;# ID 기반 컴포넌트 탐색 예시
TRACKER_ID = 10101

class DynamicCounter(ui.LayoutView):
    def __init__(self):
        super().__init__()
        self.count = 0

    # ID를 명시적으로 할당하여 추후 검색 가능하게 설정
    display = ui.TextDisplay(&quot;Count: 0&quot;, id=TRACKER_ID)

    actions = ui.ActionRow()

    @actions.button(label=&quot;증가&quot;)
    async def increment(self, interaction: discord.Interaction, button: ui.Button):
        self.count += 1
        # ID를 사용하여 깊은 계층의 컴포넌트 탐색
        display_item = self.view.find_item(TRACKER_ID)
        if isinstance(display_item, ui.TextDisplay):
            display_item.label = f&quot;Count: {self.count}&quot;
        await interaction.response.edit_message(view=self)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 ID 시스템 덕분에 UI 상태가 바뀔 때 전체를 다시 만들지 않고 특정 컴포넌트만 수정할 수 있습니다. walk_children()로 모든 자식 컴포넌트를 순회할 수도 있어서 일괄 변경이나 검증 로직 구현에 유용합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 텍스트 표시 컴포넌트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Embed로 텍스트를 표시했지만, Components V2에서는 TextDisplay라는 전용 컴포넌트가 생겼습니다. 메시지 본문과 별개로 UI 레이아웃 안에 텍스트를 넣을 수 있어서 더 유연합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 TextDisplay 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ui.TextDisplay는 Discord 마크다운을 지원하며, UI 안에서 라벨, 설명, 경고 문구 등을 표시할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Markdown 지원:&lt;/b&gt; Bold(**), Italic(*), Underline(__), Codeblock(```) 등을 포함한 표준 Discord Markdown을 렌더링합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배치 제약:&lt;/b&gt; 최상위 레벨에 위치하거나 Container, Section 내부에 중첩될 수 있습니다. ActionRow 내부에는 배치할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# TextDisplay 활용 패턴
class InfoLayout(ui.LayoutView):
    # 일반 텍스트
    simple_text = ui.TextDisplay(&quot;기본 안내 메시지입니다.&quot;)

    # 마크다운 활용 강조
    alert_text = ui.TextDisplay(
        &quot;**경고:** 이 작업은 되돌릴 수 없습니다.\n`System.reset()` 함수가 호출됩니다.&quot;
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TextDisplay는 클릭 같은 상호작용이 안 되는 정적 컴포넌트입니다. custom_id로 이벤트 콜백을 등록할 수 없고, 정보 표시용으로만 씁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Separator로 간격 조정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ui.Separator는 컴포넌트 사이의 간격을 조정하는 요소입니다. UI에서 여백은 가독성에 중요한데, V2에서는 이걸 명시적인 컴포넌트로 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Separator는 spacing과 visible 두 가지 핵심 속성을 통해 제어됩니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;b&gt;속성 (Attribute)&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;타입 (Type)&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;설명 (Description)&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;spacing&lt;/td&gt;
&lt;td&gt;SeparatorSpacing&lt;/td&gt;
&lt;td&gt;여백의 크기. small 또는 large 값을 가집니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;visible&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;True일 경우 가로 구분선(Divider) 렌더링. False일 경우 투명 여백만 적용됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class FormattingLayout(ui.LayoutView):
    title = ui.TextDisplay(&quot;설정 메뉴&quot;)

    # 가시적인 구분선 (Divider)
    line = ui.Separator(visible=True, spacing=discord.SeparatorSpacing.small)

    option_1 = ui.TextDisplay(&quot;옵션 A&quot;)

    # 투명한 큰 여백 (Spacer 역할)
    spacer = ui.Separator(visible=False, spacing=discord.SeparatorSpacing.large)

    footer = ui.TextDisplay(&quot;Copyright 2025&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;discord.SeparatorSpacing은 small과 large 두 가지가 있습니다. small은 항목 간 작은 간격에, large는 큰 구분에 씁니다. visible=True와 spacing=large를 같이 쓰면 섹션을 명확하게 나눌 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 컨테이너와 그룹화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Components V2에서는 Section과 Container로 컴포넌트를 그룹화할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 Section: 텍스트와 액세서리 조합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ui.Section은 텍스트와 액세서리 컴포넌트 하나를 가로로 배치하는 레이아웃입니다. 설정 화면의 &quot;라벨 - 버튼&quot; 패턴을 만들 때 유용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Content:&lt;/b&gt; 섹션 좌측에 표시될 텍스트입니다. 문자열을 전달하면 내부적으로 TextDisplay로 변환되고, TextDisplay 객체를 직접 전달할 수도 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Accessory:&lt;/b&gt; 섹션 우측에 표시될 상호작용 요소입니다. 주로 Button이나 Thumbnail을 씁니다. Select Menu는 액세서리로 쓸 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nsis&quot;&gt;&lt;code&gt;class SettingsLayout(ui.LayoutView):
    # 문자열 직접 전달 및 버튼 액세서리
    toggle_section = ui.Section(
        &quot;자동 응답 모드 활성화&quot;,
        accessory=ui.Button(label=&quot;ON&quot;, style=discord.ButtonStyle.success)
    )

    # TextDisplay 객체 전달 및 썸네일 액세서리
    profile_section = ui.Section(
        ui.TextDisplay(&quot;**User:** AbstractUmbra&quot;),
        accessory=ui.Thumbnail(url=&quot;[https://example.com/avatar.png](https://example.com/avatar.png)&quot;) # 가상의 Thumbnail 컴포넌트
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Section을 쓰면 ActionRow와 TextDisplay를 따로 배치하지 않아도 리스트 형태의 UI를 쉽게 만들 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Container: 테두리가 있는 박스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ui.Container는 다른 컴포넌트들을 담을 수 있는 박스입니다. Embed처럼 테두리가 있고, accent_color로 테두리 색상을 바꿀 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Container는 중첩해서 복잡한 레이아웃을 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;# Container 서브클래싱 패턴
class UserCard(ui.Container):
    def __init__(self, username: str, role_color: int):
        super().__init__(accent_color=role_color)

        # 컨테이너 내부 컴포넌트 정의
        self.name = ui.TextDisplay(f&quot;Name: {username}&quot;)
        self.divider = ui.Separator(visible=True)
        self.status = ui.TextDisplay(&quot;Status: Online&quot;)

class Dashboard(ui.LayoutView):
    # 인스턴스화하여 LayoutView에 배치
    card_1 = UserCard(&quot;Admin&quot;, 0xFF0000)

    # 빈 공간
    space = ui.Separator(spacing=discord.SeparatorSpacing.large)

    card_2 = UserCard(&quot;Moderator&quot;, 0x00FF00)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Container 안에 Container를 또 넣을 수도 있지만, 컴포넌트 제한(40개)과 모바일 가독성을 생각하면 중첩은 최소화하는 게 좋습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 상호작용 컴포넌트와 ActionRow&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Components V2에서 Button과 Select Menu의 동작 방식은 V1과 같지만, 배치 방식에는 제약이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 ActionRow 필수 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;V1에서는 View에 버튼을 추가하면 자동으로 행이 생성됐지만, V2에서는 Button과 Select Menu를 최상위에 둘 수 없습니다. 반드시 ui.ActionRow 안에 넣어야 합니다. ActionRow 하나당 최대 5개 슬롯을 가집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Button:&lt;/b&gt; 1개 슬롯 차지 (최대 5개/Row)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Select Menu:&lt;/b&gt; 5개 슬롯 전체 차지 (일반적). 따라서 하나의 ActionRow에는 하나의 Select Menu만 배치 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 데코레이터로 ActionRow 구성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ActionRow 인스턴스를 만들고, 메서드 데코레이터로 버튼과 셀렉트 메뉴를 추가하는 게 기본 패턴입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4.2.1 Button 구현&lt;/h4&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;class InteractionLayout(ui.LayoutView):
    # ActionRow 선언
    actions = ui.ActionRow()

    # 데코레이터로 버튼 부착
    @actions.button(label=&quot;승인&quot;, style=discord.ButtonStyle.primary)
    async def approve(self, interaction: discord.Interaction, button: ui.Button):
        await interaction.response.send_message(&quot;승인되었습니다.&quot;, ephemeral=True)

    @actions.button(label=&quot;거절&quot;, style=discord.ButtonStyle.danger)
    async def deny(self, interaction: discord.Interaction, button: ui.Button):
        await interaction.response.send_message(&quot;거절되었습니다.&quot;, ephemeral=True)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4.2.2 Select Menu 구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select 데코레이터를 사용하여 String Select, User Select, Role Select 등 다양한 드롭다운 메뉴를 구현합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class SelectionLayout(ui.LayoutView):
    # Select 전용 Row
    select_row = ui.ActionRow()

    @select_row.select(
        placeholder=&quot;카테고리 선택&quot;,
        min_values=1,
        max_values=1,
        options=
    )
    async def category_select(self, interaction: discord.Interaction, select: ui.Select):
        chosen = select.values
        await interaction.response.send_message(f&quot;선택: {chosen}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널이나 사용자를 선택하는 특수 셀렉트 메뉴는 cls 파라미터를 통해 타입을 지정합니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;    @select_row.select(cls=ui.ChannelSelect, channel_types=)
    async def channel_pick(self, interaction: discord.Interaction, select: ui.ChannelSelect):
        #... 구현...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 동적으로 ActionRow 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데코레이터 대신 add_item 메서드로 런타임에 동적으로 ActionRow를 구성할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;def create_dynamic_row(labels: list[str]) -&amp;gt; ui.ActionRow:
    row = ui.ActionRow()
    for label in labels:
        btn = ui.Button(label=label)
        # 콜백 별도 지정 필요
        row.add_item(btn)
    return row

class DynamicLayout(ui.LayoutView):
    def __init__(self, options):
        super().__init__()
        # 초기화 시점에 동적 Row 추가
        self.add_item(create_dynamic_row(options))&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 미디어 갤러리 (MediaGallery)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Components V2에서는 ui.MediaGallery로 첨부 파일을 갤러리 형태로 보여줄 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 MediaGallery 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MediaGallery는 MediaGalleryItem 객체들의 집합으로 구성됩니다. 각 아이템은 URL을 통해 외부 이미지를 참조하거나, attachment:// 스키마를 통해 업로드된 파일을 참조할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;class GalleryLayout(ui.LayoutView):
    # 파일 객체 준비 (전송 시 files 파라미터에 포함되어야 함)
    # 주의: 레이아웃 정의 시점에는 파일이 업로드되지 않았으므로
    # 실제 전송 시점의 attachment 논리를 이해해야 함.

    gallery = ui.MediaGallery(
        # URL 기반 아이템
        discord.MediaGalleryItem(&quot;[https://example.com/image1.png](https://example.com/image1.png)&quot;, description=&quot;외부 이미지&quot;),

        # 첨부 파일 기반 아이템 (스포일러 설정 가능)
        discord.MediaGalleryItem(&quot;attachment://local_image.jpg&quot;, spoiler=True)
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;discord.File 객체를 MediaGalleryItem 생성자에 바로 전달할 수도 있지만, 메시지 전송 시 files=[...] 파라미터에는 여전히 포함시켜야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. LayoutView 마이그레이션과 예제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 ui.View 코드를 ui.LayoutView로 바꿀 때는 다음 단계를 따르면 됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;상속 변경:&lt;/b&gt; class MyView(ui.View) -&amp;gt; class MyView(ui.LayoutView).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버튼/셀렉트 래핑:&lt;/b&gt; 최상위 레벨의 @ui.button 데코레이터를 제거하고, ui.ActionRow() 인스턴스를 생성한 후 @action_row_instance.button 형태로 변경합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레이아웃 요소 추가:&lt;/b&gt; TextDisplay나 Separator를 추가하여 기존 Embed description에 의존하던 텍스트를 컴포넌트로 이관합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 종합 예제: 서버 관리 대시보드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 V2의 주요 컴포넌트를 모두 사용한 대시보드 코드입니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import discord
from discord.ext import commands
from discord import ui

# 봇 인스턴스 설정
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix=&quot;!&quot;, intents=intents)

class ServerControlPanel(ui.LayoutView):
    &quot;&quot;&quot;
    서버 관리를 위한 Components V2 기반 대시보드 레이아웃
    &quot;&quot;&quot;

    #  헤더 영역 (TextDisplay)
    header = ui.TextDisplay(
        &quot;##  ️ 서버 제어 패널&quot;, 
        style=discord.TextStyle.paragraph
    )

    #  시각적 분리 (Separator)
    sep_header = ui.Separator(visible=True, spacing=discord.SeparatorSpacing.small)

    #  상태 모니터링 섹션 (Container + Section)
    class StatusContainer(ui.Container):
        def __init__(self):
            super().__init__(accent_color=0x5865F2) # Blurple Color

            self.cpu_section = ui.Section(
                &quot;CPU 사용량&quot;,
                accessory=ui.Button(label=&quot;34%&quot;, disabled=True, style=discord.ButtonStyle.secondary)
            )
            self.ram_section = ui.Section(
                &quot;RAM 사용량&quot;,
                accessory=ui.Button(label=&quot;12GB / 16GB&quot;, disabled=True, style=discord.ButtonStyle.secondary)
            )

    status_box = StatusContainer()

    #  여백 (Spacer)
    spacer = ui.Separator(visible=False, spacing=discord.SeparatorSpacing.large)

    #  관리 기능 선택 (ActionRow + Select)
    menu_row = ui.ActionRow()

    @menu_row.select(
        placeholder=&quot;수행할 작업을 선택하세요...&quot;,
        options=
    )
    async def action_select(self, interaction: discord.Interaction, select: ui.Select):
        selection = select.values
        # interaction handling logic
        await interaction.response.send_message(f&quot;선택된 작업: {selection}&quot;, ephemeral=True)

    #  긴급 제어 (ActionRow + Button)
    control_row = ui.ActionRow()

    @control_row.button(label=&quot;서버 재시작&quot;, style=discord.ButtonStyle.danger)
    async def reboot(self, interaction: discord.Interaction, button: ui.Button):
        await interaction.response.send_message(&quot;서버 재시작 시퀀스를 시작합니다...&quot;, ephemeral=True)

    @control_row.button(label=&quot;새로고침&quot;, style=discord.ButtonStyle.secondary)
    async def refresh(self, interaction: discord.Interaction, button: ui.Button):
        # 뷰 업데이트 예시
        await interaction.response.edit_message(view=self)

@bot.command()
async def panel(ctx):
    &quot;&quot;&quot;대시보드 출력 명령어&quot;&quot;&quot;
    view = ServerControlPanel()
    await ctx.send(view=view)

# 봇 실행 (토큰은 환경변수 등으로 관리 권장)
# bot.run(&quot;YOUR_TOKEN_HERE&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 코드 포인트&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;중첩 클래스:&lt;/b&gt; StatusContainer를 내부나 외부에 정의해서 모듈화된 UI를 만들 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비활성 버튼:&lt;/b&gt; disabled=True인 버튼은 뱃지나 상태 표시기처럼 쓸 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레이아웃 흐름:&lt;/b&gt; 헤더 -&amp;gt; 구분선 -&amp;gt; 컨테이너 -&amp;gt; 공백 -&amp;gt; 셀렉트 -&amp;gt; 버튼 순으로 수직 배치됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 성능 고려사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Components V2 사용 시 주의할 점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Payload Size:&lt;/b&gt; LayoutView는 기존 View보다 복잡한 JSON을 생성합니다. 중첩된 컨테이너와 많은 TextDisplay는 API 요청 크기를 키우므로, 40개 제한 전에 페이로드 크기 제한에 걸릴 수 있습니다.3&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Rendering Cost:&lt;/b&gt; V2 컴포넌트는 렌더링 비용이 더 높습니다. MediaGallery나 복잡한 Container 중첩은 저사양 모바일에서 느려질 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Interaction Latency:&lt;/b&gt; LayoutView는 상태가 없어서 상호작용 시 오버헤드는 적지만, find_item을 너무 많이 쓰지 않는 게 좋습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>봇 개발 팁/Discord.py</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/50</guid>
      <comments>https://dishost.tistory.com/50#entry50comment</comments>
      <pubDate>Sat, 6 Dec 2025 16:57:10 +0900</pubDate>
    </item>
    <item>
      <title>Discord.js Components V2 사용 방법 (신형 임베드, 줄 나누기, 임베드 내에 버튼 등)</title>
      <link>https://dishost.tistory.com/49</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Component V2 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 디스코드 봇 개발에서는 Embed(임베드)를 주로 사용했습니다. 하지만 임베드는 제목, 본문, 썸네일 등의 위치가 고정되어 있어 레이아웃을 마음대로 바꾸기 어려웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Component V2&lt;/b&gt;는 이러한 제약 없이 개발자가 원하는 순서대로 텍스트, 버튼, 이미지 등을 배치할 수 있는 새로운 시스템입니다. Embed 대신 Container를 사용하며, 특히 텍스트 바로 옆에 버튼을 배치하는 등 더 자유로운 디자인이 가능합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 필수 설정 (플래그)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;V2 시스템을 사용하려면 코드를 작성할 때 반드시 플래그(Flag)를 설정해야 합니다. 이 설정이 없으면 코드가 정확해도 디스코드가 이를 V1(기존 방식)으로 인식하여 오류가 발생하거나 메시지가 보이지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필수 코드 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 전송하는 옵션에 flags: MessageFlags.IsComponentsV2를 포함해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { 
    Client, 
    MessageFlags, // [중요] V2 활성화를 위한 플래그
    InteractionReplyOptions,
    CommandInteraction
} from 'discord.js';

async function sendV2Message(interaction: CommandInteraction) {
    const payload: InteractionReplyOptions = {
        // 1. V2 모드 활성화 (필수)
        flags: MessageFlags.IsComponentsV2,

        // 2. 컴포넌트 배열
        // V2가 활성화되면 'content'나 'embeds' 필드는 사용할 수 없으며,
        // 오직 'components' 배열에만 내용을 담아야 합니다.
        components: [ /* 생성한 컴포넌트들을 이곳에 넣습니다 */ ]
    };

    return payload;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 컴포넌트 4가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;V2 레이아웃을 구성하는 주요 클래스들입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Container (컨테이너)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존의 Embed를 대체하는 최상위 컴포넌트입니다.&lt;/li&gt;
&lt;li&gt;이 안에 텍스트, 섹션, 구분선 등을 담아서 한 번에 보냅니다.&lt;/li&gt;
&lt;li&gt;setAccentColor로 좌측의 색상 띠를 설정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TextDisplay (텍스트 디스플레이)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면에 글자를 표시하는 컴포넌트입니다.&lt;/li&gt;
&lt;li&gt;# 제목, **굵게** 등 마크다운 문법을 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Section (섹션)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;텍스트와 &quot;액세서리(버튼 또는 썸네일)&quot;를 가로 한 줄에 배치할 때 사용합니다.&lt;/li&gt;
&lt;li&gt;텍스트 우측에 바로 버튼을 둘 수 있어 공간 활용에 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Separator (구분선)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 사이에 가로선을 긋거나, 여백(공백)을 추가하여 간격을 조절합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실전 예제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제 1: 기본 컨테이너 (기존 임베드 대체)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 형태입니다. Container 안에 TextDisplay를 넣어 메시지를 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { 
    ContainerBuilder, 
    TextDisplayBuilder, 
    MessageFlags 
} from 'discord.js';

export async function sendSimpleNotice(interaction: any) {
    // 1. 표시할 텍스트 생성
    const noticeText = new TextDisplayBuilder()
      .setContent(&quot;#   공지사항\n이번 업데이트로 UI가 변경되었습니다.&quot;);

    // 2. 컨테이너 생성 및 텍스트 추가
    const container = new ContainerBuilder()
      .setAccentColor(0x0099FF) // 파란색 띠 설정
      .addTextDisplayComponents(noticeText); // 위에서 만든 텍스트를 컨테이너에 추가

    // 3. 전송
    await interaction.reply({
        components: [container],
        flags: MessageFlags.IsComponentsV2 // [필수] 플래그 설정
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제 2: 섹션 활용 (텍스트 옆에 버튼 배치)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명 텍스트 바로 오른쪽에 버튼을 배치하는 방법입니다. SectionBuilder를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import { 
    ContainerBuilder, 
    SectionBuilder, 
    TextDisplayBuilder, 
    ButtonBuilder, 
    ButtonStyle, 
    MessageFlags 
} from 'discord.js';

export async function sendInlineButton(interaction: any) {
    // 1. 섹션 좌측에 들어갈 설명 텍스트
    const description = new TextDisplayBuilder()
      .setContent(&quot;**디스코드 봇 호스팅**\n월 5,000원에 이용해보세요.&quot;);

    // 2. 우측에 배치할 버튼
    const subscribeBtn = new ButtonBuilder()
      .setCustomId('sub_plan_basic')
      .setLabel('구독하기')
      .setStyle(ButtonStyle.Primary); // 보라색 버튼

    // 3. 섹션(Section) 생성 및 조합
    // 텍스트와 버튼을 하나의 '섹션'으로 묶습니다.
    const productSection = new SectionBuilder()
      .addTextDisplayComponents(description) // 텍스트 추가
      .setPrimaryButtonAccessory(subscribeBtn); // 버튼을 액세서리로 설정

    // 4. 컨테이너에 담기
    const container = new ContainerBuilder()
      .setAccentColor(0x5865F2)
      .addSectionComponents(productSection); // 텍스트 대신 섹션을 추가

    await interaction.reply({
        components: [container],
        flags: MessageFlags.IsComponentsV2
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제 3: 복합 레이아웃 (드롭다운과 구분선)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 컴포넌트를 조합하는 방법입니다. 드롭다운 메뉴(Select Menu)는 Section 내부에 넣을 수 없으므로, 별도의 ActionRow에 담아 Container에 추가해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { 
    ContainerBuilder, 
    TextDisplayBuilder, 
    SeparatorBuilder, 
    SeparatorSpacingSize,
    ActionRowBuilder,
    StringSelectMenuBuilder,
    MessageFlags 
} from 'discord.js';

export async function sendComplexLayout(interaction: any) {
    // 1. 제목 텍스트
    const title = new TextDisplayBuilder().setContent(&quot;###  ️ 설정 메뉴&quot;);

    // 2. 구분선 (실선)
    const line = new SeparatorBuilder()
      .setSpacing(SeparatorSpacingSize.Small) // 좁은 간격
      .setDivider(true); // true면 실선이 보임

    // 3. 드롭다운 메뉴 생성 (ActionRow 필요)
    const selectMenu = new StringSelectMenuBuilder()
      .setCustomId('config_menu')
      .setPlaceholder('변경할 설정을 선택하세요')
      .addOptions(
            { label: '알림 설정', value: 'noti' },
            { label: '언어 설정', value: 'lang' }
        );

    // 드롭다운은 ActionRow로 감싸야 컨테이너에 넣을 수 있습니다.
    const menuRow = new ActionRowBuilder().addComponents(selectMenu);

    // 4. 여백 (투명한 공백)
    const spacer = new SeparatorBuilder()
      .setSpacing(SeparatorSpacingSize.Large) // 넓은 간격
      .setDivider(false); // false면 선 없이 공백만 생김

    // 5. 바닥글 텍스트
    const footer = new TextDisplayBuilder()
      .setContent(&quot;설정은 즉시 저장됩니다.&quot;);

    // 6. 컨테이너에 순서대로 추가
    const container = new ContainerBuilder()
      .addTextDisplayComponents(title)       // 제목
      .addSeparatorComponents(line)          // 선
      .addActionRowComponents(menuRow)       // 드롭다운 메뉴
      .addSeparatorComponents(spacer)        // 공백
      .addTextDisplayComponents(footer);     // 바닥글

    await interaction.reply({
        components: [container],
        flags: MessageFlags.IsComponentsV2
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q. 기존 EmbedBuilder는 사용할 수 없나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 네, flags: MessageFlags.IsComponentsV2를 사용하면 기존 embeds 필드는 무시됩니다. V2 레이아웃을 사용하려면 ContainerBuilder를 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q. 섹션(Section) 안에 드롭다운 메뉴도 넣을 수 있나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 불가능합니다. 섹션의 액세서리 자리에는 버튼과 **썸네일(작은 이미지)**만 배치할 수 있습니다. 드롭다운 메뉴는 ActionRow에 담아서 컨테이너에 별도로 추가해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q. 큰 이미지는 어떻게 넣나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. MediaGallery 컴포넌트를 사용합니다. 컨테이너의 addMediaGalleryComponents() 메서드를 사용하여 이미지 URL들을 배열로 추가하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q. 모바일 화면 호환성은 어떤가요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. PC에서는 Section 내부의 텍스트와 버튼이 가로로 나란히 보이지만, 화면이 좁은 모바일 기기에서는 텍스트 아래로 버튼이 줄바꿈 되어 표시될 수 있습니다.&lt;/p&gt;</description>
      <category>봇 개발 팁/Discord.js</category>
      <author>디스호스트</author>
      <guid isPermaLink="true">https://dishost.tistory.com/49</guid>
      <comments>https://dishost.tistory.com/49#entry49comment</comments>
      <pubDate>Sat, 6 Dec 2025 15:58:40 +0900</pubDate>
    </item>
  </channel>
</rss>