당신의 커맨드를
풀문 봇 안에 붙이세요

풀문 디스코드 봇은 외부 개발자의 모듈을 직접 실행해요. 슬래시 커맨드 한 개에서 버튼 인터랙션과 주기 작업까지 — 파일 하나의 export 셋이면 충분해요. 운영자가 한 줄씩 심사하고 나면, 봇은 재시작조차 없이 모듈을 받아들여요.

무엇을 붙일 수 있어요

슬래시 커맨드

execute(interaction, ctx) 하나로 완성되는 기본형이에요. 옵션은 25개까지, 한글 이름도 그대로 돼요.

버튼 인터랙션

모듈이 렌더링한 버튼의 클릭이 onButton으로 돌아와요. customId는 ctx.componentId()가 모듈 전용 네임스페이스로 만들어줘요.

주기 작업

schedules에 간격과 함수를 선언하면 봇이 살아 있는 동안 주기적으로 실행돼요. 최소 5분, 모듈당 2개까지.

원장 읽기

ctx.economy로 잔액·순위·거래·이벤트·카지노 통계를 읽어요. economy-api와 같은 감사된 SELECT만 제공되고, 쓰기 경로는 존재하지 않아요.

모듈의 기본 골격

파일 하나에 named export 세 개. 이게 전부예요. 완성된 모듈은 저장소의 dice-fortune.mjs에서 볼 수 있어요.

modules/my-fortune.mjs
export const meta = { author: '닉네임', version: '1.0.0' };

export const command = {
  name: '운세',                          // 소문자·숫자·한글 1~32자
  description: '오늘의 운세를 봐요',       // 1~100자
  options: [],
};

export async function execute(interaction, ctx) {
  await ctx.reply(`🔮 ${ctx.user.displayName}님, 오늘은 무리하지 말아요.`);
}

원장을 읽어요 — ctx.economy

계정 id를 생략하면 커맨드를 호출한 유저가 기본값이에요. 범위 밖 값은 에러가 아니라 경계값으로 조정돼요.

순위 커맨드 예시
export async function execute(interaction, ctx) {
  const w = await ctx.economy.wallet();
  if (!w) return ctx.reply('먼저 /연동코드로 계정을 만들어 주세요.');
  // 잔액 0이면 rank가 null이에요 — 서버 순위는 잔액이 있을 때만 표시.
  const rank = w.rank ? ` (서버 ${w.rank}위)` : '';
  await ctx.reply(`**${ctx.user.displayName}님** — ${w.balance.toLocaleString('ko-KR')}원${rank}`);
}
ctx.economy 메서드
메서드반환설명
wallet(id?)잔액 + 서버 순위계정이 없으면 null
transactions(id?, {limit, before})거래 페이지 + 커서최신순, limit 1~50
leaderboard(limit?)상위권 목록1~50명
events()진행 중 이벤트최대 25개
guilds()길드 + 기금 + 인원기금 순, 최대 25개
casinoToday()오늘(UTC) 게임별 집계wagered / paidOut / netBurn
casinoHistory(days?)일별 집계1~90일

선택 훅 — 버튼과 스케줄

필요할 때만 export하면 돼요. 없으면 무시돼요.

버튼: onButton

customId는 반드시 ctx.componentId(action)로 만드세요 — cm:커맨드이름:action 네임스페이스로 클릭이 이 모듈로만 돌아와요. 3초 안에 update()·reply()·deferUpdate() 중 하나를 호출해야 해요.

버튼 달기 + 클릭 처리
export async function execute(interaction, ctx) {
  await ctx.reply({
    content: '주사위를 굴렸어요!',
    components: [{ type: 1, components: [{
      type: 2, style: 1,
      customId: ctx.componentId('다시'),   // ctx가 네임스페이스를 보장해요
      label: '한 번 더',
    }] }],
  });
}

export async function onButton(interaction, ctx) {
  await interaction.update({ content: `다시 굴려서 ${1 + Math.floor(Math.random() * 6)}이 나왔어요!` });
}

스케줄: schedules

봇이 살아 있는 동안 주기적으로 도는 타이머예요. 모듈을 고치면 재시작 없이 새 타이머로 교체돼요.

30분마다 랭킹 요약 보내기
export const schedules = [{
  everyMinutes: 30,   // 5 이상의 정수, 모듈당 최대 2개
  run: async (tools) => {
    const top = await tools.economy.leaderboard(3);
    await tools.send('채널ID', `현재 1위: ${top[0]?.mcUsername ?? '없음'}`);
  },
}];

tools.send는 텍스트 채널 전용이고, 멘션은 유저 멘션만 허용돼요(@everyone·역할 멘션은 봇이 걸러줘요). 내용은 2000자로 잘려요.

제출은 이렇게 해요

  1. 만들어요 — 저장소의 template/에서 골격을 복사하거나 위 예시를 고쳐 써요. 파일은 500줄 이하로 유지해 주세요.
  2. 보내요 — git이 편하면 PR, 아니면 이슈 템플릿으로 코드를 붙여넣어도 돼요(운영자가 대신 반영해요).
  3. 심사를 받아요 — 운영자가 REVIEW.md 체크리스트대로 한 줄씩 읽어요. 모듈은 봇 프로세스 안에서 실행되기 때문에, 이 심사가 유일한 보안 경계예요.
  4. 활성화돼요 — 머지되면 운영자가 모듈을 봇에 적재하고, 목록에서 활성화해요. 이후 수정은 재시작 없이 자동으로 다시 로드돼요.

지켜야 할 것들

대부분은 로더와 심사가 막아주지만, 처음부터 알면 수정 없이 통과해요.

규칙설명
import는 discord.js와 node: 빌트인만서드파티 패키지는 거절돼요
eval·new Function·동적 import(변수) 금지난독화·minified 코드도 마찬가지예요
fetch·child_process·process.env 접근 금지외부 호출이 필요하면 심사에서 상의해요
원장 쓰기 금지읽기는 ctx.economy로만 — 자체 SQL은 불가해요
커맨드·옵션 이름은 소문자·숫자·한글대문자(라틴 포함 모든 문자)는 등록이 거부돼요
ctx.economy 대량 반복 호출 자제원장 쿼리는 같은 DB를 읽어요