디스코드 봇 만들기#33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉
한눈에 보기
#31의 임시 음성방에 파티 전용 채팅방, 방장 자동·수동 위임, 강퇴, 초대 전용(비공개) 방, 재시작해도 동작하는 버튼 패널을 얹어 파티방 봇을 완성해요. 채널 권한 덮어쓰기(set_permissions)를 켜고 지우는 방법이 핵심입니다.
- Python 3.10+ · discord.py 2.7.1 · python-dotenv 1.x 기준 — 봇 역할에 권한 관리(Manage Roles)가 꼭 있어야 해요
- 채팅방은 @everyone view_channel=False, 들어오면 True·나가면 overwrite=None으로 지금 파티에 있는 사람에게만 보여요
- 강퇴는 connect=False로 먼저 막고 move_to(None), 비공개는 @everyone connect=False + 지금 있는 사람에게 connect=True
- persistent view 3종 세트(timeout=None · 고정 custom_id · setup_hook의 add_view)로 재시작 후에도 패널 버튼이 동작해요
목차15개 항목
안녕하세요! Mojangcam입니다 😎
지난 #32에서는 지워진 메시지 내용과 누가 지웠는지까지 남기는 서버 로그(감사 로그) 채널을 만들었어요. 이번 편은 #31에서 예고했던 임시 음성방 확장편이에요. #31의 "들어오면 생기고 나가면 사라지는 음성방"에 아래 기능을 얹어서 파티방으로 키워볼게요.
- 🎉 로비에 들어가면 음성채널 + 그 방 사람만 보이는 전용 채팅방이 같이 생겨요
- 👑 방장 위임 — 방장이 나가면 남은 사람에게 자동으로 넘어가고, 명령어로 직접 넘길 수도 있어요
- 👢 강퇴 — 내보내고 다시 못 들어오게 막아요
- 🔒 초대 전용(비공개) 방 —
/파티 비공개로 잠그고/파티 초대로 부른 사람만 들어와요 - 🎛️ 채팅방 맨 위에 버튼 패널(비공개 전환·위임·강퇴)이 붙고, 봇을 재시작해도 버튼이 살아있어요
왜 채팅방까지 따로 만들까요? 🧠 음성채널에도 내장 채팅이 있긴 한데, 그 채널을 볼 수 있는 사람이면 누구나 읽을 수 있어요. 공개 파티방이면 서버 전체가 우리 파티 채팅을 보는 셈이죠. 이번 편의 채팅방은 지금 파티에 들어와 있는 사람에게만 보였다가 나가면 사라지는 진짜 전용 공간이에요. 게다가 방장 조작 패널을 붙여둘 자리도 생기고요.

📌 준비물
- Python 3.10 이상 —
dict[str, dict]표기와 최신 타입 힌트를 써요 - discord.py 2.7.1 (2026-09-26 기준 PyPI 최신) —
UserSelect는 2.1부터, 버튼 패널의 persistent view는 2.0부터예요 - python-dotenv 1.x
- 디스코드 개발자 포털에서 만든 봇 + 서버 초대 완료 (권한: 채널 관리, 권한 관리(Manage Roles), 멤버 이동, 멤버 음소거, 메시지 보내기, 링크 첨부)
pip install -U discord.py python-dotenv프로젝트 폴더에 .env 파일을 만들고 토큰을 넣어주세요. 토큰을 코드에 직접 쓰면 깃허브에 올리다 유출되기 딱 좋아요 😱
token=여기에_봇_토큰💡 .env는 꼭 .gitignore에 추가해두세요. (echo .env >> .gitignore)
⚠️ 개발자 포털 → Bot 탭에서 Server Members Intent를 켜주세요(guild.get_member로 멤버를 찾는 데 필요해요). 음성 상태(voice_states)는 특권 인텐트가 아니라서 코드에서만 켜면 됩니다. 그리고 이번 편은 채널 권한을 계속 고치기 때문에 봇 역할에 권한 관리(Manage Roles) 가 꼭 있어야 해요. #31에서는 채널 관리만으로 됐지만, set_permissions()는 이 권한이 없으면 403이 나요.
🧩 파티방은 이렇게 생겼어요
| 구성 | 역할 | 누가 볼 수 있나 |
|---|---|---|
🔊 OO의 파티 (음성채널) |
실제로 모이는 방. 비공개면 @everyone 접속 차단 |
공개: 모두 / 비공개: 초대받은 사람 + 이미 있던 사람 |
💬 💬oo-파티 (텍스트채널) |
파티 전용 채팅 + 맨 위에 방장 패널 | 지금 음성방에 있는 사람만 (나가면 안 보임) |
| 🎛️ 패널 메시지 | 🔒 비공개↔공개 / 👑 위임 / 👢 강퇴 버튼 | 방장만 누를 수 있음 |
데이터는 #31처럼 JSON 한 파일이에요. 다만 값이 방장 ID 하나에서 딕셔너리로 커졌어요.
{
"1420000000000000001": {
"guild": 1400000000000000000,
"owner": 1234567890,
"text": 1420000000000000002,
"private": false
}
}📌 1단계 - 기본 세팅과 조회 함수
# discord.py 2.7.1 / Python 3.10+ / python-dotenv 1.x
import os
import json
import asyncio
import discord
from discord import app_commands
from discord.ext import commands
from dotenv import load_dotenv
load_dotenv()
token = os.getenv("token")
LOBBY_CHANNEL_NAME = "➕ 파티방 만들기" # 여기 들어오면 새 파티방 생성
CATEGORY_NAME = "🎉 파티방" # 파티방(음성+채팅)이 생길 카테고리
DATA_FILE = "party_rooms.json"
intents = discord.Intents.default()
intents.voice_states = True # 음성 입퇴장 감지
intents.members = True # 멤버 조회(get_member)·표시 이름
# {음성채널ID(str): {"guild": 서버ID, "owner": 방장ID, "text": 채팅채널ID, "private": bool}}
rooms: dict[str, dict] = {}
lock = asyncio.Lock()
OWNER_PERMS = dict(manage_channels=True, move_members=True, mute_members=True)
def save():
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(rooms, f, ensure_ascii=False, indent=2)
def load():
global rooms
if os.path.exists(DATA_FILE):
with open(DATA_FILE, encoding="utf-8") as f:
rooms = json.load(f)
def get_room(member: discord.Member):
"""멤버가 지금 들어가 있는 파티방을 (음성채널, 정보)로 돌려줘요. 없으면 (None, None)"""
vs = member.voice
if vs and vs.channel and str(vs.channel.id) in rooms:
return vs.channel, rooms[str(vs.channel.id)]
return None, None
def room_by_text(channel_id: int):
"""채팅 채널 ID로 파티방을 찾아요 (패널 버튼용)"""
for vc_id, info in rooms.items():
if info["text"] == channel_id:
return vc_id, info
return None, None
def text_of(vc: discord.VoiceChannel):
return vc.guild.get_channel(rooms[str(vc.id)]["text"])하나씩 뜯어볼게요 💡
- OWNER_PERMS : 방장에게 주는 채널 권한 세 개를 딕셔너리로 빼뒀어요. 방을 만들 때도, 방장을 넘길 때도 같은 걸 쓰니까 한 곳에서 관리하는 거예요.
discord.PermissionOverwrite(**OWNER_PERMS)처럼 풀어서 넣습니다. - rooms의 "guild" : #31에는 없던 값이에요. 봇을 재시작했을 때 음성채널이 이미 지워져 있으면
bot.get_channel()로는 서버를 못 찾아서, 짝꿍 채팅방을 지울 수가 없거든요. 그래서 서버 ID를 같이 저장해요. - get_room / room_by_text : 슬래시 명령어는 "명령어 친 사람이 있는 음성방"으로, 패널 버튼은 "버튼이 눌린 채팅방"으로 파티방을 찾아요. 입구가 두 개라 조회 함수도 두 개예요.
📌 2단계 - 파티방 만들기·없애기
async def create_room(member: discord.Member):
guild = member.guild
category = discord.utils.get(guild.categories, name=CATEGORY_NAME)
if category is None:
category = await guild.create_category(CATEGORY_NAME)
vc = await guild.create_voice_channel(
f"{member.display_name}의 파티",
category=category,
overwrites={member: discord.PermissionOverwrite(**OWNER_PERMS)},
reason=f"{member} 파티방 생성",
)
tc = await guild.create_text_channel(
f"💬{member.display_name}-파티",
category=category,
overwrites={
guild.default_role: discord.PermissionOverwrite(view_channel=False),
guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True),
member: discord.PermissionOverwrite(view_channel=True),
},
reason=f"{member} 파티방 채팅 생성",
)
async with lock:
rooms[str(vc.id)] = {"guild": guild.id, "owner": member.id, "text": tc.id, "private": False}
save()
try:
await member.move_to(vc)
except discord.HTTPException: # 방 만드는 사이에 로비에서 나가버림
await delete_room(guild, str(vc.id))
return
await tc.send(embed=panel_embed(vc, member), view=PartyPanel())
async def delete_room(guild: discord.Guild, vc_id: str):
info = rooms.get(vc_id)
async with lock:
rooms.pop(vc_id, None)
save()
ids = [int(vc_id)] + ([info["text"]] if info else [])
for cid in ids:
ch = guild.get_channel(cid)
if ch is not None:
try:
await ch.delete(reason="파티방 종료")
except discord.NotFound:
pass
def panel_embed(vc: discord.VoiceChannel, owner: discord.Member) -> discord.Embed:
e = discord.Embed(
title=f"🎉 {vc.name}",
description="방장은 아래 버튼이나 `/파티` 명령어로 방을 관리할 수 있어요.\n"
"이 채팅방은 **지금 파티에 있는 사람만** 볼 수 있어요.",
color=discord.Color.blurple(),
)
e.add_field(name="방장", value=owner.mention)
e.add_field(name="상태", value="🔓 공개")
e.add_field(name="명령어", value="`/파티 초대` `/파티 강퇴` `/파티 위임` `/파티 비공개` `/파티 공개`", inline=False)
return e하나씩 뜯어볼게요 💡
- 채팅방 overwrites 세 줄 :
@everyone은 view_channel=False로 숨기고, 방장에게만True를 줘요. 가운데 guild.me(봇 자신)도 꼭 넣어야 해요. 안 그러면 봇이 만든 채널인데 봇이 못 보는 웃픈 상황이 생겨서 패널을 못 보내요. (봇 역할이 관리자면 없어도 되지만, 습관을 들이는 게 좋아요.) - 텍스트 채널 이름 : 디스코드가 텍스트 채널 이름을 소문자·공백은 하이픈으로 바꿔요. 한글과 이모지는 그대로 남아서
💬mojangcam-파티처럼 보여요. - move_to 실패 처리 : 채널 두 개를 만드는 데 1~2초가 걸리는데, 그 사이에 사용자가 로비에서 나가버리면
move_to가HTTPException을 던져요(#31 자주 나는 에러 3번). 그럴 땐 방금 만든 빈 방을 바로 지워요. - delete_room이 두 채널을 같이 지움 :
rooms에서 먼저 지우고(그래야 삭제 이벤트가 또 돌아도 무시돼요) 음성·채팅 채널을 차례로 지워요. 누가 손으로 먼저 지웠을 수도 있어서NotFound는 그냥 넘겨요.
📌 3단계 - 방장 위임·강퇴·비공개 함수
슬래시 명령어와 버튼이 같은 일을 하니까, 실제 동작은 함수로 빼두고 둘 다 이걸 부르게 할 거예요.
async def transfer_owner(vc: discord.VoiceChannel, new_owner: discord.Member):
info = rooms[str(vc.id)]
old = vc.guild.get_member(info["owner"])
if old is not None:
await vc.set_permissions(old, manage_channels=None, move_members=None, mute_members=None)
await vc.set_permissions(new_owner, **OWNER_PERMS)
async with lock:
info["owner"] = new_owner.id
save()
tc = text_of(vc)
if tc:
await tc.send(f"👑 이제 {new_owner.mention}님이 방장이에요!")
async def kick_member(vc: discord.VoiceChannel, member: discord.Member, reason: str):
await vc.set_permissions(member, connect=False, reason=reason) # 먼저 못 들어오게 막고
await member.move_to(None, reason=reason) # 음성에서 내보내기
async def set_private(vc: discord.VoiceChannel):
for m in vc.members: # 지금 있는 사람은 나갔다 와도 OK
await vc.set_permissions(m, connect=True)
await vc.set_permissions(vc.guild.default_role, connect=False)
async with lock:
rooms[str(vc.id)]["private"] = True
save()
async def set_public(vc: discord.VoiceChannel):
await vc.set_permissions(vc.guild.default_role, connect=None)
async with lock:
rooms[str(vc.id)]["private"] = False
save()하나씩 뜯어볼게요 💡
- set_permissions(대상, 권한=None) : 키워드 인자로 넘기면 그 대상의 기존 덮어쓰기(overwrite)에서 그 권한만 고쳐요. 옛 방장의 관리 권한 세 개만
None(서버 기본값으로 되돌림)으로 지우고, 혹시 초대받아 받은connect=True는 그대로 남기려는 거예요. 반대로overwrite=None으로 넘기면 그 대상의 덮어쓰기가 통째로 삭제돼요. - 강퇴 순서가 중요해요 :
move_to(None)이 먼저면 내보내자마자 다시 들어올 수 있어요. connect=False로 문을 잠근 뒤 내보내야 진짜 강퇴가 됩니다.move_to(None)은 "음성에서 끊기"예요. - set_private의 for문 :
@everyone의 connect를 끄면 새로 못 들어올 뿐, 이미 있는 사람은 안 튕겨요. 그런데 잠깐 나갔다 오면 못 들어오죠. 그래서 지금 있는 사람들에게 미리connect=True를 줘요. 다시 열 땐connect=None으로 서버 기본값으로 되돌리는 게 포인트!True로 넣으면 서버 설정을 무시하게 돼요.
📌 4단계 - 핵심! on_voice_state_update 세 갈래
class PartyBot(commands.Bot):
async def setup_hook(self):
self.add_view(PartyPanel()) # 재시작 후에도 패널 버튼이 살아있게 등록
self.tree.add_command(party)
await self.tree.sync()
bot = PartyBot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
load()
for vc_id in list(rooms):
guild = bot.get_guild(rooms[vc_id]["guild"])
vc = guild.get_channel(int(vc_id)) if guild else None
if guild and (vc is None or len(vc.members) == 0):
await delete_room(guild, vc_id)
print(f"✅ 로그인: {bot.user} / 파티방 {len(rooms)}개 복구")
@bot.event
async def on_voice_state_update(member: discord.Member,
before: discord.VoiceState,
after: discord.VoiceState):
if member.bot or before.channel == after.channel: # 음소거 등 채널 이동이 아닌 변화는 무시
return
# 1) 로비 입장 → 파티방 생성
if after.channel and after.channel.name == LOBBY_CHANNEL_NAME:
await create_room(member)
# 2) 파티방 입장 → 채팅방 보이게
if after.channel and str(after.channel.id) in rooms:
tc = text_of(after.channel)
if tc:
await tc.set_permissions(member, view_channel=True)
# 3) 파티방 퇴장 → 채팅방 숨기기 / 빈 방 삭제 / 방장 자동 위임
if before.channel and str(before.channel.id) in rooms:
vc = before.channel
if len(vc.members) == 0:
await delete_room(vc.guild, str(vc.id))
return
tc = text_of(vc)
if tc:
await tc.set_permissions(member, overwrite=None)
if rooms[str(vc.id)]["owner"] == member.id:
await transfer_owner(vc, vc.members[0])하나씩 뜯어볼게요 💡
- setup_hook :
commands.Bot을 상속해서 만든 메서드인데, 봇이 로그인 직후·이벤트 받기 전에 딱 한 번 실행돼요. 여기서add_view(PartyPanel())를 하면 봇이 재시작돼도 예전에 보낸 패널의 버튼이 계속 동작해요(6단계에서 자세히).on_ready는 재접속 때마다 여러 번 불릴 수 있어서 이런 등록 작업은setup_hook이 정석이에요. - before.channel == after.channel : 마이크 음소거·화면 공유만 해도 이 이벤트가 불려요. 채널이 안 바뀐 경우를 맨 위에서 걸러내면 뒤의 세 갈래가 훨씬 단순해져요.
- 2)와 3)이 동시에 참일 수 있어요 : A 파티방에서 B 파티방으로 바로 옮기면
before는 A,after는 B예요. 그래서elif가 아니라 독립된if로 두 갈래를 다 돌립니다. B 채팅방은 보이게 하고, A 채팅방은 숨기는 게 맞으니까요. - 방장 자동 위임 : 방장이 나갔는데 사람이 남아 있으면
vc.members[0](남은 사람 중 첫 번째)에게 넘겨요. #31 보너스 팁에서 한 줄로 예고했던 그 기능이에요. 이게 없으면 방장이 나간 방은 아무도 잠그거나 강퇴할 수 없는 "주인 없는 방"이 돼요.

📌 5단계 - 방장 전용 검사와 /파티 명령어 그룹
#31에서는 명령어마다 if not is_owner(...)를 반복했죠. 이번엔 #12 쿨다운·권한 편에서 본 app_commands.check로 데코레이터 한 줄로 줄여볼게요.
class NotInRoom(app_commands.CheckFailure):
pass
class NotOwner(app_commands.CheckFailure):
pass
def owner_only():
async def predicate(interaction: discord.Interaction) -> bool:
vc, info = get_room(interaction.user)
if vc is None:
raise NotInRoom()
if info["owner"] != interaction.user.id:
raise NotOwner()
return True
return app_commands.check(predicate)
party = app_commands.Group(name="파티", description="파티방 관리 (방장 전용)")
@party.command(name="초대", description="비공개 파티에 들어올 수 있게 초대해요")
@app_commands.describe(멤버="초대할 사람")
@owner_only()
async def invite(interaction: discord.Interaction, 멤버: discord.Member):
vc, _ = get_room(interaction.user)
await vc.set_permissions(멤버, connect=True, reason=f"{interaction.user} 초대")
tc = text_of(vc)
if tc:
await tc.set_permissions(멤버, view_channel=True)
try:
await 멤버.send(f"🎉 **{interaction.user.display_name}**님이 파티에 초대했어요! → {vc.jump_url}")
except discord.Forbidden: # DM을 막아둔 사람
pass
await interaction.response.send_message(f"✉️ {멤버.mention}님을 초대했어요!", ephemeral=True)
@party.command(name="강퇴", description="파티에서 내보내고 다시 못 들어오게 해요")
@app_commands.describe(멤버="내보낼 사람", 사유="감사 로그에 남을 사유")
@owner_only()
async def kick(interaction: discord.Interaction, 멤버: discord.Member, 사유: str = "사유 없음"):
vc, _ = get_room(interaction.user)
if 멤버 == interaction.user or 멤버 not in vc.members:
return await interaction.response.send_message("❌ 지금 파티에 있는 다른 사람만 내보낼 수 있어요.", ephemeral=True)
await kick_member(vc, 멤버, f"파티방 강퇴({interaction.user}): {사유}")
await interaction.response.send_message(f"👢 {멤버.mention}님을 내보냈어요.", ephemeral=True)
@party.command(name="위임", description="방장을 다른 사람에게 넘겨요")
@app_commands.describe(멤버="새 방장 (지금 파티에 있는 사람)")
@owner_only()
async def transfer(interaction: discord.Interaction, 멤버: discord.Member):
vc, _ = get_room(interaction.user)
if 멤버 == interaction.user or 멤버 not in vc.members:
return await interaction.response.send_message("❌ 지금 파티에 있는 다른 사람에게만 넘길 수 있어요.", ephemeral=True)
await transfer_owner(vc, 멤버)
await interaction.response.send_message(f"👑 {멤버.mention}님에게 방장을 넘겼어요.", ephemeral=True)
@party.command(name="비공개", description="초대 전용 파티로 바꿔요 (지금 있는 사람은 그대로)")
@owner_only()
async def private(interaction: discord.Interaction):
vc, _ = get_room(interaction.user)
await set_private(vc)
await interaction.response.send_message("🔒 초대 전용 파티가 됐어요. `/파티 초대`로 사람을 부르세요!", ephemeral=True)
@party.command(name="공개", description="누구나 들어올 수 있는 파티로 바꿔요")
@owner_only()
async def public(interaction: discord.Interaction):
vc, _ = get_room(interaction.user)
await set_public(vc)
await interaction.response.send_message("🔓 다시 공개 파티가 됐어요!", ephemeral=True)하나씩 뜯어볼게요 💡
- owner_only() : 검사 함수(predicate)가
False를 돌려주는 대신 직접 만든 예외를 던져요. 그러면 7단계의tree.error에서 "방 밖이에요"와 "방장이 아니에요"를 구분해서 안내할 수 있어요.app_commands.CheckFailure를 상속해야 에러 핸들러로 곱게 넘어갑니다. - 멤버: discord.Member : 인자 타입을 멤버로 두면 디스코드가 사람 선택 UI를 띄워줘요.
멤버 not in vc.members로 "지금 방에 있는 사람"만 받도록 한 번 더 걸러요. - vc.jump_url : 초대 DM에 채널 바로가기 링크를 넣었어요. DM에서는
#채널멘션이 제대로 안 보여서 링크가 안전해요. DM을 막아둔 사람에게는Forbidden이 나니까 조용히 넘깁니다. /파티 초대는 채팅방도 미리 열어줘요 : 초대받은 사람은 음성에 들어오기 전에도 파티 채팅방이 보여서 "지금 갈게요!" 같은 얘기를 미리 할 수 있어요. (나갔다가 다시 들어올 땐 4단계 2)가 알아서 다시 열어줘요.)
📌 6단계 - 채팅방 위 버튼 패널 (재시작해도 살아있는 버튼)
명령어를 외우기 귀찮은 방장을 위해 #5 버튼과 #6 드롭다운을 조합한 패널을 붙여요. 여기서 새로 배울 건 두 가지, UserSelect와 persistent view예요.
class PickMember(discord.ui.View):
"""버튼을 누르면 뜨는 '사람 고르기' 메뉴 (본인에게만 보임)"""
def __init__(self, vc: discord.VoiceChannel, action: str):
super().__init__(timeout=60)
self.vc, self.action = vc, action
@discord.ui.select(cls=discord.ui.UserSelect, placeholder="파티에 있는 사람을 골라주세요")
async def pick(self, interaction: discord.Interaction, select: discord.ui.UserSelect):
member = interaction.guild.get_member(select.values[0].id)
if member is None or member == interaction.user or member not in self.vc.members:
return await interaction.response.send_message("❌ 지금 파티에 있는 다른 사람만 고를 수 있어요.", ephemeral=True)
if self.action == "transfer":
await transfer_owner(self.vc, member)
text = f"👑 {member.mention}님에게 방장을 넘겼어요."
else:
await kick_member(self.vc, member, f"파티방 강퇴({interaction.user}): 패널")
text = f"👢 {member.mention}님을 내보냈어요."
await interaction.response.edit_message(content=text, view=None)
class PartyPanel(discord.ui.View):
"""파티 채팅방 맨 위에 붙는 방장 조작 패널 — timeout=None + custom_id 로 재시작 후에도 동작"""
def __init__(self):
super().__init__(timeout=None)
async def owner_room(self, interaction: discord.Interaction):
vc_id, info = room_by_text(interaction.channel.id)
vc = interaction.guild.get_channel(int(vc_id)) if vc_id else None
if vc is None:
await interaction.response.send_message("❌ 이미 끝난 파티예요.", ephemeral=True)
return None
if info["owner"] != interaction.user.id:
await interaction.response.send_message("❌ 방장만 누를 수 있어요.", ephemeral=True)
return None
return vc
@discord.ui.button(label="비공개 ↔ 공개", emoji="🔒", style=discord.ButtonStyle.secondary, custom_id="party:toggle")
async def toggle(self, interaction: discord.Interaction, button: discord.ui.Button):
vc = await self.owner_room(interaction)
if vc is None:
return
if rooms[str(vc.id)]["private"]:
await set_public(vc)
await interaction.response.send_message("🔓 누구나 들어올 수 있는 파티가 됐어요.")
else:
await set_private(vc)
await interaction.response.send_message("🔒 초대 전용 파티가 됐어요. `/파티 초대`로 사람을 부르세요!")
@discord.ui.button(label="방장 위임", emoji="👑", style=discord.ButtonStyle.primary, custom_id="party:transfer")
async def transfer_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
vc = await self.owner_room(interaction)
if vc is None:
return
await interaction.response.send_message("새 방장을 골라주세요.", view=PickMember(vc, "transfer"), ephemeral=True)
@discord.ui.button(label="강퇴", emoji="👢", style=discord.ButtonStyle.danger, custom_id="party:kick")
async def kick_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
vc = await self.owner_room(interaction)
if vc is None:
return
await interaction.response.send_message("내보낼 사람을 골라주세요.", view=PickMember(vc, "kick"), ephemeral=True)하나씩 뜯어볼게요 💡
- discord.ui.select(cls=discord.ui.UserSelect) : #6에서 쓴 일반 드롭다운은 항목을 우리가 채워야 했죠. UserSelect(2.1+)는 디스코드가 알아서 서버 멤버 검색창을 띄워줘요. 고른 결과는
select.values에Member또는User리스트로 들어오는데, 서버에 없는 사람이면User라서guild.get_member(...id)로 한 번 더 바꿔서None이면 걸러요. - persistent view 3종 세트 : ①
timeout=None② 버튼마다 고정 custom_id ③ 4단계setup_hook의add_view(). 이 셋이 갖춰지면 봇을 껐다 켜도 옛날 패널의 버튼이 계속 동작해요. 하나라도 빠지면 재시작 뒤에 "상호작용에 실패했습니다"가 떠요. 반면PickMember는 그때그때 새로 띄우는 메뉴라timeout=60으로 두고 60초 지나면 사라지게 뒀어요. - 패널이 방을 찾는 법 : 패널 메시지는 채널 ID 같은 정보를 안 갖고 있어요. 대신 "버튼이 눌린 채팅방"이 어느 파티방의 것인지
room_by_text()로 찾아요. 그래서 custom_id를party:toggle처럼 모든 방이 똑같이 써도 되고, 재시작 후에도add_view한 번으로 전부 커버됩니다. - edit_message(view=None) : 사람을 고르고 나면 드롭다운을 지우고 결과 문장만 남겨요. 안 지우면 같은 메뉴로 두 번 세 번 강퇴할 수 있어요.

📌 7단계 - 관리자 설정 명령어와 에러 처리
@bot.tree.command(name="파티설정", description="[관리자] 로비 채널과 카테고리를 만들어요")
@app_commands.checks.has_permissions(administrator=True)
async def setup(interaction: discord.Interaction):
guild = interaction.guild
category = discord.utils.get(guild.categories, name=CATEGORY_NAME)
if category is None:
category = await guild.create_category(CATEGORY_NAME)
lobby = discord.utils.get(guild.voice_channels, name=LOBBY_CHANNEL_NAME)
if lobby is None:
lobby = await guild.create_voice_channel(LOBBY_CHANNEL_NAME, category=category)
await interaction.response.send_message(
f"✅ 준비 완료! {lobby.mention}에 들어가면 파티방(음성+채팅)이 자동으로 생겨요.", ephemeral=True)
@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, NotInRoom):
msg = "❌ 파티방(임시 음성방) 안에서만 쓸 수 있어요."
elif isinstance(error, NotOwner):
msg = "❌ 방장만 쓸 수 있는 명령어예요."
elif isinstance(error, app_commands.MissingPermissions):
msg = "❌ 관리자만 쓸 수 있는 명령어예요."
else:
raise error
await interaction.response.send_message(msg, ephemeral=True)
bot.run(token)- tree.error에서 예외 종류별 안내 : 5단계에서 만든
NotInRoom·NotOwner가 여기로 와요. 안 잡으면 사용자에겐 "상호작용에 실패했습니다"만 떠서 뭘 잘못했는지 몰라요. #25 에러 핸들링 패턴 그대로예요. - 모르는 에러는 raise error로 다시 던져서 콘솔에 남겨요. 조용히 삼키면 디버깅이 지옥이 돼요.
📌 전체 코드 (복붙 OK)
party_bot.py로 저장하고 python party_bot.py로 실행하세요. 1~7단계를 순서대로 합친 것과 똑같아요.
# discord.py 2.7.1 / Python 3.10+ / python-dotenv 1.x
import os
import json
import asyncio
import discord
from discord import app_commands
from discord.ext import commands
from dotenv import load_dotenv
load_dotenv()
token = os.getenv("token")
# ── 설정 ──────────────────────────────────────────────
LOBBY_CHANNEL_NAME = "➕ 파티방 만들기" # 여기 들어오면 새 파티방 생성
CATEGORY_NAME = "🎉 파티방" # 파티방(음성+채팅)이 생길 카테고리
DATA_FILE = "party_rooms.json"
intents = discord.Intents.default()
intents.voice_states = True # 음성 입퇴장 감지
intents.members = True # 멤버 조회(get_member)·표시 이름
# {음성채널ID(str): {"guild": 서버ID, "owner": 방장ID, "text": 채팅채널ID, "private": bool}}
rooms: dict[str, dict] = {}
lock = asyncio.Lock()
OWNER_PERMS = dict(manage_channels=True, move_members=True, mute_members=True)
def save():
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(rooms, f, ensure_ascii=False, indent=2)
def load():
global rooms
if os.path.exists(DATA_FILE):
with open(DATA_FILE, encoding="utf-8") as f:
rooms = json.load(f)
def get_room(member: discord.Member):
"""멤버가 지금 들어가 있는 파티방을 (음성채널, 정보)로 돌려줘요. 없으면 (None, None)"""
vs = member.voice
if vs and vs.channel and str(vs.channel.id) in rooms:
return vs.channel, rooms[str(vs.channel.id)]
return None, None
def room_by_text(channel_id: int):
"""채팅 채널 ID로 파티방을 찾아요 (패널 버튼용)"""
for vc_id, info in rooms.items():
if info["text"] == channel_id:
return vc_id, info
return None, None
def text_of(vc: discord.VoiceChannel):
return vc.guild.get_channel(rooms[str(vc.id)]["text"])
# ── 파티방 만들기·없애기 ────────────────────────────────
async def create_room(member: discord.Member):
guild = member.guild
category = discord.utils.get(guild.categories, name=CATEGORY_NAME)
if category is None:
category = await guild.create_category(CATEGORY_NAME)
vc = await guild.create_voice_channel(
f"{member.display_name}의 파티",
category=category,
overwrites={member: discord.PermissionOverwrite(**OWNER_PERMS)},
reason=f"{member} 파티방 생성",
)
tc = await guild.create_text_channel(
f"💬{member.display_name}-파티",
category=category,
overwrites={
guild.default_role: discord.PermissionOverwrite(view_channel=False),
guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True),
member: discord.PermissionOverwrite(view_channel=True),
},
reason=f"{member} 파티방 채팅 생성",
)
async with lock:
rooms[str(vc.id)] = {"guild": guild.id, "owner": member.id, "text": tc.id, "private": False}
save()
try:
await member.move_to(vc)
except discord.HTTPException: # 방 만드는 사이에 로비에서 나가버림
await delete_room(guild, str(vc.id))
return
await tc.send(embed=panel_embed(vc, member), view=PartyPanel())
async def delete_room(guild: discord.Guild, vc_id: str):
info = rooms.get(vc_id)
async with lock:
rooms.pop(vc_id, None)
save()
ids = [int(vc_id)] + ([info["text"]] if info else [])
for cid in ids:
ch = guild.get_channel(cid)
if ch is not None:
try:
await ch.delete(reason="파티방 종료")
except discord.NotFound:
pass
def panel_embed(vc: discord.VoiceChannel, owner: discord.Member) -> discord.Embed:
e = discord.Embed(
title=f"🎉 {vc.name}",
description="방장은 아래 버튼이나 `/파티` 명령어로 방을 관리할 수 있어요.\n"
"이 채팅방은 **지금 파티에 있는 사람만** 볼 수 있어요.",
color=discord.Color.blurple(),
)
e.add_field(name="방장", value=owner.mention)
e.add_field(name="상태", value="🔓 공개")
e.add_field(name="명령어", value="`/파티 초대` `/파티 강퇴` `/파티 위임` `/파티 비공개` `/파티 공개`", inline=False)
return e
# ── 방장 위임·강퇴·비공개 (명령어와 버튼이 같이 쓰는 함수) ────
async def transfer_owner(vc: discord.VoiceChannel, new_owner: discord.Member):
info = rooms[str(vc.id)]
old = vc.guild.get_member(info["owner"])
if old is not None:
await vc.set_permissions(old, manage_channels=None, move_members=None, mute_members=None)
await vc.set_permissions(new_owner, **OWNER_PERMS)
async with lock:
info["owner"] = new_owner.id
save()
tc = text_of(vc)
if tc:
await tc.send(f"👑 이제 {new_owner.mention}님이 방장이에요!")
async def kick_member(vc: discord.VoiceChannel, member: discord.Member, reason: str):
await vc.set_permissions(member, connect=False, reason=reason) # 먼저 못 들어오게 막고
await member.move_to(None, reason=reason) # 음성에서 내보내기
async def set_private(vc: discord.VoiceChannel):
for m in vc.members: # 지금 있는 사람은 나갔다 와도 OK
await vc.set_permissions(m, connect=True)
await vc.set_permissions(vc.guild.default_role, connect=False)
async with lock:
rooms[str(vc.id)]["private"] = True
save()
async def set_public(vc: discord.VoiceChannel):
await vc.set_permissions(vc.guild.default_role, connect=None)
async with lock:
rooms[str(vc.id)]["private"] = False
save()
# ── 봇 본체 + 음성 입퇴장 ─────────────────────────────────
class PartyBot(commands.Bot):
async def setup_hook(self):
self.add_view(PartyPanel()) # 재시작 후에도 패널 버튼이 살아있게 등록
self.tree.add_command(party)
await self.tree.sync()
bot = PartyBot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
load()
for vc_id in list(rooms):
guild = bot.get_guild(rooms[vc_id]["guild"])
vc = guild.get_channel(int(vc_id)) if guild else None
if guild and (vc is None or len(vc.members) == 0):
await delete_room(guild, vc_id)
print(f"✅ 로그인: {bot.user} / 파티방 {len(rooms)}개 복구")
@bot.event
async def on_voice_state_update(member: discord.Member,
before: discord.VoiceState,
after: discord.VoiceState):
if member.bot or before.channel == after.channel: # 음소거 등 채널 이동이 아닌 변화는 무시
return
# 1) 로비 입장 → 파티방 생성
if after.channel and after.channel.name == LOBBY_CHANNEL_NAME:
await create_room(member)
# 2) 파티방 입장 → 채팅방 보이게
if after.channel and str(after.channel.id) in rooms:
tc = text_of(after.channel)
if tc:
await tc.set_permissions(member, view_channel=True)
# 3) 파티방 퇴장 → 채팅방 숨기기 / 빈 방 삭제 / 방장 자동 위임
if before.channel and str(before.channel.id) in rooms:
vc = before.channel
if len(vc.members) == 0:
await delete_room(vc.guild, str(vc.id))
return
tc = text_of(vc)
if tc:
await tc.set_permissions(member, overwrite=None)
if rooms[str(vc.id)]["owner"] == member.id:
await transfer_owner(vc, vc.members[0])
# ── 방장 전용 검사 + /파티 명령어 그룹 ────────────────────
class NotInRoom(app_commands.CheckFailure):
pass
class NotOwner(app_commands.CheckFailure):
pass
def owner_only():
async def predicate(interaction: discord.Interaction) -> bool:
vc, info = get_room(interaction.user)
if vc is None:
raise NotInRoom()
if info["owner"] != interaction.user.id:
raise NotOwner()
return True
return app_commands.check(predicate)
party = app_commands.Group(name="파티", description="파티방 관리 (방장 전용)")
@party.command(name="초대", description="비공개 파티에 들어올 수 있게 초대해요")
@app_commands.describe(멤버="초대할 사람")
@owner_only()
async def invite(interaction: discord.Interaction, 멤버: discord.Member):
vc, _ = get_room(interaction.user)
await vc.set_permissions(멤버, connect=True, reason=f"{interaction.user} 초대")
tc = text_of(vc)
if tc:
await tc.set_permissions(멤버, view_channel=True)
try:
await 멤버.send(f"🎉 **{interaction.user.display_name}**님이 파티에 초대했어요! → {vc.jump_url}")
except discord.Forbidden: # DM을 막아둔 사람
pass
await interaction.response.send_message(f"✉️ {멤버.mention}님을 초대했어요!", ephemeral=True)
@party.command(name="강퇴", description="파티에서 내보내고 다시 못 들어오게 해요")
@app_commands.describe(멤버="내보낼 사람", 사유="감사 로그에 남을 사유")
@owner_only()
async def kick(interaction: discord.Interaction, 멤버: discord.Member, 사유: str = "사유 없음"):
vc, _ = get_room(interaction.user)
if 멤버 == interaction.user or 멤버 not in vc.members:
return await interaction.response.send_message("❌ 지금 파티에 있는 다른 사람만 내보낼 수 있어요.", ephemeral=True)
await kick_member(vc, 멤버, f"파티방 강퇴({interaction.user}): {사유}")
await interaction.response.send_message(f"👢 {멤버.mention}님을 내보냈어요.", ephemeral=True)
@party.command(name="위임", description="방장을 다른 사람에게 넘겨요")
@app_commands.describe(멤버="새 방장 (지금 파티에 있는 사람)")
@owner_only()
async def transfer(interaction: discord.Interaction, 멤버: discord.Member):
vc, _ = get_room(interaction.user)
if 멤버 == interaction.user or 멤버 not in vc.members:
return await interaction.response.send_message("❌ 지금 파티에 있는 다른 사람에게만 넘길 수 있어요.", ephemeral=True)
await transfer_owner(vc, 멤버)
await interaction.response.send_message(f"👑 {멤버.mention}님에게 방장을 넘겼어요.", ephemeral=True)
@party.command(name="비공개", description="초대 전용 파티로 바꿔요 (지금 있는 사람은 그대로)")
@owner_only()
async def private(interaction: discord.Interaction):
vc, _ = get_room(interaction.user)
await set_private(vc)
await interaction.response.send_message("🔒 초대 전용 파티가 됐어요. `/파티 초대`로 사람을 부르세요!", ephemeral=True)
@party.command(name="공개", description="누구나 들어올 수 있는 파티로 바꿔요")
@owner_only()
async def public(interaction: discord.Interaction):
vc, _ = get_room(interaction.user)
await set_public(vc)
await interaction.response.send_message("🔓 다시 공개 파티가 됐어요!", ephemeral=True)
# ── 채팅방 위 버튼 패널 ───────────────────────────────────
class PickMember(discord.ui.View):
"""버튼을 누르면 뜨는 '사람 고르기' 메뉴 (본인에게만 보임)"""
def __init__(self, vc: discord.VoiceChannel, action: str):
super().__init__(timeout=60)
self.vc, self.action = vc, action
@discord.ui.select(cls=discord.ui.UserSelect, placeholder="파티에 있는 사람을 골라주세요")
async def pick(self, interaction: discord.Interaction, select: discord.ui.UserSelect):
member = interaction.guild.get_member(select.values[0].id)
if member is None or member == interaction.user or member not in self.vc.members:
return await interaction.response.send_message("❌ 지금 파티에 있는 다른 사람만 고를 수 있어요.", ephemeral=True)
if self.action == "transfer":
await transfer_owner(self.vc, member)
text = f"👑 {member.mention}님에게 방장을 넘겼어요."
else:
await kick_member(self.vc, member, f"파티방 강퇴({interaction.user}): 패널")
text = f"👢 {member.mention}님을 내보냈어요."
await interaction.response.edit_message(content=text, view=None)
class PartyPanel(discord.ui.View):
"""파티 채팅방 맨 위에 붙는 방장 조작 패널 — timeout=None + custom_id 로 재시작 후에도 동작"""
def __init__(self):
super().__init__(timeout=None)
async def owner_room(self, interaction: discord.Interaction):
vc_id, info = room_by_text(interaction.channel.id)
vc = interaction.guild.get_channel(int(vc_id)) if vc_id else None
if vc is None:
await interaction.response.send_message("❌ 이미 끝난 파티예요.", ephemeral=True)
return None
if info["owner"] != interaction.user.id:
await interaction.response.send_message("❌ 방장만 누를 수 있어요.", ephemeral=True)
return None
return vc
@discord.ui.button(label="비공개 ↔ 공개", emoji="🔒", style=discord.ButtonStyle.secondary, custom_id="party:toggle")
async def toggle(self, interaction: discord.Interaction, button: discord.ui.Button):
vc = await self.owner_room(interaction)
if vc is None:
return
if rooms[str(vc.id)]["private"]:
await set_public(vc)
await interaction.response.send_message("🔓 누구나 들어올 수 있는 파티가 됐어요.")
else:
await set_private(vc)
await interaction.response.send_message("🔒 초대 전용 파티가 됐어요. `/파티 초대`로 사람을 부르세요!")
@discord.ui.button(label="방장 위임", emoji="👑", style=discord.ButtonStyle.primary, custom_id="party:transfer")
async def transfer_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
vc = await self.owner_room(interaction)
if vc is None:
return
await interaction.response.send_message("새 방장을 골라주세요.", view=PickMember(vc, "transfer"), ephemeral=True)
@discord.ui.button(label="강퇴", emoji="👢", style=discord.ButtonStyle.danger, custom_id="party:kick")
async def kick_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
vc = await self.owner_room(interaction)
if vc is None:
return
await interaction.response.send_message("내보낼 사람을 골라주세요.", view=PickMember(vc, "kick"), ephemeral=True)
# ── 관리자용 세팅 + 에러 처리 ─────────────────────────────
@bot.tree.command(name="파티설정", description="[관리자] 로비 채널과 카테고리를 만들어요")
@app_commands.checks.has_permissions(administrator=True)
async def setup(interaction: discord.Interaction):
guild = interaction.guild
category = discord.utils.get(guild.categories, name=CATEGORY_NAME)
if category is None:
category = await guild.create_category(CATEGORY_NAME)
lobby = discord.utils.get(guild.voice_channels, name=LOBBY_CHANNEL_NAME)
if lobby is None:
lobby = await guild.create_voice_channel(LOBBY_CHANNEL_NAME, category=category)
await interaction.response.send_message(
f"✅ 준비 완료! {lobby.mention}에 들어가면 파티방(음성+채팅)이 자동으로 생겨요.", ephemeral=True)
@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, NotInRoom):
msg = "❌ 파티방(임시 음성방) 안에서만 쓸 수 있어요."
elif isinstance(error, NotOwner):
msg = "❌ 방장만 쓸 수 있는 명령어예요."
elif isinstance(error, app_commands.MissingPermissions):
msg = "❌ 관리자만 쓸 수 있는 명령어예요."
else:
raise error
await interaction.response.send_message(msg, ephemeral=True)
bot.run(token)💡 #31의 /음성방 이름·/음성방 인원 명령어도 그대로 쓰고 싶다면, @voice.command 대신 @party.command로 데코레이터만 바꾸고 is_owner 검사를 @owner_only()로 교체해서 붙여 넣으면 돼요.
📌 실행 결과
python party_bot.py실행 → 콘솔에✅ 로그인: 봇이름 / 파티방 0개 복구- 관리자가
/파티설정→ "🎉 파티방" 카테고리와 "➕ 파티방 만들기" 로비가 생겨요. - 로비에 들어가면 "내닉네임의 파티" 음성방 + "💬내닉네임-파티" 채팅방이 같이 생기고 자동으로 이동돼요. 채팅방 맨 위엔 버튼 3개짜리 패널이 붙어 있고요.
- 친구가 음성방에 들어오면 그 사람 화면에도 채팅방이 스르륵 나타나고, 나가면 사라져요. 다른 멤버 눈에는 채팅방이 애초에 안 보입니다.
- 🔒 버튼(또는
/파티 비공개)을 누르면@everyone접속이 막히고,/파티 초대 멤버:@친구로 부른 사람만 들어올 수 있어요. 초대받은 사람은 DM으로 바로가기 링크를 받아요. - 👢 강퇴 버튼 → 드롭다운에서 사람 선택 → 그 사람은 음성에서 튕기고 다시 못 들어와요.
- 방장이 나가면 채팅방에 "👑 이제 OO님이 방장이에요!"가 뜨고, 모두 나가면 음성방·채팅방이 둘 다 사라져요. 봇을 재시작해도 남아있던 패널 버튼이 그대로 동작합니다.
[이미지: 비공개로 바꾼 뒤 초대받지 않은 멤버가 음성방에 들어가려 할 때 뜨는 "이 채널에 연결할 권한이 없습니다" 안내]
💡 보너스 - 이렇게 확장해보세요
- 방 이름에 상태 표시 : 비공개가 되면
🔒를 이름 앞에 붙이고 싶죠? 다만 채널 이름 변경은 채널당 10분에 2번으로 제한이 빡빡해서 잠금·해제를 반복하면 429가 나요. 이름 대신 패널 임베드의 "상태" 필드를edit하는 쪽을 추천해요. - 파티 모집글 자동 게시 :
create_room끝에서 모집 채널에 "🎮 OO님이 파티를 열었어요 → 바로가기" 임베드를 보내면 #30 투표봇처럼 참여를 유도할 수 있어요. - 파티 활동 로그 : #32 로그봇의
send_log()에 강퇴·위임 이벤트를 흘려보내면 "누가 누구를 왜 강퇴했는지"가 서버 로그에 남아요.kick_member의reason이 그대로 감사 로그에도 찍힙니다. - JSON → aiosqlite : 서버가 여러 개고 파티가 수십 개씩 열린다면 #23 aiosqlite 편처럼 DB로 옮기세요.
rooms딕셔너리를 읽고 쓰는 곳이save()/load()와 몇 군데뿐이라 바꾸기 쉬워요.
📌 자주 나는 에러
discord.errors.Forbidden: 403 Forbidden (error code: 50013): Missing Permissions— set_permissions에서 → 원인: 봇 역할에 권한 관리(Manage Roles) 가 없음. 채널 권한 덮어쓰기는 채널 관리가 아니라 이 권한이 필요해요 → 해결: 서버 설정 → 역할 → 봇 역할에 권한 관리 체크. 그리고 봇은 자기가 가진 권한만 남에게 줄 수 있으니 멤버 이동·음소거도 봇에게 있어야 해요.- 패널을 보냈는데 채팅방이 텅 비어 있어요 /
Forbidden(tc.send) → 원인: 채팅방 overwrites에guild.me를 빼먹어서 봇 자신이 채널을 못 봄 → 해결: 2단계처럼guild.me: PermissionOverwrite(view_channel=True, send_messages=True)추가. - 봇 재시작 후 패널 버튼을 누르면 "상호작용에 실패했습니다" → 원인: persistent view 3종 세트 중 하나가 빠짐 —
timeout=None, 고정custom_id,setup_hook의add_view()→ 해결: 셋 다 확인.on_ready에add_view를 넣으면 재접속 때마다 중복 등록되니setup_hook에 두세요. TypeError: ... got an unexpected keyword argument 'cls'→ 원인: discord.py 2.0 이하라서ui.select(cls=UserSelect)가 없음(2.1부터) → 해결:pip install -U discord.py로 2.7.1로 올리기.- 강퇴했는데 바로 다시 들어와요 → 원인:
move_to(None)을 먼저 하고connect=False를 나중에 함, 또는 강퇴 대상이 관리자라서 채널 권한 덮어쓰기가 안 먹음 → 해결: 3단계 순서(막고 → 내보내기) 확인. 관리자는 채널 권한으로 막을 수 없는 게 디스코드 규칙이에요. AttributeError: 'User' object has no attribute 'voice'→ 원인: 명령어를 DM에서 쳐서interaction.user가Member가 아닌User로 들어옴 → 해결: 명령어 그룹에@app_commands.guild_only()를 붙이거나,owner_only안에서interaction.guild is None이면NotInRoom을 던지게 하기.- 방장이 나갔는데 위임 메시지가 안 떠요 → 원인: 방장이 나감과 동시에 방이 비어서
delete_room으로 먼저 빠짐(정상), 또는intents.members를 안 켜서get_member가None→ 해결: 남은 사람이 있는데도 안 뜨면 포털 Server Members Intent와intents.members = True를 확인.
🙌 마무리
오늘은 #31의 임시 음성방에 전용 채팅방, 방장 위임(자동+수동), 강퇴, 초대 전용 방, 재시작해도 살아있는 버튼 패널을 얹어서 진짜 쓸 만한 파티방 봇을 완성했어요. 정리하면 이래요.
- 채팅방은
@everyoneview_channel=False + 입장할 때True, 나갈 때overwrite=None으로 있는 사람에게만 보이게 한다 - 강퇴는 connect=False로 막은 다음
move_to(None), 비공개는@everyoneconnect=False + 지금 있는 사람에게 connect=True - 방장 권한은
set_permissions(대상, 권한=None)으로 그 권한만 지우고 새 방장에게 넘긴다 - persistent view 3종 세트(
timeout=None· 고정custom_id·setup_hook의add_view)와 UserSelect로 명령어 없이도 조작하는 패널
다음 편에서는 시리즈에서 오래 기다리셨던 Docker로 디스코드 봇 배포하기 — Dockerfile·docker compose·.env 주입·재시작 정책까지, 봇을 컨테이너에 담아 24시간 돌리는 법을 다뤄볼게요. 기대해주세요 🙌
여러분 서버 파티방엔 어떤 기능이 더 있으면 좋을까요? 댓글로 알려주시면 보너스에 추가해볼게요 😎
📚 디스코드 봇 만들기 시리즈 (최근 편)
- #25 봇이 죽지 않게! 에러 핸들링·로깅 체계 → /posts/discord-bot-25-error-handling/
- #26 티켓(Ticket) 문의 시스템 → /posts/discord-bot-26-ticket/
- #27 티켓 담당자 배정·우선순위 → /posts/discord-bot-27-ticket-assign/
- #28 레벨·경험치(XP) 시스템 → /posts/discord-bot-28-level-xp/
- #29 화폐·상점(Economy) 시스템 (초안 2026-09-17, 새 사이트 미발행)
- #30 투표·설문(Poll) 봇 → /posts/discord-bot-30-poll/
- #31 임시 음성채널(Temp Voice) 자동 생성·삭제 (티스토리 /119 비공개, 새 사이트 미발행 — 발행 후 링크 삽입)
- #32 서버 로그·감사(Audit Log) 채널 → /posts/discord-bot-32-audit-log/
- #33 임시 음성방 확장, 파티방 만들기 ← 지금 이 글!
- #34 Docker로 디스코드 봇 배포하기 (예정)
- 전체 로드맵(#1~#24 순서대로) → /posts/discord-bot-series-roadmap/
연재 · 32편 중 32번째
디스코드 봇 만들기
- #1디스코드 봇 만들기 - Slash Command 완전 정복!
- #2Slash Command에 옵션(Arguments) 추가하기!
- #3Slash Command에 서브 커맨드(Subcommand) 넣기!
- #4Slash Command로 임베드 전송하기!
- #5버튼(Button) 상호작용 구현하기!
- #6드롭다운(Select Menu) 메뉴 구현하기!
- #7모달(Modal)로 입력 받기 구현하기 (파이썬 예제)
- #8임베드(Embed) 이미지·필드로 꾸미기 (파이썬 예제)
- #9AI(Claude·ChatGPT API) 연동하기! (파이썬 예제)
- #10다국어(i18n) 지원하기! (파이썬 예제)
- #11플라스크(Flask)로 웹 대시보드 만들기! (파이썬 예제)
- #12명령어 쿨다운(Cooldown)·권한(Permission) 관리하기! (파이썬 예제)
- #13역할·채널 자동화로 서버 관리하기! (파이썬 예제)
- #14데이터 저장하기! JSON vs SQLite (파이썬 예제)
- #15슬래시 명령어 그룹(Group)으로 명령어 정리하기! (파이썬 예제)
- #16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제)
- #17RAG로 문서 기반 답변봇 만들기! 우리 서버 전용 FAQ 봇 (파이썬 예제)
- #18투표(Poll) 기능 만들기! 다수결로 정하는 서버 규칙 (파이썬 예제)
- #19Cog로 코드 쪼개기! 1000줄 봇도 안 더러워지는 구조 (파이썬 예제)
- #20자동 환영 메시지·온보딩 만들기! 규칙 동의 버튼으로 자동 역할 부여 (파이썬 예제)
- #21키워드 자동응답·금지어 필터 만들기! 경고 누적으로 자동 제재까지 (파이썬 예제)
- #22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제)
- #23aiosqlite로 비동기 DB 제대로 설계하기! JSON 탈출 (파이썬 예제)
- #24채널 난독화(Channel Obfuscation) 대응하기! 11/16부터 안 보이는 채널이 생겨요 (파이썬 예제)
- #25봇이 죽지 않게! 에러 핸들링·로깅 체계 잡기 (파이썬 예제)
- #26티켓(Ticket) 문의 시스템 만들기! 버튼 하나로 1:1 상담 채널 자동 생성 (파이썬 예제)
- #27티켓 담당자 배정·우선순위 만들기! 스태프가 여러 명이어도 안 꼬이게 (파이썬 예제)
- #28레벨·경험치(XP) 시스템 만들기! 채팅할수록 레벨업, 랭킹판까지 (파이썬 예제)
- #29화폐·상점(Economy) 시스템 만들기! 코인 벌어서 아이템 사기 (파이썬 예제)
- #30투표·설문(Poll) 봇 만들기! 네이티브 투표 + 버튼으로 실시간 집계까지 (파이썬 예제) 🗳️
- #32서버 로그·감사(Audit Log) 채널 만들기! 메시지 삭제·수정·입퇴장·추방까지 자동 기록 (파이썬 예제) 🕵️
- #33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉지금 읽는 글
