feat: 회의예약 시, 이메일발송 기능 보강

This commit is contained in:
djeon 2025-10-26 10:34:16 +09:00
parent 3f20f19f44
commit d708f0b501
9 changed files with 16395 additions and 18 deletions

View File

@ -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" />

View 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 실행 권한이 필요합니다

View 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
View 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

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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
)
)

View File

@ -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: