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