-
Notifications
You must be signed in to change notification settings - Fork 0
[25.06.25 / TASK-214] Feature - 센트리 슬랙 연동 및 API 통합 #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,165 @@ | ||||||||||||||||||||||||||||||||||||||||||
| import { NextFunction, Request, RequestHandler, Response } from 'express'; | ||||||||||||||||||||||||||||||||||||||||||
| import { SlackService } from '@/services/slack.service'; | ||||||||||||||||||||||||||||||||||||||||||
| import { SentryService } from '@/services/sentry.service'; | ||||||||||||||||||||||||||||||||||||||||||
| import logger from '@/configs/logger.config'; | ||||||||||||||||||||||||||||||||||||||||||
| import { PermissionCheckResponseDto, SlackSuccessResponseDto } from '@/types'; | ||||||||||||||||||||||||||||||||||||||||||
| import { SentryActionData, SentryApiAction } from '@/types/models/Sentry.type'; | ||||||||||||||||||||||||||||||||||||||||||
| import { getNewStatusFromAction } from '@/utils/sentry.util'; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export class SlackController { | ||||||||||||||||||||||||||||||||||||||||||
| constructor( | ||||||||||||||||||||||||||||||||||||||||||
| private slackService: SlackService, | ||||||||||||||||||||||||||||||||||||||||||
| private sentryService: SentryService, | ||||||||||||||||||||||||||||||||||||||||||
| ) {} | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| checkPermissions: RequestHandler = async ( | ||||||||||||||||||||||||||||||||||||||||||
| req: Request, | ||||||||||||||||||||||||||||||||||||||||||
| res: Response<PermissionCheckResponseDto>, | ||||||||||||||||||||||||||||||||||||||||||
| next: NextFunction, | ||||||||||||||||||||||||||||||||||||||||||
| ): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| const permissions = await this.slackService.checkPermissions(); | ||||||||||||||||||||||||||||||||||||||||||
| const response = new PermissionCheckResponseDto(true, 'Slack 권한 확인 완료', permissions, null); | ||||||||||||||||||||||||||||||||||||||||||
| res.status(200).json(response); | ||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||
| logger.error('Slack 권한 확인 실패:', error instanceof Error ? error.message : '알 수 없는 오류'); | ||||||||||||||||||||||||||||||||||||||||||
| next(error); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| testBot: RequestHandler = async ( | ||||||||||||||||||||||||||||||||||||||||||
| req: Request, | ||||||||||||||||||||||||||||||||||||||||||
| res: Response<SlackSuccessResponseDto>, | ||||||||||||||||||||||||||||||||||||||||||
| next: NextFunction, | ||||||||||||||||||||||||||||||||||||||||||
| ): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| if (!this.slackService.hasBotToken() && !this.slackService.hasWebhookUrl()) { | ||||||||||||||||||||||||||||||||||||||||||
| const response = new SlackSuccessResponseDto( | ||||||||||||||||||||||||||||||||||||||||||
| false, | ||||||||||||||||||||||||||||||||||||||||||
| 'SLACK_BOT_TOKEN 또는 SLACK_WEBHOOK_URL 환경 변수가 설정되지 않았습니다.', | ||||||||||||||||||||||||||||||||||||||||||
| {}, | ||||||||||||||||||||||||||||||||||||||||||
| 'MISSING_SLACK_CONFIG' | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| res.status(400).json(response); | ||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const testMessage = { | ||||||||||||||||||||||||||||||||||||||||||
| text: '🤖 봇 테스트 메시지입니다!', | ||||||||||||||||||||||||||||||||||||||||||
| attachments: [ | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| color: 'good', | ||||||||||||||||||||||||||||||||||||||||||
| fields: [ | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| title: '테스트 결과', | ||||||||||||||||||||||||||||||||||||||||||
| value: '✅ Slack 연동이 정상적으로 작동합니다.', | ||||||||||||||||||||||||||||||||||||||||||
| short: false, | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||
| footer: `테스트 시간: ${new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })}`, | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| await this.slackService.sendMessage(testMessage); | ||||||||||||||||||||||||||||||||||||||||||
| const response = new SlackSuccessResponseDto(true, '봇 테스트 메시지 전송 완료!', {}, null); | ||||||||||||||||||||||||||||||||||||||||||
| res.status(200).json(response); | ||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||
| logger.error('봇 테스트 실패:', error instanceof Error ? error.message : '알 수 없는 오류'); | ||||||||||||||||||||||||||||||||||||||||||
| next(error); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| handleInteractive: RequestHandler = async ( | ||||||||||||||||||||||||||||||||||||||||||
| req: Request, | ||||||||||||||||||||||||||||||||||||||||||
| res: Response, | ||||||||||||||||||||||||||||||||||||||||||
| next: NextFunction, | ||||||||||||||||||||||||||||||||||||||||||
| ): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| const payload = JSON.parse(req.body.payload); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (payload.type === 'interactive_message' && payload.actions && payload.actions[0]) { | ||||||||||||||||||||||||||||||||||||||||||
| const action = payload.actions[0]; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (action.name === 'sentry_action') { | ||||||||||||||||||||||||||||||||||||||||||
| const [actionType, issueId, organizationSlug, projectSlug] = action.value.split(':'); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const actionData: SentryActionData = { | ||||||||||||||||||||||||||||||||||||||||||
| action: actionType as SentryApiAction, | ||||||||||||||||||||||||||||||||||||||||||
| issueId, | ||||||||||||||||||||||||||||||||||||||||||
| organizationSlug, | ||||||||||||||||||||||||||||||||||||||||||
| projectSlug, | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (actionData.issueId && actionData.organizationSlug && actionData.projectSlug) { | ||||||||||||||||||||||||||||||||||||||||||
| logger.info('Processing Sentry action:', actionData); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const result = await this.sentryService.handleIssueAction(actionData); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (result.success) { | ||||||||||||||||||||||||||||||||||||||||||
| const updatedMessage = this.createSuccessMessage(actionData, payload.original_message || {}); | ||||||||||||||||||||||||||||||||||||||||||
| res.json(updatedMessage); | ||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||
| const errorMessage = this.createErrorMessage(result.error || 'Unknown error', payload.original_message || {}); | ||||||||||||||||||||||||||||||||||||||||||
| res.json(errorMessage); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| res.json({ text: '❌ 잘못된 요청입니다.' }); | ||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||
| logger.error('Interactive 처리 실패:', error instanceof Error ? error.message : '알 수 없는 오류'); | ||||||||||||||||||||||||||||||||||||||||||
| next(error); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private createSuccessMessage(actionData: SentryActionData, originalMessage: unknown): unknown { | ||||||||||||||||||||||||||||||||||||||||||
| const { action } = actionData; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const updatedMessage = JSON.parse(JSON.stringify(originalMessage)); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
| const updatedMessage = JSON.parse(JSON.stringify(originalMessage)); | |
| const updatedMessage = structuredClone(originalMessage); |
🤖 Prompt for AI Agents
In src/controllers/slack.controller.ts at line 121, replace the inefficient deep
copy using JSON.parse(JSON.stringify()) with a more robust method such as
structuredClone if available, or use a utility function from a library like
lodash's cloneDeep to handle deep copying safely and efficiently, especially to
support circular references and functions.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
옵셔널 체이닝 사용 권장
정적 분석 도구에서 제안한 대로 옵셔널 체이닝을 사용하여 코드를 더 안전하게 만들어주세요.
- if (updatedMessage.attachments && updatedMessage.attachments[0]) {
+ if (updatedMessage.attachments?.[0]) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (updatedMessage.attachments && updatedMessage.attachments[0]) { | |
| const newStatus = getNewStatusFromAction(action); | |
| const statusColors = { | |
| 'resolved': 'good', | |
| 'ignored': 'warning', | |
| 'archived': '#808080', | |
| 'unresolved': 'danger', | |
| }; | |
| updatedMessage.attachments[0].color = statusColors[newStatus as keyof typeof statusColors] || 'good'; | |
| if (updatedMessage.attachments?.[0]) { | |
| const newStatus = getNewStatusFromAction(action); | |
| const statusColors = { | |
| 'resolved': 'good', | |
| 'ignored': 'warning', | |
| 'archived': '#808080', | |
| 'unresolved': 'danger', | |
| }; | |
| updatedMessage.attachments[0].color = statusColors[newStatus as keyof typeof statusColors] || 'good'; |
🧰 Tools
🪛 Biome (1.9.4)
[error] 128-131: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🤖 Prompt for AI Agents
In src/controllers/slack.controller.ts around lines 123 to 132, the code
accesses updatedMessage.attachments[0] without optional chaining, which can
cause runtime errors if attachments or the first element is undefined. Update
the code to use optional chaining when accessing attachments and its first
element to safely handle cases where these might be undefined, improving code
safety and preventing potential crashes.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
delete 연산자 사용 지양
delete 연산자는 성능에 부정적 영향을 미칩니다. undefined 할당을 사용해주세요.
- delete updatedMessage.attachments[0].actions;
+ updatedMessage.attachments[0].actions = undefined;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| delete updatedMessage.attachments[0].actions; | |
| updatedMessage.attachments[0].actions = undefined; |
🤖 Prompt for AI Agents
In src/controllers/slack.controller.ts at line 144, replace the use of the
delete operator on updatedMessage.attachments[0].actions with assigning
undefined to updatedMessage.attachments[0].actions to avoid performance issues
caused by delete.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
깊은 복사 방식 개선 (에러 메시지 생성)
여기서도 동일한 비효율적인 복사 방식이 사용되고 있습니다.
- const updatedMessage = JSON.parse(JSON.stringify(originalMessage));
+ const updatedMessage = structuredClone(originalMessage);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const updatedMessage = JSON.parse(JSON.stringify(originalMessage)); | |
| const updatedMessage = structuredClone(originalMessage); |
🧰 Tools
🪛 Biome (1.9.4)
[error] 151-153: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
🤖 Prompt for AI Agents
In src/controllers/slack.controller.ts at line 151, the code uses
JSON.parse(JSON.stringify(originalMessage)) for deep copying, which is
inefficient and can cause issues with certain data types. Replace this with a
more efficient and reliable deep copy method, such as using a utility function
like lodash's cloneDeep or a custom deep copy function that handles complex
objects properly.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
옵셔널 체이닝 사용 권장 (에러 메시지)
에러 메시지 생성 부분에서도 옵셔널 체이닝을 사용해주세요.
- if (updatedMessage.attachments && updatedMessage.attachments[0]) {
+ if (updatedMessage.attachments?.[0]) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (updatedMessage.attachments && updatedMessage.attachments[0]) { | |
| updatedMessage.attachments[0].fields.push({ | |
| title: '❌ 오류 발생', | |
| value: error, | |
| short: false, | |
| }); | |
| updatedMessage.attachments[0].color = 'danger'; | |
| } | |
| if (updatedMessage.attachments?.[0]) { | |
| updatedMessage.attachments[0].fields.push({ | |
| title: '❌ 오류 발생', | |
| value: error, | |
| short: false, | |
| }); | |
| updatedMessage.attachments[0].color = 'danger'; | |
| } |
🤖 Prompt for AI Agents
In src/controllers/slack.controller.ts around lines 153 to 161, the error
message assignment uses a direct reference to the error variable without
optional chaining. Update the code to use optional chaining when accessing the
error message to safely handle cases where the error might be undefined or null,
preventing potential runtime errors.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import { NextFunction, Request, RequestHandler, Response } from 'express'; | ||
| import { SlackService } from '@/services/slack.service'; | ||
| import { SentryService } from '@/services/sentry.service'; | ||
| import { SentryWebhookData, SlackMessage } from '@/types'; | ||
| import logger from '@/configs/logger.config'; | ||
| import { formatSentryIssueForSlack, createStatusUpdateMessage } from '@/utils/slack.util'; | ||
|
|
||
| export class WebhookController { | ||
| constructor( | ||
| private slackService: SlackService, | ||
| private sentryService: SentryService, | ||
| ) {} | ||
|
|
||
| handleSentryWebhook: RequestHandler = async ( | ||
| req: Request, | ||
| res: Response, | ||
| next: NextFunction, | ||
| ): Promise<void> => { | ||
| try { | ||
| const sentryData = req.body; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 웹훅 페이로드에 대한 입력 검증 추가 필요
다음과 같은 개선사항을 제안합니다: handleSentryWebhook: RequestHandler = async (
req: Request,
res: Response,
next: NextFunction,
): Promise<void> => {
try {
- const sentryData = req.body;
+ const sentryData = req.body as SentryWebhookData;
+
+ // 웹훅 페이로드 기본 검증
+ if (!sentryData || !sentryData.action || !sentryData.data) {
+ logger.warn('유효하지 않은 Sentry 웹훅 페이로드:', sentryData);
+ res.status(400).json({ message: 'Invalid webhook payload' });
+ return;
+ }
const slackMessage = await this.formatSentryDataForSlack(sentryData);🤖 Prompt for AI Agents |
||
|
|
||
| const slackMessage = await this.formatSentryDataForSlack(sentryData); | ||
|
|
||
| if (slackMessage === null) { | ||
| logger.info('기존 메시지 업데이트 완료, 새 메시지 전송 생략'); | ||
| res.status(200).json({ message: 'Webhook processed successfully' }); | ||
| return; | ||
| } | ||
|
|
||
| const issueId = sentryData.data?.issue?.id; | ||
| await this.slackService.sendMessage(slackMessage, issueId); | ||
|
|
||
| res.status(200).json({ message: 'Webhook processed successfully' }); | ||
| } catch (error) { | ||
| logger.error('Sentry webhook 처리 실패:', error instanceof Error ? error.message : '알 수 없는 오류'); | ||
| next(error); | ||
| } | ||
| }; | ||
|
|
||
| private async formatSentryDataForSlack(sentryData: SentryWebhookData): Promise<SlackMessage | null> { | ||
| const { action, data } = sentryData; | ||
|
|
||
| if (action === 'resolved' || action === 'unresolved' || action === 'ignored') { | ||
| return await this.handleIssueStatusChange(sentryData); | ||
| } | ||
|
|
||
| if (action === 'created' && data.issue) { | ||
| return formatSentryIssueForSlack(sentryData, this.sentryService.hasSentryToken()); | ||
| } | ||
|
|
||
| return { | ||
| text: `🔔 Sentry 이벤트: ${action || 'Unknown action'}`, | ||
| attachments: [ | ||
| { | ||
| color: 'warning', | ||
| fields: [ | ||
| { | ||
| title: '이벤트 타입', | ||
| value: action || 'Unknown', | ||
| short: true, | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| private async handleIssueStatusChange(sentryData: SentryWebhookData): Promise<SlackMessage | null> { | ||
| const { data } = sentryData; | ||
| const issue = data.issue; | ||
|
|
||
| if (!issue) { | ||
| logger.warn('이슈 정보가 없습니다:', sentryData); | ||
| return formatSentryIssueForSlack(sentryData, this.sentryService.hasSentryToken()); | ||
| } | ||
|
|
||
| logger.info(`이슈 상태 변경 감지: ${issue.id} → ${sentryData.action}`); | ||
|
|
||
| const messageInfo = this.slackService.getMessageInfo(issue.id); | ||
|
|
||
| if (messageInfo) { | ||
| logger.info('기존 메시지 발견, 업데이트 시도'); | ||
|
|
||
| try { | ||
| const updatedMessage = createStatusUpdateMessage( | ||
| sentryData, | ||
| this.sentryService.hasSentryToken() | ||
| ); | ||
|
|
||
| await this.slackService.updateMessage( | ||
| messageInfo.channel, | ||
| messageInfo.ts, | ||
| updatedMessage | ||
| ); | ||
|
|
||
| logger.info('기존 메시지 업데이트 완료'); | ||
| return null; | ||
|
|
||
| } catch (error) { | ||
| logger.error('메시지 업데이트 실패, 새 메시지로 전송:', error instanceof Error ? error.message : '알 수 없는 오류'); | ||
|
|
||
| } | ||
| } else { | ||
| logger.info('기존 메시지 없음, 새 메시지 생성'); | ||
| } | ||
|
|
||
| return formatSentryIssueForSlack(sentryData, this.sentryService.hasSentryToken()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import express, { Router } from 'express'; | ||
| import { SlackController } from '@/controllers/slack.controller'; | ||
| import { SentryService } from '@/services/sentry.service'; | ||
| import { SlackService } from '@/services/slack.service'; | ||
|
|
||
| const router: Router = express.Router(); | ||
|
|
||
| const slackService = new SlackService(); | ||
| const sentryService = new SentryService(); | ||
| const slackController = new SlackController(slackService, sentryService); | ||
|
||
|
|
||
| /** | ||
| * @swagger | ||
| * /slack/check-permissions: | ||
| * get: | ||
| * summary: Slack 권한 확인 | ||
| * description: Slack Bot의 권한 상태를 확인합니다. | ||
| * tags: [Slack] | ||
| * responses: | ||
| * 200: | ||
| * description: 권한 확인 성공 | ||
| * content: | ||
| * application/json: | ||
| * schema: | ||
| * $ref: '#/components/schemas/PermissionCheckResponseDto' | ||
| * 400: | ||
| * description: Bot Token 미설정 | ||
| * 500: | ||
| * description: 서버 오류 | ||
| */ | ||
| router.get('/slack/check-permissions', slackController.checkPermissions); | ||
|
||
|
|
||
| /** | ||
| * @swagger | ||
| * /slack/test-bot: | ||
| * post: | ||
| * summary: 봇 테스트 | ||
| * description: Slack Bot 테스트 메시지를 전송합니다. | ||
| * tags: [Slack] | ||
| * responses: | ||
| * 200: | ||
| * description: 테스트 메시지 전송 성공 | ||
| * content: | ||
| * application/json: | ||
| * schema: | ||
| * $ref: '#/components/schemas/SlackSuccessResponseDto' | ||
| * 400: | ||
| * description: Slack 연동 미설정 | ||
| * 500: | ||
| * description: 서버 오류 | ||
| */ | ||
| router.post('/slack/test-bot', slackController.testBot); | ||
|
|
||
| /** | ||
| * @swagger | ||
| * /slack/interactive: | ||
| * post: | ||
| * summary: Slack Interactive Components 처리 | ||
| * description: Slack에서 전송되는 버튼 클릭 등의 상호작용을 처리합니다. | ||
| * tags: [Slack] | ||
| * requestBody: | ||
| * required: true | ||
| * content: | ||
| * application/x-www-form-urlencoded: | ||
| * schema: | ||
| * type: object | ||
| * properties: | ||
| * payload: | ||
| * type: string | ||
| * description: JSON 형태의 Slack payload (URL encoded) | ||
| * responses: | ||
| * 200: | ||
| * description: 상호작용 처리 성공 | ||
| * content: | ||
| * application/json: | ||
| * schema: | ||
| * type: object | ||
| * properties: | ||
| * text: | ||
| * type: string | ||
| * example: "버튼 클릭 처리 완료" | ||
| * response_type: | ||
| * type: string | ||
| * enum: [in_channel, ephemeral] | ||
| * 400: | ||
| * description: 잘못된 요청 | ||
| */ | ||
| router.post('/slack/interactive', slackController.handleInteractive); | ||
|
|
||
| export default router; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON 파싱에 에러 처리 추가 필요
JSON.parse에 대한 에러 처리가 없어 잘못된 페이로드로 인해 서버가 크래시될 수 있습니다.다음과 같이 수정해주세요:
📝 Committable suggestion
🤖 Prompt for AI Agents