forked from AcaMate/AcaMate_API
[♻️] PUSH 로직 변경 - uid 와 pid 를 활용해 푸시 전송가능, 뱃지 설정 가능, 컨텐츠 추가 가능
This commit is contained in:
parent
f443f3410c
commit
76d989a4fa
|
@ -25,11 +25,16 @@ public class AppDbContext: DbContext
|
|||
public DbSet<Location> Location { get; set; }
|
||||
public DbSet<Contact> Contact { get; set; }
|
||||
|
||||
//MARK: PUSH
|
||||
public DbSet<DBPayload> DBPayload { get; set; }
|
||||
public DbSet<PushCabinet> PushCabinet { get; set; }
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<User_Academy>()
|
||||
.HasKey(ua => new { ua.uid, ua.bid });
|
||||
|
||||
modelBuilder.Entity<PushCabinet>()
|
||||
.HasKey(p => new { p.uid, p.pid });
|
||||
}
|
||||
}
|
|
@ -2,6 +2,10 @@ using AcaMate.Common.Models;
|
|||
using AcaMate.V1.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Update.Internal;
|
||||
|
||||
using AcaMate.Common.Data;
|
||||
using AcaMate.V1.Services;
|
||||
|
||||
namespace AcaMate.V1.Controllers;
|
||||
|
@ -14,10 +18,14 @@ public class PushController : ControllerBase
|
|||
{
|
||||
private readonly ILogger<PushController> _logger;
|
||||
private readonly IPushQueue _pushQueue;
|
||||
public PushController(ILogger<PushController> logger, IPushQueue pushQueue)
|
||||
private readonly AppDbContext _dbContext;
|
||||
private readonly IRepositoryService _repositoryService;
|
||||
public PushController(ILogger<PushController> logger, IPushQueue pushQueue, AppDbContext dbContext, IRepositoryService repositoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
_pushQueue = pushQueue;
|
||||
_dbContext = dbContext;
|
||||
_repositoryService = repositoryService;
|
||||
}
|
||||
|
||||
[HttpGet()]
|
||||
|
@ -27,13 +35,12 @@ public class PushController : ControllerBase
|
|||
return Ok("SEND");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sends a push notification to the specified device token with the provided payload.
|
||||
/// </summary>
|
||||
/// <param name="deviceToken">The device token to send the notification to.</param>
|
||||
/// <param name="payload">The payload of the push notification.</param>
|
||||
///
|
||||
/// <returns>An IActionResult indicating the result of the operation.</returns>
|
||||
/// <response code="200">Push notification sent successfully.</response>
|
||||
/// <response code="400">Invalid input parameters.</response>
|
||||
|
@ -42,58 +49,78 @@ public class PushController : ControllerBase
|
|||
[HttpPost("send")]
|
||||
[CustomOperation("푸시전송", "저장된 양식으로, 사용자에게 푸시를 전송한다.(로컬 테스트 불가)", "푸시")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
|
||||
public async Task<IActionResult> SendPush(string deviceToken, [FromBody] Payload payload)
|
||||
public async Task<IActionResult> SendPush([FromBody] PushRequest pushRequest)
|
||||
{
|
||||
|
||||
if (string.IsNullOrWhiteSpace(deviceToken) || payload == null)
|
||||
return BadRequest(APIResponse.InvalidInputError());
|
||||
|
||||
var pushRequest = new PushRequest
|
||||
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
|
||||
|
||||
try
|
||||
{
|
||||
deviceToken = deviceToken,
|
||||
payload = payload
|
||||
};
|
||||
_pushQueue.Enqueue(pushRequest);
|
||||
return Ok("[푸시] 접수 완료");
|
||||
var payload = await _dbContext.DBPayload
|
||||
.Where(p => p.pid == pushRequest.pid)
|
||||
.Select(p => new Payload
|
||||
{
|
||||
aps = new Aps
|
||||
{
|
||||
alert = new Alert { title = p.title, body = p.body, subtitle = p.subtitle ?? "" },
|
||||
category = p.category
|
||||
},
|
||||
pid = pushRequest.pid,
|
||||
content = p.content ?? "",
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
//
|
||||
//
|
||||
// var isDev = _env.IsDevelopment();
|
||||
//
|
||||
// var pushFileSetting = new PushFileSetting()
|
||||
// {
|
||||
// uri = isDev ? "https://api.sandbox.push.apple.com/" : "https://api.push.apple.com/",
|
||||
// p12Path = isDev ? "/src/private/AM_Push_Sandbox.p12" : "/src/private/AM_Push.p12",
|
||||
// p12PWPath = "/src/private/appleKeys.json",
|
||||
// apnsTopic = "me.myds.ipstein.acamate.AcaMate"
|
||||
// };
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// if (await new ApnsPushService().SendPushNotificationAsync(deviceToken, pushFileSetting, payload))
|
||||
// {
|
||||
// return Ok(APIResponse.Send("000", "정상", Empty));
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// return StatusCode(500, APIResponse.UnknownError());
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// catch (ServiceConnectionFailedException failEx)
|
||||
// {
|
||||
// _logger.LogError($"[푸시][에러] : {failEx}");
|
||||
// return StatusCode(300, APIResponse.InternalSeverError());
|
||||
// }
|
||||
// catch (HttpRequestException httpEx)
|
||||
// {
|
||||
// _logger.LogError($"[푸시][에러] : {httpEx}");
|
||||
// return StatusCode(300, APIResponse.InternalSeverError());
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _logger.LogError($"[푸시][에러] : {ex}");
|
||||
// return StatusCode(500, APIResponse.UnknownError());
|
||||
// }
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
foreach (var uid in pushRequest.uids)
|
||||
{
|
||||
var badge = await _dbContext.PushCabinet
|
||||
.Where(c => c.uid == uid && c.check_yn == false && c.pid != pushRequest.pid)
|
||||
.CountAsync();
|
||||
|
||||
var pushToken = await _dbContext.User
|
||||
.Where(u => u.uid == uid)
|
||||
.Select(u => u.push_token)
|
||||
.FirstOrDefaultAsync() ?? "";
|
||||
|
||||
var pushCabinet = new PushCabinet
|
||||
{
|
||||
uid = uid,
|
||||
pid = pushRequest.pid,
|
||||
send_date = DateTime.Now,
|
||||
};
|
||||
|
||||
if (payload != null) payload.aps.badge = badge + 1;
|
||||
|
||||
var pushData = new PushData
|
||||
{
|
||||
pushToken = pushToken,
|
||||
payload = payload ?? throw new PushInvalidException("payload is NULL")
|
||||
};
|
||||
await _repositoryService.SaveDataFK<PushCabinet>(pushCabinet);
|
||||
|
||||
_pushQueue.Enqueue(pushData);
|
||||
}
|
||||
});
|
||||
|
||||
return Ok(APIResponse.Send("000", "정상", Empty));
|
||||
|
||||
}
|
||||
catch (PushInvalidException ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return Ok(APIResponse.Send("001", "푸시 송신: 푸시 전송 중 문제 발생",Empty));
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"[푸시] {ex.Message}");
|
||||
return StatusCode(500, APIResponse.UnknownError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -205,7 +205,8 @@ public class UserController : ControllerBase
|
|||
type = request.type,
|
||||
device_id = request.device_id,
|
||||
auto_login_yn = request.auto_login_yn,
|
||||
login_date = request.login_date
|
||||
login_date = request.login_date,
|
||||
push_token = request.push_token
|
||||
};
|
||||
var login = new Login
|
||||
{
|
||||
|
|
|
@ -50,5 +50,16 @@ public class ServiceConnectionFailedException : Exception
|
|||
public ServiceConnectionFailedException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PUSH 서비스 중 데이터 사용에 문제가 발생했을시
|
||||
/// </summary>
|
||||
public class PushInvalidException : Exception
|
||||
{
|
||||
public PushInvalidException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
|
@ -1,11 +1,112 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace AcaMate.V1.Models;
|
||||
|
||||
/*
|
||||
* iOS Payload 정의
|
||||
* aps 딕셔너리 구성
|
||||
* 1. alert : 실질적으로 사용자에게 노출되는 항목
|
||||
* 1.1. title : 제목
|
||||
* 1.2. subtitle : 부제목
|
||||
* 1.3. body : 본문 내용
|
||||
* 2. badge : 앱 아이콘에 표시할 숫자
|
||||
* 3. sound : 소리인데 "default" 가능
|
||||
* 4. content-available : 백그라운드 업데이트나 사일런트 푸시 송신시 사용한다.
|
||||
* 1로 설정해 사용자가 직접 보지 않아도 앱이 업데이트 되게 할 수 있으며,
|
||||
* UI에 알림이 표시되지 않고 데이터만 전달할 때 필요하다.
|
||||
* 5. mutable-content : 값을 1로 설정해두면 Notification Service Extension을 통해 알림의 내용을 변경할 수 있다.
|
||||
* 6. category : 사용자 인터렉션 구성시에 사용하는 식별자이다.
|
||||
* 7. thread-id : 관련 알림들을 그룹화해 관리할 때 사용한다.
|
||||
*/
|
||||
|
||||
/*
|
||||
* FCM Payload
|
||||
* https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?hl=ko&_gl=1*3awp3i*_up*MQ..*_ga*MTMyNDk4ODU5MC4xNzQxMTM1MzI3*_ga_CW55HF8NVT*MTc0MTEzNTMyNy4xLjAuMTc0MTEzNTM4My4wLjAuMA..#Notification
|
||||
* {
|
||||
"name": string,
|
||||
"data": { // 입력 전용으로 임의의 가
|
||||
string: string,
|
||||
...
|
||||
},
|
||||
"notification": {
|
||||
object (Notification)
|
||||
},
|
||||
"android": {
|
||||
object (AndroidConfig)
|
||||
},
|
||||
"webpush": {
|
||||
object (WebpushConfig)
|
||||
},
|
||||
"apns": {
|
||||
object (ApnsConfig)
|
||||
},
|
||||
"fcm_options": {
|
||||
object (FcmOptions)
|
||||
},
|
||||
|
||||
// Union field target can be only one of the following:
|
||||
"token": string,
|
||||
"topic": string,
|
||||
"condition": string
|
||||
// End of list of possible types for union field target.
|
||||
}
|
||||
* 1. Notification 영역
|
||||
* 1.1. title: 알림 제목
|
||||
* 1.2. body: 알림 내용
|
||||
* 1.3. image: 알림에 표시할 이미지 URL
|
||||
* 1.4. sound: 알림 재생 시 사용할 사운드
|
||||
* 1.5. click_action: 알림 클릭 시 실행할 액션(인텐트 액션 문자열)
|
||||
* 1.6. tag: 동일 태그를 가진 알림끼리 대체 또는 그룹화할 때 사용
|
||||
* 1.7. color: 알림 아이콘 배경색 (16진수 코드 등)
|
||||
* 1.8. body_loc_key 및 body_loc_args: 본문에 사용할 지역화 키와 인자 배열
|
||||
* 1.9. title_loc_key 및 title_loc_args: 제목에 사용할 지역화 키와 인자 배열
|
||||
* 1.10. channel_id: Android Oreo 이상에서 알림 채널 식별자
|
||||
* 1.11. ticker, sticky, event_time, notification_priority, visibility, notification_count, light_settings, vibrate_timings 등: 사용자 경험이나 알림의 동작 방식을 세밀하게 제어할 때 사용
|
||||
* 2. Data 영역 : 임의의 키-값 쌍을 포함하고 UI에 표시되는 내용이 아닌 앱의 로직에 활용할 데이터를 보낼 때 사용한다.
|
||||
* 3. 기타 전송 옵션
|
||||
* 3.1. priority: 메시지 전송 우선순위 (“high” 또는 “normal”)
|
||||
* 3.2. time_to_live (ttl): 메시지 유효 시간(초 단위)
|
||||
* 3.3. collapse_key: 동일 collapse_key를 가진 메시지는 최신 하나로 교체됨
|
||||
* 3.4. restricted_package_name: 메시지를 수신할 앱의 패키지 이름 (Android 전용)
|
||||
*/
|
||||
|
||||
[Table("payload")]
|
||||
public class DBPayload
|
||||
{
|
||||
[Key]
|
||||
[MaxLength(22)]
|
||||
public string pid { get; set; }
|
||||
public string title {get; set;}
|
||||
public string? subtitle {get; set;}
|
||||
public string body {get; set;}
|
||||
public bool alert_yn {get; set;}
|
||||
public string category {get; set;}
|
||||
public string content {get; set;}
|
||||
}
|
||||
|
||||
[Table("push_cabinet")]
|
||||
public class PushCabinet
|
||||
{
|
||||
public string uid { get; set; }
|
||||
public string pid { get; set; }
|
||||
public DateTime send_date { get; set; }
|
||||
public bool check_yn { get; set; }
|
||||
}
|
||||
|
||||
public class PushRequest
|
||||
{
|
||||
public List<string> uids { get; set; }
|
||||
public string pid { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class Payload
|
||||
{
|
||||
public Aps aps { get; set; }
|
||||
public string pid { get; set; }
|
||||
public string content { get; set; }
|
||||
// public string customKey { get; set; } 이런식으로 추가도 가능
|
||||
|
||||
public string ToJson()
|
||||
|
@ -51,8 +152,8 @@ public class PushFileSetting
|
|||
public string apnsTopic { get; set; }
|
||||
}
|
||||
|
||||
public class PushRequest
|
||||
public class PushData
|
||||
{
|
||||
public string deviceToken { get; set; }
|
||||
public string pushToken { get; set; }
|
||||
public Payload payload { get; set; }
|
||||
}
|
|
@ -59,6 +59,7 @@ public class User
|
|||
|
||||
[Required(ErrorMessage = "필수 항목 누락")]
|
||||
public DateTime login_date { get; set; }
|
||||
public string? push_token { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
@ -128,6 +129,7 @@ public class UserAll
|
|||
|
||||
public bool auto_login_yn { get; set; }
|
||||
public DateTime login_date { get; set; }
|
||||
public string? push_token { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "필수 항목 누락")]
|
||||
public string email { get; set; }
|
||||
|
|
|
@ -9,28 +9,28 @@ namespace AcaMate.V1.Services;
|
|||
|
||||
public interface IPushQueue
|
||||
{
|
||||
void Enqueue(PushRequest request);
|
||||
Task<PushRequest> DequeueAsync(CancellationToken cancellationToken);
|
||||
void Enqueue(PushData pushData);
|
||||
Task<PushData> DequeueAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public class InMemoryPushQueue: IPushQueue
|
||||
{
|
||||
private readonly ConcurrentQueue<PushRequest> _queue = new ConcurrentQueue<PushRequest>();
|
||||
private readonly ConcurrentQueue<PushData> _queue = new ConcurrentQueue<PushData>();
|
||||
private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);
|
||||
|
||||
public void Enqueue(PushRequest request)
|
||||
public void Enqueue(PushData pushData)
|
||||
{
|
||||
if( request is null )
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
_queue.Enqueue(request);
|
||||
if( pushData is null )
|
||||
throw new ArgumentNullException(nameof(pushData));
|
||||
_queue.Enqueue(pushData);
|
||||
_signal.Release();
|
||||
}
|
||||
|
||||
public async Task<PushRequest> DequeueAsync(CancellationToken cancellationToken)
|
||||
public async Task<PushData> DequeueAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _signal.WaitAsync(cancellationToken);
|
||||
_queue.TryDequeue(out var request);
|
||||
return request;
|
||||
_queue.TryDequeue(out var pushData);
|
||||
return pushData;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,10 +49,10 @@ public class PushBackgroundService : BackgroundService
|
|||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var pushRequest = await _queue.DequeueAsync(stoppingToken);
|
||||
var pushData = await _queue.DequeueAsync(stoppingToken);
|
||||
try
|
||||
{
|
||||
await _pushService.SendPushNotificationAsync(pushRequest.deviceToken, pushRequest.payload);
|
||||
await _pushService.SendPushNotificationAsync(pushData.pushToken, pushData.payload);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -17,6 +17,7 @@ public interface IRepositoryService
|
|||
{
|
||||
Task<bool> SaveData<T, K>(T entity, Expression<Func<T, K>> key) where T : class;
|
||||
Task<ValidateToken> ValidateToken(string token, string refresh);
|
||||
Task<bool> SaveDataFK<T>(T entity) where T : class;
|
||||
}
|
||||
|
||||
public class RepositoryService: IRepositoryService
|
||||
|
@ -140,5 +141,54 @@ public class RepositoryService: IRepositoryService
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// public async Task SaveData<T, T1>(T pushCabinet)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
|
||||
|
||||
public async Task<bool> SaveDataFK<T>(T entity) where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
// EF Core 메타데이터를 통해 엔티티 T의 기본 키 속성 정보를 가져옴
|
||||
var keyProperties = _dbContext.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties;
|
||||
|
||||
// 각 키 속성에 대해, entity에서 실제 키 값을 추출
|
||||
var keyValues = keyProperties
|
||||
.Select(p => typeof(T).GetProperty(p.Name).GetValue(entity))
|
||||
.ToArray();
|
||||
|
||||
// 기본 키 값을 이용해서 기존 엔티티를 찾음 (복합 키도 자동으로 처리됨)
|
||||
var existingEntity = await _dbContext.Set<T>().FindAsync(keyValues);
|
||||
|
||||
if (existingEntity != null)
|
||||
{
|
||||
_logger.LogInformation($"[{typeof(T)}] 기존 데이터 발견: 기본 키 값({string.Join(", ", keyValues)})");
|
||||
// 기존 엔티티를 업데이트: 새 entity의 값으로 교체
|
||||
_dbContext.Entry(existingEntity).CurrentValues.SetValues(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"[{typeof(T)}] 신규 데이터 등록: 기본 키 값({string.Join(", ", keyValues)})");
|
||||
// 데이터가 없으면 새 엔티티 추가
|
||||
_dbContext.Set<T>().Add(entity);
|
||||
}
|
||||
|
||||
// 변경 사항 저장
|
||||
await _dbContext.SaveChangesAsync();
|
||||
_logger.LogInformation($"[{typeof(T)}] DB 저장 완료");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"[{typeof(T)}] 저장 중 오류 발생: {ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user