feat: /내계정 게스트 계정 조회 슬래시 커맨드 - #35
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
임시K 예비 계정 자동 채움. vitest include에 scripts 테스트 추가. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements the /내계정 slash command and a build script to pair event participants with guest accounts via a CSV file. The feedback suggests enhancing robustness by adding a try-catch block to the Discord interaction handler for graceful error handling, introducing validation in the build script to catch duplicate names or fields containing commas, and adding corresponding unit tests to verify these safety checks.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export function createMyAccountHandler(csvPath: string): EventHandler<'interactionCreate'> { | ||
| return { | ||
| event: 'interactionCreate', | ||
| handle: async (interaction: Interaction): Promise<void> => { | ||
| if (!interaction.isChatInputCommand() || interaction.commandName !== '내계정') { | ||
| return; | ||
| } | ||
| const name = interaction.options.getString('이름', true); | ||
| const content = buildAccountReply(await readCsv(csvPath), name); | ||
| await interaction.reply({ content, flags: MessageFlags.Ephemeral }); | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
디스코드 상호작용(Interaction) 응답 처리 중 발생할 수 있는 예외를 안전하게 처리하기 위해 try-catch 블록을 추가하는 것이 좋습니다. 설계 문서에 명시된 '핸들러 예외 발생 시 ephemeral 일반 오류 메시지 응답 및 콘솔 에러 로그 출력' 요구사항을 충족하고, 예기치 못한 봇 크래시나 Unhandled Rejection을 방지할 수 있습니다.
export function createMyAccountHandler(csvPath: string): EventHandler<'interactionCreate'> {
return {
event: 'interactionCreate',
handle: async (interaction: Interaction): Promise<void> => {
if (!interaction.isChatInputCommand() || interaction.commandName !== '내계정') {
return;
}
try {
const name = interaction.options.getString('이름', true);
const content = buildAccountReply(await readCsv(csvPath), name);
await interaction.reply({ content, flags: MessageFlags.Ephemeral });
} catch (error) {
console.error('Failed to handle /내계정 command:', error);
try {
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: '요청을 처리하는 중 오류가 발생했습니다. 운영자에게 문의하세요.',
flags: MessageFlags.Ephemeral,
});
}
} catch (replyError) {
console.error('Failed to send error reply:', replyError);
}
}
},
};
}| const names = pRows | ||
| .slice(1) | ||
| .map((r) => (r[nameIdx] ?? '').trim()) | ||
| .filter((n) => n.length > 0); | ||
| const accounts = aRows.slice(1).map((r) => ({ | ||
| username: (r[userIdx] ?? '').trim(), | ||
| password: (r[passIdx] ?? '').trim(), | ||
| })); | ||
|
|
||
| if (names.length > accounts.length) { | ||
| throw new Error(`참가자(${names.length})가 계정(${accounts.length})보다 많습니다.`); | ||
| } |
There was a problem hiding this comment.
참가자 명단에 동명이인(중복된 이름)이 존재하거나 이름/계정 정보에 쉼표(,)가 포함되어 있을 경우, 봇 런타임에서 조회가 불가능해지거나 단순 split(',') 파서가 오작동하여 계정 정보가 깨질 위험이 있습니다. 빌드 시점에 이를 미리 검증하고 에러를 발생시켜 운영자가 사전에 인지하고 수정할 수 있도록 안전 장치를 추가하는 것이 좋습니다.
const names = pRows
.slice(1)
.map((r) => (r[nameIdx] ?? '').trim())
.filter((n) => n.length > 0);
const duplicateNames = names.filter((item, index) => names.indexOf(item) !== index);
if (duplicateNames.length > 0) {
throw new Error('참가자 명단에 중복된 이름이 있습니다. 동명이인은 구분 가능하게 수정해야 합니다: ' + [...new Set(duplicateNames)].join(', '));
}
names.forEach((name) => {
if (name.includes(',')) {
throw new Error('이름에 쉼표(,)가 포함되어 있어 CSV 파싱에 문제가 발생할 수 있습니다: ' + name);
}
});
const accounts = aRows.slice(1).map((r) => {
const username = (r[userIdx] ?? '').trim();
const password = (r[passIdx] ?? '').trim();
if (username.includes(',') || password.includes(',')) {
throw new Error('계정 정보(username/password)에 쉼표(,)가 포함되어 있습니다: ' + username + ' / ' + password);
}
return { username, password };
});
if (names.length > accounts.length) {
throw new Error('참가자(' + names.length + ')가 계정(' + accounts.length + ')보다 많습니다.');
}| test('throws when there are more participants than accounts', () => { | ||
| const tooFew = `번호,role_id,role_name,password,username,id | ||
| 1,7,방문객,pw0000000001,guest0000001,10001`; | ||
| expect(() => buildAccountsCsv(PARTICIPANTS, tooFew)).toThrow(/참가자.*계정/); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
새롭게 추가된 동명이인 및 쉼표 포함 검증 로직에 대한 단위 테스트를 추가하여 빌드 스크립트의 안정성을 보장합니다.
| test('throws when there are more participants than accounts', () => { | |
| const tooFew = `번호,role_id,role_name,password,username,id | |
| 1,7,방문객,pw0000000001,guest0000001,10001`; | |
| expect(() => buildAccountsCsv(PARTICIPANTS, tooFew)).toThrow(/참가자.*계정/); | |
| }); | |
| }); | |
| test('throws when there are more participants than accounts', () => { | |
| const tooFew = `번호,role_id,role_name,password,username,id | |
| 1,7,방문객,pw0000000001,guest0000001,10001`; | |
| expect(() => buildAccountsCsv(PARTICIPANTS, tooFew)).toThrow(/참가자.*계정/); | |
| }); | |
| test('throws when there are duplicate names in participants', () => { | |
| const duplicates = '타임스탬프,"개인정보 수집, 이용에 동의합니다.",이름,소속,직업,기대,참가비 입금 확인\n2026-07-01,동의,조부용,SCG,개발자,네트워킹,네\n2026-07-01,동의,조부용,우테코,대학생,강연,네'; | |
| expect(() => buildAccountsCsv(duplicates, ACCOUNTS)).toThrow(/중복된 이름/); | |
| }); | |
| test('throws when a name contains a comma', () => { | |
| const commaName = '타임스탬프,"개인정보 수집, 이용에 동의합니다.",이름,소속,직업,기대,참가비 입금 확인\n2026-07-01,동의,"조,부용",SCG,개발자,네트워킹,네'; | |
| expect(() => buildAccountsCsv(commaName, ACCOUNTS)).toThrow(/쉼표/); | |
| }); | |
| }); |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n 안전화 - 커맨드명 '내계정'을 상수로 추출해 정의/가드 동기화 - interaction.reply 실패를 try/catch로 감싸 unhandled rejection 방지 - readCsv: ENOENT(행사후 삭제)는 조용히, 그 외 오류는 warn 로그 - build-accounts.mjs isMain을 pathToFileURL 정식 비교로 교체 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SSH 없이 .env와 동일 방식으로 CSV를 EC2에 배치. - create-accounts-csv composite action (선택적; 비면 스킵) - deploy.yml: GUEST_ACCOUNTS_CSV secret → data/accounts.csv, zip 조건부 포함, runner 정리 - deploy.sh: 배치된 CSV chmod 600 - 시크릿 비우고 재배포 시 EC2에서 제거되어 조회 차단(fail-closed) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
게스트 계정 확인 기능을 별도 독립 봇(network-account-bot)으로 분리하여 진행합니다. 마이그레이션 중인 메인 봇과 결합하지 않기 위해 이 PR은 닫습니다. (intents 정리는 #36에서 계속) |
무엇을
행사 참가자가 슬래시 커맨드
/내계정에 본인 이름을 입력하면, 배정된 게스트 네트워크 계정(username/password)을 본인만 보이는(ephemeral) 메시지로 확인하는 기능.2026 여름 초록 밋업(하루 행사)용. 슬래시 커맨드 프레임워크(#6) 없이 최소 구현했다 (커맨드가 이 하나뿐).
어떻게
src/config:GUEST_ACCOUNTS_CSV_PATHenv 추가 (기본data/accounts.csv)src/guest-accounts/csv.ts: CSV 파싱 + 이름 완전일치 조회 (found / not-found / 동명이인 방어)src/guest-accounts/command.ts:/내계정정의 + 응답 문구 +interactionCreate핸들러 (요청마다 파일 읽기 → ephemeral 응답)src/index.ts: 핸들러 연결 + ready 이후 guild scope 커맨드 등록scripts/build-accounts.mjs: 참가자 목록 + GuestManager 계정 export →name,username,password매핑 CSV 생성. 남는 계정은 현장 워크인용임시1..임시K로 자동 채움데이터/보안
rm하면 조회 자동 차단(fail-closed).gitignore로 커밋 차단, 예시(data/accounts.example.csv)만 커밋검증
운영자 후속 작업
node scripts/build-accounts.mjs <참가자.csv> <계정.csv> data/accounts.csv→ EC2 업로드 +chmod 600applications.commands스코프로 서버에 초대돼 있어야 슬래시 커맨드가 뜸rm data/accounts.csv🤖 Generated with Claude Code