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