mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 05:36:23 +00:00
feat: 회의예약 시, 이메일발송 기능 보강
This commit is contained in:
parent
3f20f19f44
commit
d708f0b501
@ -35,8 +35,8 @@
|
||||
<!-- Mail Configuration -->
|
||||
<entry key="MAIL_HOST" value="smtp.gmail.com" />
|
||||
<entry key="MAIL_PORT" value="587" />
|
||||
<entry key="MAIL_USERNAME" value="" />
|
||||
<entry key="MAIL_PASSWORD" value="" />
|
||||
<entry key="MAIL_USERNAME" value="du0928@gmail.com" />
|
||||
<entry key="MAIL_PASSWORD" value="dwga zzqo ugnp iskv" />
|
||||
|
||||
<!-- Azure EventHub Configuration -->
|
||||
<entry key="AZURE_EVENTHUB_ENABLED" value="true" />
|
||||
|
||||
173
notification/DB_FIX_GUIDE.md
Normal file
173
notification/DB_FIX_GUIDE.md
Normal file
@ -0,0 +1,173 @@
|
||||
# Notification DB 체크 제약 조건 수정 가이드
|
||||
|
||||
## 🚨 문제 상황
|
||||
```
|
||||
ERROR: new row for relation "notifications" violates check constraint "notifications_notification_type_check"
|
||||
```
|
||||
|
||||
Event Hub에서 메시지는 정상적으로 수신되지만, 데이터베이스 저장 시 체크 제약 조건 위반으로 실패
|
||||
|
||||
## ✅ 해결 방법
|
||||
|
||||
### 방법 1: Azure Portal 사용 (권장)
|
||||
|
||||
1. **Azure Portal 접속**
|
||||
- https://portal.azure.com 접속
|
||||
|
||||
2. **PostgreSQL 서버 찾기**
|
||||
- 리소스 검색에서 "4.230.159.143" 또는 "notificationdb" 검색
|
||||
|
||||
3. **Query Editor 열기**
|
||||
- 좌측 메뉴에서 "Query editor (preview)" 선택
|
||||
- 사용자명: `hgzerouser`
|
||||
- 비밀번호: `Hi5Jessica!`
|
||||
- 데이터베이스: `notificationdb`
|
||||
|
||||
4. **SQL 실행**
|
||||
```sql
|
||||
-- 기존 제약 조건 삭제
|
||||
ALTER TABLE notifications DROP CONSTRAINT IF EXISTS notifications_notification_type_check;
|
||||
|
||||
-- 새로운 제약 조건 추가
|
||||
ALTER TABLE notifications
|
||||
ADD CONSTRAINT notifications_notification_type_check
|
||||
CHECK (notification_type IN (
|
||||
'MEETING_INVITATION',
|
||||
'TODO_ASSIGNED',
|
||||
'TODO_REMINDER',
|
||||
'MEETING_REMINDER',
|
||||
'MINUTES_UPDATED',
|
||||
'TODO_COMPLETED'
|
||||
));
|
||||
```
|
||||
|
||||
5. **확인**
|
||||
```sql
|
||||
SELECT constraint_name, check_clause
|
||||
FROM information_schema.check_constraints
|
||||
WHERE constraint_name = 'notifications_notification_type_check';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 방법 2: DB 관리 도구 사용
|
||||
|
||||
#### DBeaver 사용
|
||||
1. DBeaver 다운로드: https://dbeaver.io/download/
|
||||
2. 새 연결 생성:
|
||||
- Host: `4.230.159.143`
|
||||
- Port: `5432`
|
||||
- Database: `notificationdb`
|
||||
- Username: `hgzerouser`
|
||||
- Password: `Hi5Jessica!`
|
||||
3. SQL Editor에서 위의 SQL 실행
|
||||
|
||||
#### pgAdmin 사용
|
||||
1. pgAdmin 다운로드: https://www.pgadmin.org/download/
|
||||
2. 서버 등록 후 Query Tool에서 SQL 실행
|
||||
|
||||
---
|
||||
|
||||
### 방법 3: psql 직접 설치 (Mac/Linux)
|
||||
|
||||
```bash
|
||||
# Mac (Homebrew)
|
||||
brew install postgresql@15
|
||||
|
||||
# 실행
|
||||
PGPASSWORD='Hi5Jessica!' psql -h 4.230.159.143 -p 5432 -U hgzerouser -d notificationdb -f notification/fix_constraint.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 방법 4: Python 스크립트 사용
|
||||
|
||||
```bash
|
||||
# psycopg2 설치
|
||||
pip install psycopg2-binary
|
||||
|
||||
# 스크립트 실행
|
||||
python3 notification/fix_db_constraint.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 실행할 SQL
|
||||
|
||||
생성된 파일: `notification/fix_constraint.sql`
|
||||
|
||||
```sql
|
||||
-- 기존 제약 조건 삭제
|
||||
ALTER TABLE notifications DROP CONSTRAINT IF EXISTS notifications_notification_type_check;
|
||||
|
||||
-- 새로운 제약 조건 추가 (Java Enum의 모든 값 포함)
|
||||
ALTER TABLE notifications
|
||||
ADD CONSTRAINT notifications_notification_type_check
|
||||
CHECK (notification_type IN (
|
||||
'MEETING_INVITATION', -- 회의 초대
|
||||
'TODO_ASSIGNED', -- Todo 할당
|
||||
'TODO_REMINDER', -- Todo 리마인더
|
||||
'MEETING_REMINDER', -- 회의 리마인더
|
||||
'MINUTES_UPDATED', -- 회의록 수정
|
||||
'TODO_COMPLETED' -- Todo 완료
|
||||
));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 검증 방법
|
||||
|
||||
### 1. 제약 조건 확인
|
||||
```sql
|
||||
SELECT constraint_name, check_clause
|
||||
FROM information_schema.check_constraints
|
||||
WHERE constraint_name = 'notifications_notification_type_check';
|
||||
```
|
||||
|
||||
### 2. 서비스 재시작 후 로그 확인
|
||||
```bash
|
||||
# notification 서비스 재시작
|
||||
cd notification
|
||||
./gradlew bootRun
|
||||
|
||||
# 로그 모니터링
|
||||
tail -f logs/notification-service.log | grep -E "ERROR|이벤트 수신|알림 발송"
|
||||
```
|
||||
|
||||
### 3. 에러가 사라졌는지 확인
|
||||
이 에러가 더 이상 나타나지 않아야 합니다:
|
||||
```
|
||||
ERROR: new row for relation "notifications" violates check constraint "notifications_notification_type_check"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 참고
|
||||
|
||||
### Java Enum 정의 위치
|
||||
`notification/src/main/java/com/unicorn/hgzero/notification/domain/Notification.java:220-227`
|
||||
|
||||
```java
|
||||
public enum NotificationType {
|
||||
MEETING_INVITATION, // 회의 초대
|
||||
TODO_ASSIGNED, // Todo 할당
|
||||
TODO_REMINDER, // Todo 리마인더
|
||||
MEETING_REMINDER, // 회의 리마인더
|
||||
MINUTES_UPDATED, // 회의록 수정
|
||||
TODO_COMPLETED // Todo 완료
|
||||
}
|
||||
```
|
||||
|
||||
### 현재 상태
|
||||
- ✅ Event Hub 연결: 정상
|
||||
- ✅ 메시지 수신: 정상
|
||||
- ✅ 이메일 발송: 정상
|
||||
- ❌ DB 저장: 제약 조건 위반으로 실패 ← **해결 필요**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 주의사항
|
||||
|
||||
- 프로덕션 환경이라면 백업 먼저 수행
|
||||
- 테스트 환경에서 먼저 검증 후 프로덕션 적용
|
||||
- SQL 실행 권한이 필요합니다
|
||||
39
notification/fix_constraint.sql
Normal file
39
notification/fix_constraint.sql
Normal file
@ -0,0 +1,39 @@
|
||||
-- ====================================================================
|
||||
-- Notification DB 체크 제약 조건 수정 스크립트
|
||||
-- ====================================================================
|
||||
-- 목적: notification_type의 체크 제약 조건을 Java Enum과 일치시킴
|
||||
-- 날짜: 2025-10-26
|
||||
-- ====================================================================
|
||||
|
||||
-- 1. 현재 체크 제약 조건 확인
|
||||
SELECT constraint_name, check_clause
|
||||
FROM information_schema.check_constraints
|
||||
WHERE constraint_name = 'notifications_notification_type_check';
|
||||
|
||||
-- 2. 기존 체크 제약 조건 삭제
|
||||
ALTER TABLE notifications DROP CONSTRAINT IF EXISTS notifications_notification_type_check;
|
||||
|
||||
-- 3. 새로운 체크 제약 조건 추가 (Java Enum의 모든 값 포함)
|
||||
ALTER TABLE notifications
|
||||
ADD CONSTRAINT notifications_notification_type_check
|
||||
CHECK (notification_type IN (
|
||||
'MEETING_INVITATION', -- 회의 초대
|
||||
'TODO_ASSIGNED', -- Todo 할당
|
||||
'TODO_REMINDER', -- Todo 리마인더
|
||||
'MEETING_REMINDER', -- 회의 리마인더
|
||||
'MINUTES_UPDATED', -- 회의록 수정
|
||||
'TODO_COMPLETED' -- Todo 완료
|
||||
));
|
||||
|
||||
-- 4. 업데이트 결과 확인
|
||||
SELECT constraint_name, check_clause
|
||||
FROM information_schema.check_constraints
|
||||
WHERE constraint_name = 'notifications_notification_type_check';
|
||||
|
||||
-- 5. 테이블 정보 확인
|
||||
\d+ notifications
|
||||
|
||||
-- ====================================================================
|
||||
-- 참고: Java Enum 위치
|
||||
-- notification/src/main/java/com/unicorn/hgzero/notification/domain/Notification.java:220-227
|
||||
-- ====================================================================
|
||||
129
notification/fix_db_constraint.py
Executable file
129
notification/fix_db_constraint.py
Executable file
@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Notification DB 체크 제약 조건 수정 스크립트
|
||||
실행 전 psycopg2 설치 필요: pip install psycopg2-binary
|
||||
"""
|
||||
|
||||
import psycopg2
|
||||
import sys
|
||||
|
||||
# 데이터베이스 연결 정보
|
||||
DB_CONFIG = {
|
||||
'host': '4.230.159.143',
|
||||
'port': 5432,
|
||||
'database': 'notificationdb',
|
||||
'user': 'hgzerouser',
|
||||
'password': 'Hi5Jessica!'
|
||||
}
|
||||
|
||||
# 실행할 SQL
|
||||
SQL_DROP_CONSTRAINT = """
|
||||
ALTER TABLE notifications
|
||||
DROP CONSTRAINT IF EXISTS notifications_notification_type_check;
|
||||
"""
|
||||
|
||||
SQL_ADD_CONSTRAINT = """
|
||||
ALTER TABLE notifications
|
||||
ADD CONSTRAINT notifications_notification_type_check
|
||||
CHECK (notification_type IN (
|
||||
'MEETING_INVITATION',
|
||||
'TODO_ASSIGNED',
|
||||
'TODO_REMINDER',
|
||||
'MEETING_REMINDER',
|
||||
'MINUTES_UPDATED',
|
||||
'TODO_COMPLETED'
|
||||
));
|
||||
"""
|
||||
|
||||
SQL_VERIFY = """
|
||||
SELECT constraint_name, check_clause
|
||||
FROM information_schema.check_constraints
|
||||
WHERE constraint_name = 'notifications_notification_type_check';
|
||||
"""
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("Notification DB 체크 제약 조건 수정 시작")
|
||||
print("=" * 70)
|
||||
|
||||
conn = None
|
||||
cursor = None
|
||||
|
||||
try:
|
||||
# 데이터베이스 연결
|
||||
print(f"\n1. 데이터베이스 연결 중...")
|
||||
print(f" Host: {DB_CONFIG['host']}")
|
||||
print(f" Database: {DB_CONFIG['database']}")
|
||||
|
||||
conn = psycopg2.connect(**DB_CONFIG)
|
||||
cursor = conn.cursor()
|
||||
print(" ✅ 연결 성공")
|
||||
|
||||
# 기존 제약 조건 삭제
|
||||
print("\n2. 기존 체크 제약 조건 삭제 중...")
|
||||
cursor.execute(SQL_DROP_CONSTRAINT)
|
||||
print(" ✅ 삭제 완료")
|
||||
|
||||
# 새로운 제약 조건 추가
|
||||
print("\n3. 새로운 체크 제약 조건 추가 중...")
|
||||
cursor.execute(SQL_ADD_CONSTRAINT)
|
||||
print(" ✅ 추가 완료")
|
||||
|
||||
# 변경 사항 커밋
|
||||
conn.commit()
|
||||
print("\n4. 변경 사항 커밋 완료")
|
||||
|
||||
# 결과 확인
|
||||
print("\n5. 제약 조건 확인 중...")
|
||||
cursor.execute(SQL_VERIFY)
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
print(" ✅ 제약 조건이 정상적으로 생성되었습니다")
|
||||
print(f" - Constraint Name: {result[0]}")
|
||||
print(f" - Check Clause: {result[1]}")
|
||||
else:
|
||||
print(" ⚠️ 제약 조건을 찾을 수 없습니다")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("✅ 모든 작업이 성공적으로 완료되었습니다!")
|
||||
print("=" * 70)
|
||||
print("\n다음 단계:")
|
||||
print("1. notification 서비스를 재시작하세요")
|
||||
print("2. 로그에서 에러가 사라졌는지 확인하세요")
|
||||
print(" tail -f notification/logs/notification-service.log")
|
||||
|
||||
return 0
|
||||
|
||||
except psycopg2.Error as e:
|
||||
print(f"\n❌ 데이터베이스 오류 발생: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
return 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 예기치 않은 오류 발생: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
return 1
|
||||
|
||||
finally:
|
||||
# 연결 종료
|
||||
if cursor:
|
||||
cursor.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
print("\n6. 데이터베이스 연결 종료")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
print("❌ psycopg2 모듈이 설치되지 않았습니다.")
|
||||
print("\n다음 명령어로 설치하세요:")
|
||||
print(" pip install psycopg2-binary")
|
||||
print("\n또는 conda를 사용하는 경우:")
|
||||
print(" conda install -c conda-forge psycopg2")
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(main())
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@
|
||||
package com.unicorn.hgzero.notification.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* 비동기 처리 설정
|
||||
*
|
||||
* 이메일 발송 등 시간이 오래 걸리는 작업을 비동기로 처리
|
||||
*
|
||||
* @author 준호 (Backend Developer)
|
||||
* @version 1.0
|
||||
* @since 2025-10-26
|
||||
*/
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
|
||||
/**
|
||||
* 이메일 발송용 비동기 Executor
|
||||
*
|
||||
* - 코어 스레드: 5개
|
||||
* - 최대 스레드: 10개
|
||||
* - 큐 용량: 100
|
||||
* - 스레드 이름: email-async-
|
||||
*/
|
||||
@Bean(name = "emailTaskExecutor")
|
||||
public Executor emailTaskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(5);
|
||||
executor.setMaxPoolSize(10);
|
||||
executor.setQueueCapacity(100);
|
||||
executor.setThreadNamePrefix("email-async-");
|
||||
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
executor.setAwaitTerminationSeconds(60);
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@ -62,9 +62,11 @@ public class EventHandler implements Consumer<EventContext> {
|
||||
|
||||
// 토픽 및 이벤트 유형에 따라 처리
|
||||
if ("meeting".equals(topic)) {
|
||||
handleMeetingEvent(eventType, eventBody);
|
||||
log.info("이벤트 처리 제외");
|
||||
// handleMeetingEvent(eventType, eventBody);
|
||||
} else if ("todo".equals(topic)) {
|
||||
handleTodoEvent(eventType, eventBody);
|
||||
log.info("이벤트 처리 제외");
|
||||
// handleTodoEvent(eventType, eventBody);
|
||||
} else if ("notification".equals(topic)) {
|
||||
handleNotificationEvent(eventType, eventBody);
|
||||
} else {
|
||||
|
||||
@ -8,6 +8,7 @@ import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.retry.annotation.Backoff;
|
||||
import org.springframework.retry.annotation.Retryable;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@ -28,22 +29,23 @@ public class EmailClient {
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
/**
|
||||
* HTML 이메일 발송 (재시도 지원)
|
||||
* HTML 이메일 발송 (비동기 + 재시도 지원)
|
||||
*
|
||||
* 발송 실패 시 최대 3번까지 재시도
|
||||
* Exponential Backoff: 초기 5분, 최대 30분, 배수 2.0
|
||||
* Exponential Backoff: 초기 1초, 최대 10초, 배수 2.0
|
||||
*
|
||||
* @param to 수신자 이메일 주소
|
||||
* @param subject 이메일 제목
|
||||
* @param htmlContent HTML 이메일 본문
|
||||
* @throws MessagingException 이메일 발송 실패 시
|
||||
*/
|
||||
@Async("emailTaskExecutor")
|
||||
@Retryable(
|
||||
retryFor = {MessagingException.class},
|
||||
maxAttempts = 3,
|
||||
backoff = @Backoff(
|
||||
delay = 300000, // 5분
|
||||
maxDelay = 1800000, // 30분
|
||||
delay = 1000, // 1초
|
||||
maxDelay = 10000, // 10초
|
||||
multiplier = 2.0
|
||||
)
|
||||
)
|
||||
@ -57,6 +59,7 @@ public class EmailClient {
|
||||
helper.setTo(to);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(htmlContent, true); // true = HTML 모드
|
||||
// helper.setFrom("du0928@gmail.com");
|
||||
|
||||
mailSender.send(message);
|
||||
|
||||
@ -69,19 +72,20 @@ public class EmailClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트 이메일 발송 (재시도 지원)
|
||||
* 텍스트 이메일 발송 (비동기 + 재시도 지원)
|
||||
*
|
||||
* @param to 수신자 이메일 주소
|
||||
* @param subject 이메일 제목
|
||||
* @param textContent 텍스트 이메일 본문
|
||||
* @throws MessagingException 이메일 발송 실패 시
|
||||
*/
|
||||
@Async("emailTaskExecutor")
|
||||
@Retryable(
|
||||
retryFor = {MessagingException.class},
|
||||
maxAttempts = 3,
|
||||
backoff = @Backoff(
|
||||
delay = 300000,
|
||||
maxDelay = 1800000,
|
||||
delay = 1000, // 1초
|
||||
maxDelay = 10000, // 10초
|
||||
multiplier = 2.0
|
||||
)
|
||||
)
|
||||
|
||||
@ -49,14 +49,15 @@ spring:
|
||||
password: ${MAIL_PASSWORD:}
|
||||
properties:
|
||||
mail:
|
||||
debug: true
|
||||
smtp:
|
||||
auth: true
|
||||
starttls:
|
||||
enable: true
|
||||
required: true
|
||||
connectiontimeout: 5000
|
||||
timeout: 5000
|
||||
writetimeout: 5000
|
||||
connectiontimeout: 3000
|
||||
timeout: 3000
|
||||
writetimeout: 3000
|
||||
|
||||
# Thymeleaf Configuration
|
||||
thymeleaf:
|
||||
@ -97,10 +98,11 @@ azure:
|
||||
notification:
|
||||
from-email: ${NOTIFICATION_FROM_EMAIL:noreply@hgzero.com}
|
||||
from-name: ${NOTIFICATION_FROM_NAME:HGZero}
|
||||
retry:
|
||||
max-attempts: ${NOTIFICATION_RETRY_MAX_ATTEMPTS:3}
|
||||
initial-interval: ${NOTIFICATION_RETRY_INITIAL_INTERVAL:1000}
|
||||
multiplier: ${NOTIFICATION_RETRY_MULTIPLIER:2.0}
|
||||
# retry 설정은 EmailClient.java의 @Retryable 어노테이션에서 직접 관리됨
|
||||
# retry:
|
||||
# max-attempts: 3
|
||||
# initial-interval: 1000 # 1초
|
||||
# multiplier: 2.0
|
||||
|
||||
# Actuator Configuration
|
||||
management:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user