[♻️] PUSH 로직 변경 - uid 와 pid 를 활용해 푸시 전송가능, 뱃지 설정 가능, 컨텐츠 추가 가능

This commit is contained in:
김선규 2025-03-05 15:30:06 +09:00
parent f443f3410c
commit 76d989a4fa
8 changed files with 270 additions and 73 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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