알림 서비스
이 페이지는 발신번호 등록 상태, 발송 요청, 발송 이력, 반 대상 수신자 확장,
재사용 가능한 SMS 템플릿, staff 소유 SMS 수신 그룹을 담당하는 테넌트 범위 SMS
모듈 lumie-backend/modules/notification의 레퍼런스입니다.
소스 경로
| 경로 | 역할 |
|---|---|
lumie-backend/modules/notification/src/main/java/com/lumie/notification/adapter/in/web/{SmsController,SmsSenderSettingController,SmsTemplateController,SolapiSmsWebhookController,InternalSmsSenderSettingController}.java | 공개 SMS 인터페이스, 테넌트 발신번호 설정 엔드포인트, SOLAPI 콜백 수신기, 내부 승인 라우트 |
lumie-backend/modules/notification/src/main/java/com/lumie/notification/adapter/in/scheduling/SmsDispatchScheduler.java | 기한이 지난 PENDING 메시지를 처리하는 ShedLock 보호 예약 dispatcher |
lumie-backend/modules/notification/src/main/java/com/lumie/notification/adapter/out/external/{MockSmsProviderAdapter,SolapiSmsProviderAdapter}.java | 교체 가능한 outbound SMS 제공자 어댑터. SOLAPI는 이제 config 대신 SendCommand.from()에서 발신번호를 읽습니다. |
lumie-backend/modules/notification/src/main/java/com/lumie/notification/adapter/out/config/SmsProperties.java | SMS 제공자 선택과 필수 SOLAPI credential 및 base URL |
lumie-backend/modules/notification/src/main/java/com/lumie/notification/adapter/out/persistence/SmsSenderSettingPersistenceAdapter.java | sms_sender_settings용 테넌트 범위 persistence 어댑터 |
lumie-backend/modules/notification/src/main/java/com/lumie/notification/application/service/{SmsCommandService,SmsDispatchService,SmsSenderSettingService,SmsWebhookService,SmsQueryService,SmsRecipientGroupService}.java | 발송, dispatch, 발신번호 설정, webhook, 목록, 이력, 수신 그룹, 템플릿 로직 |
lumie-backend/modules/notification/src/main/java/com/lumie/notification/domain/entity/{SmsMessage,SmsRecipientGroup,SmsSenderSetting,SmsTemplate}.java | 테넌트 발신번호 설정 row를 포함한 주요 notification aggregate |
lumie-backend/app/src/main/java/com/lumie/app/config/internal/InternalHmacAuthFilter.java | tenant에 바인딩된 발신번호 설정 승인 갱신을 포함한 /internal/**용 HMAC gate |
lumie-backend/app/src/main/java/com/lumie/app/config/SecurityConfig.java | /v1/sms/**와 /v1/sms-templates/**용 staff 역할 접근 gate |
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sql | sms_messages와 sms_templates의 기준 테이블 생성과 RLS |
lumie-backend/app/src/main/resources/db/migration/public/V71__sms_message_mvp_metadata.sql | 예약, 취소, 가격 메타데이터, optimistic locking, SMS 이력 index |
lumie-backend/app/src/main/resources/db/migration/public/V73__create_sms_recipient_groups.sql | staff 소유 수신 그룹 table, JSON recipients, 테넌트 RLS, OWNER-staff FK |
lumie-backend/app/src/main/resources/db/migration/public/V74__link_sms_messages_to_recipient_groups.sql | 선택적 sms_messages.recipient_group_id 연결과 테넌트-safe FK |
lumie-backend/app/src/main/resources/db/migration/public/V76__sms_provider_dispatch_tracking.sql | 제공자, 제공자 group id, dispatch attempt, 제공자 status/error, dispatch/confirmation timestamp |
lumie-backend/app/src/main/resources/db/migration/public/V78__sms_sender_settings.sql | sms_sender_settings table, 테넌트 RLS, unique 테넌트/제공자 contract, nullable sms_messages.sender_number backfill column |
공개 인터페이스
| 엔드포인트 | 목적 |
|---|---|
GET /v1/sms/sender-setting | 현재 tenant의 SOLAPI 발신번호 설정 row를 OWNER 전용으로 조회하거나, row가 없으면 synthetic NOT_CONFIGURED 응답을 반환 |
GET /v1/sms/sender-setting/status | 발송 UI용 staff-safe 발신번호 상태 조회. provider와 등록 status만 반환 |
PUT /v1/sms/sender-setting | tenant의 SOLAPI 발신번호를 OWNER 전용으로 upsert |
POST /v1/sms/send | 명시적인 수신자 목록 또는 저장된 수신 그룹으로 SMS 발송 |
GET /v1/sms/recipient-groups | 현재 staff 구성원의 수신 그룹 목록을 수신자별 전화번호 없이 조회 |
GET /v1/sms/recipient-groups/{id} | 현재 staff 소유 수신 그룹 하나와 수신자를 조회 |
POST /v1/sms/recipient-groups | 현재 staff 소유 수신 그룹 생성 |
GET /v1/sms/history, GET /v1/sms/history/{id} | 페이지네이션된 SMS 이력과 개별 발송 조회 |
POST /v1/sms/history/{id}/cancel | 대기 중인 예약 발송 취소 |
POST /v1/sms/send/class/{classId} | 수업 enrollment에서 수신자를 확장한 뒤 한 번에 발송 |
POST /v1/sms/solapi/webhook | X-Solapi-Secret: sha1(SOLAPI_WEBHOOK_TOKEN)으로 보호되는 SOLAPI 콜백 수신기 |
GET /v1/sms-templates, POST /v1/sms-templates, PATCH /v1/sms-templates/{id}, DELETE /v1/sms-templates/{id} | SMS 템플릿 CRUD |
/v1/sms/**와 /v1/sms-templates/**는 staff 역할(OWNER, MANAGER,
INSTRUCTOR)을 요구합니다. 전체 발신번호 설정 조회와 갱신은 컨트롤러 내부에
명시적인 AuthorizationGuard.requireOwner() 검사를 추가로 둡니다. staff-safe
/status 엔드포인트는 발송 UI가 미승인 발송을 차단하되, OWNER가 아닌 staff에게 실제
발신번호나 거절 사유를 노출하지 않기 위해 provider와 status만 반환합니다. 직접
발송, 수업 발송, 수신 그룹 생성은 Idempotency-Key가 필수입니다. SMS 응답에는
수신자 PII가 포함될 수 있으므로 SMS write 경로는 idempotency row에 응답 본문을
저장하지 않습니다. 발송 UI는 같은 draft와 재시도에 대해 하나의 idempotency key를
유지하고, 성공이 확인되거나 사용자가 본문, 수신 그룹, 템플릿, 예약 시간을 바꿀 때만
새 key로 교체합니다.
발신번호 설정 공개 인터페이스는 의도적으로 v1 범위에서 단일 제공자, 그리고
테넌트/제공자 쌍당 단일 발신번호만 지원합니다. 코드는 solapi row만 upsert
합니다.
내부 인터페이스와 의존성
| 의존성 | 역할 |
|---|---|
InternalHmacAuthFilter | X-Tenant-Slug, X-Timestamp, X-Signature로 /internal/**를 인증하고, 그 뒤 테넌트 컨텍스트를 복원 해 RLS로 보호된 발신번호 설정 row가 보이도록 합니다. |
StaffService | 직접 발송과 수업 대상 발송 모두에서 UserContextHolder로 sender를 해석 |
ClassService | 수업 존재 여부를 검증하고 등록된 student ID를 해석 |
StudentService | student ID를 이름과 학부모 전화번호로 확장 |
SmsRecipientGroupPersistencePort | staff 소유 수신 그룹을 저장하고 id와 ownerStaffId로 그룹을 로드 |
SmsSenderSettingPersistencePort | RLS 아래에서 tenant의 단일 solapi 발신번호 설정 row를 로드하고 저장 |
SmsProviderPort | outbound provider를 추상화합니다. lumie.sms.provider=mock이 안전한 기본값이고, solapi는 실제 발송을 활성화하며 이제 메시지별 발신번호를 받습니다. |
TenantService | 내부 승인 라우트, scheduler, webhook 처리가 RLS 아래의 테넌트 범위 SMS row를 읽기 전에 테넌트 컨텍스트를 복원할 수 있게 합니다. |
현재 코드베이스에는 공개된 libs/internal-api notification 계약이 없습니다.
집계와 테이블
| 집계 | 참고 |
|---|---|
SmsMessage | sender, nullable senderNumber, recipient type, JSON recipient list, 내용, 예약/취소 시각, SMS/LMS 산정, 가격 산정, 제공자 추적 필드, 상태, 성공/실패 수를 저장 |
SmsRecipientGroup | staff 소유 group name, icon, JSON recipient list를 저장하며 읽기와 발송 전에 테넌트 및 OWNER 검사를 적용 |
SmsSenderSetting | 한 provider에 대한 테넌트 범위 SMS 발신번호, registrationStatus, optional rejectionReason, approvedAt, rejectedAt, timestamp, optimistic-lock version, unique (tenant_id, provider) 제약을 저장 |
SmsTemplate | 재사용 가능한 템플릿 이름, 내용, 카테고리를 저장 |
V78__sms_sender_settings.sql은 다른 테넌트 table과 같은 app.tenant_id
policy shape로 sms_sender_settings에 RLS를 활성화하고 강제합니다. 이
migration은 또한 nullable sms_messages.sender_number를 추가해 발송 이력이
실제로 사용된 발신번호를 보존할 수 있게 합니다. 새 메시지는 생성 시점에 승인된
발신번호를 저장합니다. sender_number가 NULL인 legacy row는 아래의
대체 처리 경로로 여전히 dispatch할 수 있습니다.
런타임 흐름
계약 참고 사항
발신번호 설정 흐름은 SmsSenderSettingService.SOLAPI_PROVIDER에서 solapi
provider로 하드코딩되어 있습니다. GET /v1/sms/sender-setting은 tenant에 row가
있으면 일반 SmsSenderSettingResponse를 반환하고, row가 없으면 식별자와
timestamp가 모두 null인 synthetic NOT_CONFIGURED 응답을 반환합니다.
SmsSenderSetting은 저장되는 번호를 숫자만 남기도록 정규화하고,
SmsSenderSettingResponse는 설정 API 응답을 위해 발신번호에 다시 하이픈을
넣습니다. SmsMessageResponse는 sms_messages의 저장된 senderNumber를
그대로 반환하므로, 이력 row는 다시 포맷하지 않고 저장된 실제 dispatch 번호를
보존합니다.
발신번호를 변경하면 승인 metadata가 지워지고 row는 PENDING으로
되돌아갑니다. 이미 승인된 동일한 번호를 다시 제출하면 no-op으로 처리되고 row는
APPROVED 상태를 유지합니다.
// lumie-backend/modules/notification/src/main/java/com/lumie/notification/domain/entity/SmsSenderSetting.java
public void submit(String senderNumber) {
String normalized = normalizeSenderNumber(senderNumber);
if (Objects.equals(this.senderNumber, normalized)
&& this.registrationStatus == SmsSenderSettingStatus.APPROVED) {
return;
}
this.senderNumber = normalized;
this.registrationStatus = SmsSenderSettingStatus.PENDING;
this.rejectionReason = null;
this.approvedAt = null;
this.rejectedAt = null;
}
lumie.sms.provider=solapi일 때 SMS 발송 생성은 메시지를 저장하기 전에
tenant의 승인된 발신번호를 반드시 요구합니다. sendSms(...)와
sendToClass(...)는 메시지를 만들기 전에 발신번호를 해석하며, 승인된 번호는
즉시 발송과 예약 발송 모두에 대해 sms_messages.sender_number에 저장됩니다.
기본 mock provider는 local/dev provider이므로 발신번호 설정 승인 row를
요구하지 않습니다.
// lumie-backend/modules/notification/src/main/java/com/lumie/notification/application/service/SmsSenderSettingService.java
public String resolveSenderNumberForDispatch(String savedSenderNumber) {
if (savedSenderNumber != null && !savedSenderNumber.isBlank()) {
return savedSenderNumber;
}
return requireApprovedSenderNumber();
}
즉시 발송은 먼저 PENDING으로 저장되고, 이후 SmsDispatchService가 database
커밋 뒤에 디스패치합니다. 예약 발송은 SmsDispatchScheduler가 기한 도래 상태로
찾을 때까지 PENDING으로 남습니다. Provider HTTP 호출은 transaction
밖에서 수행되고, 제공자 결과는 별도 transaction으로 저장됩니다.
DISPATCHED는 provider가 메시지를 접수했고 최종 delivery confirmation은
아직 남아 있음을 의미합니다.
sms_messages.sender_number가 NULL인 legacy live-제공자 row는 dispatch
시점에 tenant의 현재 승인된 발신번호로 대체 처리하여 여전히 dispatch할 수
있습니다. 이 덕분에 migration 이전 이력 row도 계속 발송 가능하지만, tenant에
더 이상 승인된 발신번호가 없으면 여전히 실패합니다. mock provider는 실제
발신번호를 사용하지 않으므로 이 대체 처리를 우회합니다.
이 모듈은 교체 가능한 SMS provider를 지원합니다. lumie.sms.provider의
기본값은 mock이고, solapi는 실제 SOLAPI 발송을 활성화합니다.
SmsProperties.Solapi는 apiKey, apiSecret, baseUrl,
webhookToken만 정의하며, SolapiSmsProviderAdapter는 누락된 credential
또는 webhook token을 거부합니다. 코드는 더 이상 SOLAPI_FROM_NUMBER를
읽지 않습니다.
다음은 lumie-backend/app/src/main/resources/application.yaml의 대표 형태이며,
lumie-backend/modules/notification/src/main/java/com/lumie/notification/adapter/out/config/SmsProperties.java가
이를 바인딩합니다.
lumie:
sms:
provider: solapi
solapi:
api-key: ${SOLAPI_API_KEY}
api-secret: ${SOLAPI_API_SECRET}
base-url: ${SOLAPI_BASE_URL:https://api.solapi.com}
webhook-token: ${SOLAPI_WEBHOOK_TOKEN}
PATCH /internal/sms/sender-settings/\{tenantSlug\}/approval은 v1에서 수동
운영 전용 단계입니다. 이 라우트는 APPROVED 또는 REJECTED만 허용하고,
REJECTED에는 rejection reason을 필수로 요구하며, /internal/** HMAC auth
뒤에서 실행되고, RLS로 보호된 row를 갱신하기 전에 path slug에서 테넌트
context를 다시 바인딩합니다. v1은 SOLAPI 발신번호 등록이나 서류 제출을
자동화하지 않으며, 하나의 테넌트/제공자 쌍에 대해 여러 발신번호도 지원하지
않습니다.
SOLAPI callback은 수신자별 customFields(lumieTenant, lumieSmsId,
lumieRecipient)를 사용해 SmsWebhookService가 테넌트 범위 row를 RLS
아래에서 갱신하기 전에 테넌트 컨텍스트를 복원할 수 있게 합니다.
SmsDispatchService는 Lumie message id와 recipient index로 callback을 반영하고,
제공자 message id를 검증하며, 저장된 row와 콜백 양쪽에 제공자 group id가
있을 때 group id도 검증합니다. SOLAPI가 group id를 생략하면 기존 저장 group id는
보존하고, group id가 없던 legacy row는 callback에 group id가 있을 때 기록합니다.
Callback 엔드포인트는 sha1(SOLAPI_WEBHOOK_TOKEN) 값을 담은 X-Solapi-Secret를
요구합니다.
직접 발송 요청은 recipients를 제공해야 합니다. 그룹 발송 요청은
recipientGroupId를 제공하고 inline recipients를 포함하지 않아야 합니다.
SmsCommandService는 인증된 staff 구성원의 ownerStaffId로 그룹을 로드하고,
수신자를 서버에서 파생한 뒤 저장된 그룹 수신자에서 message recipient type을
추론합니다. INSTRUCTOR 사용자는 본인이 만든 예약 문자만 취소할 수 있고,
OWNER와 MANAGER는 다른 직원의 예약 문자도 취소할 수 있습니다.
예시 계약
이 예시는 SmsController, SmsSenderSettingController,
InternalSmsSenderSettingController, SendSmsRequest,
UpsertSmsSenderSettingRequest, UpdateSmsSenderSettingApprovalRequest,
RecipientRequest, CreateSmsRecipientGroupRequest, SmsMessageResponse,
SmsSenderSettingResponse, SmsSenderSettingStatusResponse,
SmsRecipientGroupResponse, SendToClassRequest, PageResponse,
app/src/test/resources/openapi/api-docs.json에서 직접 가져왔습니다.
발신번호 설정 조회
GET /v1/sms/sender-setting
HTTP/1.1 200 OK
{
"id": null,
"provider": "solapi",
"senderNumber": null,
"registrationStatus": "NOT_CONFIGURED",
"rejectionReason": null,
"approvedAt": null,
"rejectedAt": null,
"createdAt": null,
"updatedAt": null
}
발신번호 상태 조회
GET /v1/sms/sender-setting/status
HTTP/1.1 200 OK
{
"provider": "solapi",
"registrationStatus": "PENDING"
}
발신번호 설정 갱신
PUT /v1/sms/sender-setting
Content-Type: application/json
{
"senderNumber": "021-234-5678"
}
HTTP/1.1 200 OK
{
"id": 1,
"provider": "solapi",
"senderNumber": "02-1234-5678",
"registrationStatus": "PENDING",
"rejectionReason": null,
"approvedAt": null,
"rejectedAt": null,
"createdAt": "<timestamp>",
"updatedAt": "<timestamp>"
}
내부 승인 갱신
PATCH /internal/sms/sender-settings/inst-acme/approval
X-Tenant-Slug: inst-acme
X-Timestamp: 1782700800
X-Signature: <hex hmac>
Content-Type: application/json
{
"registrationStatus": "APPROVED"
}
HTTP/1.1 200 OK
{
"id": 1,
"provider": "solapi",
"senderNumber": "02-1234-5678",
"registrationStatus": "APPROVED",
"rejectionReason": null,
"approvedAt": "<timestamp>",
"rejectedAt": null,
"createdAt": "<timestamp>",
"updatedAt": "<timestamp>"
}
SMS 발송
POST /v1/sms/send
Idempotency-Key: sms-20260701-01
Content-Type: application/json
{
"recipients": [
{
"phone": "01012345678",
"name": "Kim Student",
"studentId": 101,
"className": null,
"recipientRole": "GUARDIAN"
}
],
"title": "Class reminder",
"content": "{학생명} 학부모님, 10:00 수업 안내드립니다.",
"templateId": 3,
"scheduledAt": "2026-07-01T02:00:00Z"
}
HTTP/1.1 201 Created
{
"id": 55,
"senderId": 7,
"senderNumber": "0212345678",
"recipientType": "GUARDIAN",
"recipients": [
{
"phone": "010-****-5678",
"name": "Kim Student",
"studentId": 101,
"className": null,
"recipientRole": "GUARDIAN",
"deliveryStatus": "PENDING"
}
],
"recipientCount": 1,
"title": "Class reminder",
"recipientGroupId": null,
"content": "{학생명} 학부모님, 10:00 수업 안내드립니다.",
"templateId": 3,
"scheduledAt": "2026-07-01T02:00:00Z",
"cancelledAt": null,
"sentAt": null,
"messageType": "SMS",
"estimatedSegments": 1,
"unitPrice": 20,
"totalPrice": 20,
"status": "PENDING",
"successCount": 0,
"failCount": 0,
"createdAt": "<timestamp>",
"updatedAt": "<timestamp>"
}
수신 그룹
POST /v1/sms/recipient-groups
Idempotency-Key: sms-group-20260701-01
Content-Type: application/json
{
"name": "Final class guardians",
"icon": "book",
"recipients": [
{
"phone": "01012345678",
"name": "Kim Student",
"studentId": 101,
"className": null,
"recipientRole": "GUARDIAN"
}
]
}
HTTP/1.1 201 Created
{
"id": 10,
"name": "Final class guardians",
"icon": "book",
"recipients": [
{
"phone": "010-****-5678",
"name": "Kim Student",
"studentId": 101,
"className": null,
"recipientRole": "GUARDIAN",
"deliveryStatus": "PENDING"
}
],
"createdAt": "<timestamp>",
"updatedAt": "<timestamp>"
}
이력 페이지
GET /v1/sms/history?page=0&size=20&status=SENT
HTTP/1.1 200 OK
{
"items": [
{
"id": 55,
"senderId": 7,
"senderNumber": "0212345678",
"recipientType": "GUARDIAN",
"recipientCount": 1,
"recipients": [],
"title": "Class reminder",
"recipientGroupId": null,
"content": "{학생명} 학부모님, 10:00 수업 안내드립니다.",
"templateId": 3,
"scheduledAt": null,
"cancelledAt": null,
"sentAt": "<timestamp>",
"messageType": "SMS",
"estimatedSegments": 1,
"unitPrice": 20,
"totalPrice": 20,
"status": "SENT",
"successCount": 1,
"failCount": 0,
"createdAt": "<timestamp>",
"updatedAt": "<timestamp>"
}
],
"page": 0,
"perPage": 20,
"total": 1,
"totalPages": 1,
"hasNext": false
}
실패, 재시도, 관측성
sendSms(...)와sendToClass(...)는 tenant에APPROVED상태의 발신번호 설정 row가 없으면 요청을 거부합니다. 이 에러는SmsErrorCode.SENDER_SETTING_NOT_APPROVED에서 옵니다.PUT /v1/sms/sender-setting은PhoneNumberUtils.isValid(...)로 발신번호를 검증하고 저장 전에 숫자만 남기도록 정규화합니다.PATCH /internal/sms/sender-settings/\{tenantSlug\}/approval은APPROVED또는REJECTED이외의 상태를 거부하고, 이유 없는REJECTED를 거부하며, 알 수 없는 tenant를 거부하고, HMAC 인증된 테넌트 컨텍스트가 path slug와 일치하지 않으면403을 반환합니다.- unique
(tenant_id, provider)제약과 service 레벨의solapi조회 덕분에 v1은 tenant당 SOLAPI 전용 발신번호 설정 행 하나만 지원합니다. - 두 발송 경로 모두 sender 누락, 누락되었거나 유효하지 않은 수신자 전화번호, 빈 수신자 집합을 거부합니다.
- 직접 발송은
recipients가 없는 payload를 거부하고, 그룹 발송은recipientGroupId와 inlinerecipients가 함께 있는 payload를 거부합니다. - 수신 그룹은 현재 staff 구성원이 소유합니다. 목록 조회는 수신자 전화번호가
없는 summary DTO를 사용하고, 상세 조회와 발송 경로는
id와ownerStaffId로 그룹을 로드합니다. sendToClass(...)는 존재하지 않는 class와 등록 학생이 없는 class도 거부합니다.scheduledAt은 미래 시각이어야 합니다. 과거 또는 현재 시각은 즉시 발송으로 떨어지지 않고 거부됩니다.- 발송 엔드포인트는 idempotency key가 필수이지만, 템플릿 생성과 템플릿 수정은 idempotency layer를 사용하지 않습니다.
GET /v1/sms/history는recipientCount,senderNumber, 저장된 메시지contentpreview, 빈recipients배열을 반환합니다. 목록 화면은 수신자별 전화번호나 개인화된 렌더링 본문을 받지 않습니다.GET /v1/sms/history/{id}는 보호된 상세 화면에서 전체 저장 본문을 반환하고, 수신자 전화번호는 마스킹하며 개인화된 렌더링 본문도 반환하지 않습니다.- Programmatic dispatch transaction은
@RequiresNewTransactiontemplate를 사용하고, claim, result, failure, webhook update 쿼리 전에RlsTenantTransactionBinder로app.tenant_id를 바인딩합니다. 테넌트 컨텍스트가 없으면 빈 RLS 조회로 조용히 진행하지 않고 repository 접근 전에 실패합니다. - Provider 발송 실패는
sms_messages.last_provider_error에 기록됩니다. 재시도 가능한 실패는lumie.sms.max-dispatch-attempts에 도달하기 전까지PENDING으로 남고, 최대 시도 횟수를 넘으면 메시지는FAILED가 됩니다. sms_messages.sender_number가NULL인 legacy row는resolveSenderNumberForDispatch(...)를 통해 여전히 dispatch할 수 있지만, dispatch 시점에 tenant에 현재 승인된 발신번호 설정 row가 있을 때만 가능합니다.- SOLAPI delivery callback은 수신자별 제공자 id/status code를 갱신하고,
더 이상 pending/dispatched 수신자가 없을 때 메시지를
SENT,FAILED, 또는PARTIAL로 집계합니다. sms_messages,sms_sender_settings,sms_templates는 모두 테넌트 table이므로, 이력 조회는 RLS 아래에서 테넌트 범위를 유지합니다. OWNER/MANAGER는 테넌트-wide 이력을 읽고, INSTRUCTOR는 자신의senderId로 추가 제한됩니다.
검증
cd lumie-backend
./gradlew :modules:notification:test --tests '*SmsSenderSetting*'
./gradlew :modules:notification:test --tests '*SmsDispatchServiceTest'
./gradlew :modules:notification:test --tests '*SmsQueryServiceTest' --tests '*SmsControllerTest'
./gradlew :modules:notification:test --tests '*SmsCommandServiceTest'
./gradlew -Pintegration :libs:common:test --tests com.lumie.common.tenant.RlsTenantContextAspectIntegrationTest
./gradlew -Pintegration :app:test --tests com.lumie.app.config.RlsTenantTransactionBinderIntegrationTest --tests com.lumie.app.SmsDispatchServiceWiringTest
예상 성공 신호:
- Gradle이
BUILD SUCCESSFUL로 종료되고, notification 테스트가 여전히 발신번호 설정 read/write, internal 승인, 승인된 발신번호 gating, legacyNULL sender_number대체 처리 경로, history preview와 detail 본문 차이를 다룹니다. - Integration check는 기존 transactional RLS aspect가 테넌트 컨텍스트를 계속
바인딩하는지와 디스패치 흐름이 커밋 후 디스패치 업데이트에 쓰는
REQUIRES_NEWtransaction template를 받는지 검증합니다. SmsProperties가 여전히provider,dispatchBatchSize,maxDispatchAttempts,solapi.{apiKey,apiSecret,baseUrl,webhookToken}만 노출하고,V78__sms_sender_settings.sql이 여전히 RLS로 보호된sms_sender_settingstable과 nullablesms_messages.sender_number를 정의합니다.