1+ import { NextFunction , Request , RequestHandler , Response } from 'express' ;
2+ import { SlackService } from '@/services/slack.service' ;
3+ import { SentryService } from '@/services/sentry.service' ;
4+ import logger from '@/configs/logger.config' ;
5+ import { PermissionCheckResponseDto , SlackSuccessResponseDto } from '@/types' ;
6+ import { SentryActionData , SentryApiAction } from '@/types/models/Sentry.type' ;
7+ import { getNewStatusFromAction } from '@/utils/sentry.util' ;
8+
9+ export class SlackController {
10+ constructor (
11+ private slackService : SlackService ,
12+ private sentryService : SentryService ,
13+ ) { }
14+
15+ checkPermissions : RequestHandler = async (
16+ req : Request ,
17+ res : Response < PermissionCheckResponseDto > ,
18+ next : NextFunction ,
19+ ) : Promise < void > => {
20+ try {
21+ const permissions = await this . slackService . checkPermissions ( ) ;
22+ const response = new PermissionCheckResponseDto ( true , 'Slack 권한 확인 완료' , permissions , null ) ;
23+ res . status ( 200 ) . json ( response ) ;
24+ } catch ( error ) {
25+ logger . error ( 'Slack 권한 확인 실패:' , error instanceof Error ? error . message : '알 수 없는 오류' ) ;
26+ next ( error ) ;
27+ }
28+ } ;
29+
30+ testBot : RequestHandler = async (
31+ req : Request ,
32+ res : Response < SlackSuccessResponseDto > ,
33+ next : NextFunction ,
34+ ) : Promise < void > => {
35+ try {
36+ if ( ! this . slackService . hasBotToken ( ) && ! this . slackService . hasWebhookUrl ( ) ) {
37+ const response = new SlackSuccessResponseDto (
38+ false ,
39+ 'SLACK_BOT_TOKEN 또는 SLACK_WEBHOOK_URL 환경 변수가 설정되지 않았습니다.' ,
40+ { } ,
41+ 'MISSING_SLACK_CONFIG'
42+ ) ;
43+ res . status ( 400 ) . json ( response ) ;
44+ return ;
45+ }
46+
47+ const testMessage = {
48+ text : '🤖 봇 테스트 메시지입니다!' ,
49+ attachments : [
50+ {
51+ color : 'good' ,
52+ fields : [
53+ {
54+ title : '테스트 결과' ,
55+ value : '✅ Slack 연동이 정상적으로 작동합니다.' ,
56+ short : false ,
57+ } ,
58+ ] ,
59+ footer : `테스트 시간: ${ new Date ( ) . toLocaleString ( 'ko-KR' , { timeZone : 'Asia/Seoul' } ) } ` ,
60+ } ,
61+ ] ,
62+ } ;
63+
64+ await this . slackService . sendMessage ( testMessage ) ;
65+ const response = new SlackSuccessResponseDto ( true , '봇 테스트 메시지 전송 완료!' , { } , null ) ;
66+ res . status ( 200 ) . json ( response ) ;
67+ } catch ( error ) {
68+ logger . error ( '봇 테스트 실패:' , error instanceof Error ? error . message : '알 수 없는 오류' ) ;
69+ next ( error ) ;
70+ }
71+ } ;
72+
73+ handleInteractive : RequestHandler = async (
74+ req : Request ,
75+ res : Response ,
76+ next : NextFunction ,
77+ ) : Promise < void > => {
78+ try {
79+ const payload = JSON . parse ( req . body . payload ) ;
80+
81+ if ( payload . type === 'interactive_message' && payload . actions && payload . actions [ 0 ] ) {
82+ const action = payload . actions [ 0 ] ;
83+
84+ if ( action . name === 'sentry_action' ) {
85+ const [ actionType , issueId , organizationSlug , projectSlug ] = action . value . split ( ':' ) ;
86+
87+ const actionData : SentryActionData = {
88+ action : actionType as SentryApiAction ,
89+ issueId,
90+ organizationSlug,
91+ projectSlug,
92+ } ;
93+
94+ if ( actionData . issueId && actionData . organizationSlug && actionData . projectSlug ) {
95+ logger . info ( 'Processing Sentry action:' , actionData ) ;
96+
97+ const result = await this . sentryService . handleIssueAction ( actionData ) ;
98+
99+ if ( result . success ) {
100+ const updatedMessage = this . createSuccessMessage ( actionData , payload . original_message || { } ) ;
101+ res . json ( updatedMessage ) ;
102+ } else {
103+ const errorMessage = this . createErrorMessage ( result . error || 'Unknown error' , payload . original_message || { } ) ;
104+ res . json ( errorMessage ) ;
105+ }
106+ return ;
107+ }
108+ }
109+ }
110+
111+ res . json ( { text : '❌ 잘못된 요청입니다.' } ) ;
112+ } catch ( error ) {
113+ logger . error ( 'Interactive 처리 실패:' , error instanceof Error ? error . message : '알 수 없는 오류' ) ;
114+ next ( error ) ;
115+ }
116+ } ;
117+
118+ private createSuccessMessage ( actionData : SentryActionData , originalMessage : unknown ) : unknown {
119+ const { action } = actionData ;
120+
121+ const updatedMessage = JSON . parse ( JSON . stringify ( originalMessage ) ) ;
122+
123+ if ( updatedMessage . attachments && updatedMessage . attachments [ 0 ] ) {
124+ const newStatus = getNewStatusFromAction ( action ) ;
125+ const statusColors = {
126+ 'resolved' : 'good' ,
127+ 'ignored' : 'warning' ,
128+ 'archived' : '#808080' ,
129+ 'unresolved' : 'danger' ,
130+ } ;
131+
132+ updatedMessage . attachments [ 0 ] . color = statusColors [ newStatus as keyof typeof statusColors ] || 'good' ;
133+
134+ const statusMapping = {
135+ 'resolved' : 'RESOLVED' ,
136+ 'ignored' : 'IGNORED' ,
137+ 'archived' : 'ARCHIVED' ,
138+ 'unresolved' : 'UNRESOLVED' ,
139+ } ;
140+
141+ const statusText = statusMapping [ newStatus as keyof typeof statusMapping ] || newStatus . toUpperCase ( ) ;
142+ updatedMessage . attachments [ 0 ] . footer = `✅ ${ statusText } | 처리 완료: ${ new Date ( ) . toLocaleString ( 'ko-KR' , { timeZone : 'Asia/Seoul' } ) } ` ;
143+
144+ delete updatedMessage . attachments [ 0 ] . actions ;
145+ }
146+
147+ return updatedMessage ;
148+ }
149+
150+ private createErrorMessage ( error : string , originalMessage : unknown ) : unknown {
151+ const updatedMessage = JSON . parse ( JSON . stringify ( originalMessage ) ) ;
152+
153+ if ( updatedMessage . attachments && updatedMessage . attachments [ 0 ] ) {
154+ updatedMessage . attachments [ 0 ] . fields . push ( {
155+ title : '❌ 오류 발생' ,
156+ value : error ,
157+ short : false ,
158+ } ) ;
159+
160+ updatedMessage . attachments [ 0 ] . color = 'danger' ;
161+ }
162+
163+ return updatedMessage ;
164+ }
165+ }
0 commit comments