Merge pull request 'main' (#35) from seonkyu.kim/AcaMate_API:main into debug
All checks were successful
Back/pipeline/head This commit looks good

Reviewed-on: https://git.ipstein.myds.me/AcaMate/AcaMate_API/pulls/35
This commit is contained in:
김선규 2025-03-07 08:39:49 +00:00
commit ace8fe8623
10 changed files with 695 additions and 145 deletions

28
Diary/25.03.md Normal file
View File

@ -0,0 +1,28 @@
# 2025년 3월 To-do
## 6일 (목)
### 1. PUSH API 만들기
1. [X] 푸시 목록 확인 : [./push]
2. [X] 푸시 만들기 [./push/create]
3. [ ] 푸시 삭제하기 [./push/delete]
4. [ ] 사용자가 받은 전체 푸시 확인 [./push/list]
### 2. 학원 구분이 가능하게 하는 방법으로 바꾸기
1. [x] 푸시 전송
2. [x] 푸시 변경
---
## 7일(금)
### 1. PUSH API 만들기
1. [X] 푸시 삭제하기 [./push/delete]
2. [ ] 사용자가 받은 전체 푸시 확인 [./push/list]
### 2. log 기록 남게 만들기
1. [ ] 유저 관련 테이블들 로그 기록 만들기
2. [ ] 푸시 관련 테이블들 로그 기록 만들기
### 3. 출력 및 오류 메세지 알아보기 쉽게 변경하기
1. [X] 메세지 출력에 summary 추가하기
### 4. DB 저장 & 삭제 로직 변경하기
1. [X] 저장 로직 통일하기
2. [X] 삭제 로직 통일하기

View File

@ -25,11 +25,27 @@ public class AppDbContext: DbContext
public DbSet<Location> Location { get; set; } public DbSet<Location> Location { get; set; }
public DbSet<Contact> Contact { get; set; } public DbSet<Contact> Contact { get; set; }
//MARK: PUSH
public DbSet<DBPayload> DBPayload { get; set; }
public DbSet<PushCabinet> PushCabinet { get; set; }
//MARK: LOG
public DbSet<LogPush> LogPush { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Entity<User_Academy>() modelBuilder.Entity<User_Academy>()
.HasKey(ua => new { ua.uid, ua.bid }); .HasKey(ua => new { ua.uid, ua.bid });
modelBuilder.Entity<PushCabinet>()
.HasKey(c => new { c.uid, c.bid, c.pid });
modelBuilder.Entity<DBPayload>()
.HasKey(p => new { p.bid, p.pid });
// modelBuilder.Entity<LogPush>().HasNoKey();
} }
} }

View File

@ -1,8 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Update.Internal;
using Microsoft.AspNetCore.Http.HttpResults;
using AcaMate.Common.Data;
using AcaMate.V1.Services;
using AcaMate.Common.Models; using AcaMate.Common.Models;
using AcaMate.V1.Models; using AcaMate.V1.Models;
using Microsoft.AspNetCore.Mvc;
using AcaMate.V1.Services;
namespace AcaMate.V1.Controllers; namespace AcaMate.V1.Controllers;
@ -14,86 +21,327 @@ public class PushController : ControllerBase
{ {
private readonly ILogger<PushController> _logger; private readonly ILogger<PushController> _logger;
private readonly IPushQueue _pushQueue; 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; _logger = logger;
_pushQueue = pushQueue; _pushQueue = pushQueue;
_dbContext = dbContext;
_repositoryService = repositoryService;
} }
[HttpGet()] [HttpGet()]
[CustomOperation("푸시 확인", "저장된 양식을 확인 할 수 있다..", "푸시")] [CustomOperation("푸시 확인", "저장된 양식을 확인 할 수 있다.", "푸시")]
public IActionResult GetPushData() public async Task<IActionResult> GetPush(string bid, string? pid, string? category)
{ {
return Ok("SEND"); if (!(await _dbContext.Academy.AnyAsync(a=>a.bid == bid)))
return Ok(APIResponse.Send("100", "존재하지 않는 BID", Empty));
List<DBPayload> pushData = new List<DBPayload>();
if (pid == null && category == null)
{
pushData = await _dbContext.DBPayload
.Where(p => p.bid == bid)
.ToListAsync();
} }
else if (pid != null && category == null)
{
pushData = await _dbContext.DBPayload
.Where(p => p.bid == bid && p.pid == pid)
.ToListAsync();
}
else if (pid == null && category != null)
{
pushData = await _dbContext.DBPayload
.Where(p => p.bid == bid && p.category == category)
.ToListAsync();
}
else //if (pid != null && category != null)
{
pushData = await _dbContext.DBPayload
.Where(p => p.bid == bid && p.pid == pid && p.category == category)
.ToListAsync();
}
try
{
if (pushData.Count > 0)
{
return Ok(APIResponse.Send("000", "정상", pushData));
}
return Ok(APIResponse.Send("001", "PUSH 데이터가 없음", Empty));
}
catch (Exception ex)
{
_logger.LogError($"[푸시] {ex.Message}");
return StatusCode(500, APIResponse.UnknownError());
}
}
/// <summary> /// <summary>
/// Sends a push notification to the specified device token with the provided payload. /// Sends a push notification to the specified device token with the provided payload.
/// </summary> /// </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> /// <returns>An IActionResult indicating the result of the operation.</returns>
/// <response code="200">Push notification sent successfully.</response> /// <response code="200">Push notification sent successfully.</response>
/// <response code="400">Invalid input parameters.</response> /// <response code="400">Invalid input parameters.</response>
/// <response code="500">Internal server error occurred.</response> /// <response code="500">Internal server error occurred.</response>
/// <response code="999">Service unavailable.</response> /// <response code="999">Service unavailable.</response>
[HttpPost("send")] [HttpPost("send")]
[CustomOperation("푸시전송", "저장된 양식으로, 사용자에게 푸시를 전송한다.(로컬 테스트 불가)", "푸시")] [CustomOperation("푸시송", "저장된 양식으로, 사용자에게 푸시를 송한다.", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))] [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)
{ {
string summary = String.Empty;
try {
summary = _repositoryService.ReadSummary(typeof(PushController), "SendPush");
if (string.IsNullOrWhiteSpace(deviceToken) || payload == null) if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
return BadRequest(APIResponse.InvalidInputError());
var pushRequest = new PushRequest var payload = await _dbContext.DBPayload
.Where(p => p.pid == pushRequest.pid && p.bid == pushRequest.bid)
.Select(p => new Payload
{ {
deviceToken = deviceToken, aps = new Aps
payload = payload {
alert = new Alert { title = p.title, body = p.body, subtitle = p.subtitle ?? "" },
category = p.category
},
pid = pushRequest.pid,
bid = pushRequest.bid,
content = p.content ?? "",
})
.FirstOrDefaultAsync();
await Task.Run(async () =>
{
foreach (var uid in pushRequest.uids)
{
if (
await _dbContext.UserAcademy
.Where(ua => ua.uid == uid && ua.bid == pushRequest.bid)
.AnyAsync()
)
{
var badge = await _dbContext.PushCabinet
.Where(c => c.uid == uid
&& c.bid == pushRequest.bid
&& c.pid != pushRequest.pid
&& c.check_yn == false)
.CountAsync();
var pushToken = await _dbContext.User
.Where(u => u.uid == uid)
.Select(u => u.push_token)
.FirstOrDefaultAsync() ?? "";
var pushCabinet = new PushCabinet
{
uid = uid,
bid = pushRequest.bid,
pid = pushRequest.pid,
send_date = DateTime.Now,
}; };
_pushQueue.Enqueue(pushRequest);
return Ok("[푸시] 접수 완료");
// if (payload != null) payload.aps.badge = badge + 1;
//
// var isDev = _env.IsDevelopment(); var pushData = new PushData
// {
// var pushFileSetting = new PushFileSetting() pushToken = pushToken,
// { payload = payload ?? throw new PushInvalidException("payload is NULL")
// 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", await _repositoryService.SaveData<PushCabinet>(pushCabinet);
// p12PWPath = "/src/private/appleKeys.json",
// apnsTopic = "me.myds.ipstein.acamate.AcaMate" _pushQueue.Enqueue(pushData);
// }; }
//
// try }
// { });
// if (await new ApnsPushService().SendPushNotificationAsync(deviceToken, pushFileSetting, payload))
// { return Ok(APIResponse.Send("000", "정상", Empty));
// return Ok(APIResponse.Send("000", "정상", Empty));
// } }
// else catch (PushInvalidException ex)
// { {
// return StatusCode(500, APIResponse.UnknownError()); _logger.LogError(ex.Message);
// } return Ok(APIResponse.Send("001", $"[{summary}]: 푸시 송신 중 문제 발생",Empty));
// }
// } catch (Exception ex)
// catch (ServiceConnectionFailedException failEx) {
// { _logger.LogError($"[푸시] {ex.Message}");
// _logger.LogError($"[푸시][에러] : {failEx}"); return StatusCode(500, APIResponse.UnknownError());
// 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());
// }
} }
} }
[HttpPost("set")]
[CustomOperation("[푸시 변경]", " .", "")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> SetPush([FromBody] DBPayload request)
{
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
try
{
var dbPayload = await _dbContext.DBPayload
.FirstOrDefaultAsync(p => p.pid == request.pid && p.bid == request.bid);
if (dbPayload != null)
{
if (dbPayload.title != request.title && request.title != "") dbPayload.title = request.title;
if (dbPayload.body != request.body && request.body != "") dbPayload.body = request.body;
if (dbPayload.subtitle != request.subtitle) dbPayload.subtitle = request.subtitle;
if (dbPayload.alert_yn != request.alert_yn) dbPayload.alert_yn = request.alert_yn;
if (dbPayload.category != request.category && request.category != "") dbPayload.category = request.category;
if (dbPayload.content != request.content) dbPayload.content = request.content;
// if (await _repositoryService.SaveData<DBPayload, string>(dbPayload, p => p.pid))
if (await _repositoryService.SaveData<DBPayload>(dbPayload))
return Ok(APIResponse.Send("000", "[푸시 변경] : PUSH 정보 변경 완료", Empty));
}
return Ok(APIResponse.Send("100", "PID, BID 또는 Cabinet 오류", Empty));
}
catch (Exception ex)
{
_logger.LogError($"[푸시] {ex.Message}");
return StatusCode(500, APIResponse.UnknownError());
}
}
[HttpPost("create")]
[CustomOperation("푸시 생성", "새로운 푸시 양식을 생성한다.", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> CreatePush(string token, string refresh, [FromBody] CreatePush createPush)
{
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(refresh)) return BadRequest(APIResponse.InvalidInputError());
if(!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
var validateToken = await _repositoryService.ValidateToken(token, refresh);
var uid = validateToken.uid;
Func<string, int, string> randomLetter = (letters, count) => new string(Enumerable.Range(0, count).Select(_ => letters[new Random().Next(letters.Length)]).ToArray());
var letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var digits = "0123456789";
var frontLetters = $"{randomLetter(letters, 1)}{randomLetter(digits, 1)}{randomLetter(letters, 1)}";
var afterLetters = $"{randomLetter(letters, 1)}{randomLetter(digits, 1)}{randomLetter(letters, 1)}";
string summary = String.Empty;
try {
summary = _repositoryService.ReadSummary(typeof(PushController), "CreatePush");
if (await _dbContext.Academy.AnyAsync(a => a.bid == createPush.bid))
{
DBPayload payload = new DBPayload
{
bid = createPush.bid,
pid = $"AP{DateTime.Now:yyyyMMdd}{frontLetters}{DateTime.Now:HHmmss}{afterLetters}",
title = createPush.title,
subtitle = createPush.subtitle,
body = createPush.body,
alert_yn = createPush.alert_yn,
category = createPush.category,
content = createPush.content,
};
if (await _repositoryService.SaveData<DBPayload>(payload))
{
var logPush = new LogPush
{
bid = payload.bid,
pid = payload.pid,
create_uid = uid,
create_date = DateTime.Now,
log = $"[{summary}] {payload.pid} 최초 생성 - {uid}"
};
// 로그를 이제 만들어서 추가를 해야 합니다.
if (await _repositoryService.SaveData<LogPush>(logPush))
_logger.LogInformation("[푸시 생성] 로그 추가");
return Ok(APIResponse.Send("000", "정상, push 저장 완료", Empty));
}
}
return Ok(APIResponse.Send("100", "학원 정보(BID) 확인 불가", Empty));
}
catch (TokenException tokenEx)
{
_logger.LogInformation($"[푸시 생성] : {tokenEx}");
return Ok(APIResponse.Send("001", "[푸시 생성] : 토큰에 문제가 있음",Empty));
}
catch (RefreshRevokeException refreshEx)
{
_logger.LogInformation($"[푸시 생성] : {refreshEx}");
return Ok(APIResponse.Send("001", "[푸시 생성] : 폐기된 리프레시 토큰",Empty));
}
catch (Exception ex)
{
_logger.LogError($"[푸시] {ex.Message}");
return StatusCode(500, APIResponse.UnknownError());
}
}
[HttpDelete("delete")]
[CustomOperation("푸시 삭제", "저장된 푸시 양식을 삭제 한다.", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> DeletePush(string token, string refresh, string bid, string pid)
{
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(refresh)) return BadRequest(APIResponse.InvalidInputError());
if(!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
var validateToken = await _repositoryService.ValidateToken(token, refresh);
var uid = validateToken.uid;
string summary = String.Empty;
try
{
summary = _repositoryService.ReadSummary(typeof(PushController), "DeletePush");
var payload = await _dbContext.DBPayload.FirstOrDefaultAsync(p => p.bid == bid && p.pid == pid);
if (await _repositoryService.DeleteData<DBPayload>(payload))
{
// 로그를 이제 만들어서 추가를 해야 합니다.
var logPush = new LogPush
{
bid = bid,
pid = pid,
create_uid = uid,
create_date = DateTime.Now,
log = $"[{summary}] {pid} 삭제 - {uid}"
};
// 로그를 이제 만들어서 추가를 해야 합니다.
if (await _repositoryService.SaveData<LogPush>(logPush))
_logger.LogInformation($"[{summary}] 로그 추가");
return Ok(APIResponse.Send("000", "정상, push 삭제 완료", Empty));
}
return Ok(APIResponse.Send("001", "push 삭제 실패", Empty));
}
catch (Exception ex)
{
_logger.LogError($"{ex}");
return BadRequest(APIResponse.UnknownError());
}
}
}// END PUSH CONTROLLER

View File

@ -114,7 +114,8 @@ public class UserController : ControllerBase
var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); var refreshToken = _jwtTokenService.GenerateRefreshToken(uid);
_logger.LogInformation($"{uid}: {accessToken}, {refreshToken}"); _logger.LogInformation($"{uid}: {accessToken}, {refreshToken}");
await _repositoryService.SaveData<RefreshToken, string>(refreshToken, rt => rt.uid); // await _repositoryService.SaveData<RefreshToken, string>(refreshToken, rt => rt.uid);
await _repositoryService.SaveData<RefreshToken>(refreshToken);
return Ok(APIResponse.Send("000","정상", new return Ok(APIResponse.Send("000","정상", new
{ {
@ -172,12 +173,12 @@ public class UserController : ControllerBase
catch (TokenException tokenEx) catch (TokenException tokenEx)
{ {
_logger.LogInformation($"[로그인] : {tokenEx}"); _logger.LogInformation($"[로그인] : {tokenEx}");
return Ok(APIResponse.Send("001", "로그인 진행: 토큰에 문제가 있음",Empty)); return Ok(APIResponse.Send("001", "[로그인] : 토큰에 문제가 있음",Empty));
} }
catch (RefreshRevokeException refreshEx) catch (RefreshRevokeException refreshEx)
{ {
_logger.LogInformation($"[로그인] : {refreshEx}"); _logger.LogInformation($"[로그인] : {refreshEx}");
return Ok(APIResponse.Send("001", "로그인 진행: 리프레시 토큰 폐기",Empty)); return Ok(APIResponse.Send("001", "[로그인] : 폐기된 리프레시 토큰",Empty));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -205,7 +206,8 @@ public class UserController : ControllerBase
type = request.type, type = request.type,
device_id = request.device_id, device_id = request.device_id,
auto_login_yn = request.auto_login_yn, 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 var login = new Login
{ {
@ -235,17 +237,18 @@ public class UserController : ControllerBase
}; };
if (await _repositoryService.SaveData<User, string>(user, u => u.uid)) if (await _repositoryService.SaveData<User>(user))
{ {
await _repositoryService.SaveData<Login, string>(login, l => l.sns_id); await _repositoryService.SaveData<Login>(login);
await _repositoryService.SaveData<Permission, string>(permission, p => p.uid); await _repositoryService.SaveData<Permission>(permission);
await _repositoryService.SaveData<Contact, string>(contact, c => c.uid); await _repositoryService.SaveData<Contact>(contact);
} }
// TO-DO: jwt 토큰 만들어서 여기서 보내는 작업을 해야 함 // TO-DO: jwt 토큰 만들어서 여기서 보내는 작업을 해야 함
var token = _jwtTokenService.GenerateJwtToken(uid); var token = _jwtTokenService.GenerateJwtToken(uid);
var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); var refreshToken = _jwtTokenService.GenerateRefreshToken(uid);
await _repositoryService.SaveData<RefreshToken, string>(refreshToken, rt => rt.uid);
await _repositoryService.SaveData<RefreshToken>(refreshToken);
return Ok(APIResponse.Send("000","정상",new return Ok(APIResponse.Send("000","정상",new
{ {
@ -267,7 +270,7 @@ public class UserController : ControllerBase
if (refreshToken != null) if (refreshToken != null)
{ {
refreshToken.revoke_Date = DateTime.Now; refreshToken.revoke_Date = DateTime.Now;
await _repositoryService.SaveData<RefreshToken, string>(refreshToken, rt => rt.uid); await _repositoryService.SaveData<RefreshToken>(refreshToken);
return Ok(APIResponse.Send("000", "로그아웃 정상", Empty)); return Ok(APIResponse.Send("000", "로그아웃 정상", Empty));
} }
else else

View File

@ -42,6 +42,7 @@ public class FileContentNotFoundException : Exception
{ {
} }
} }
/// <summary> /// <summary>
/// 외부 서비스에 연결시 연결 실패시 /// 외부 서비스에 연결시 연결 실패시
/// </summary> /// </summary>
@ -52,3 +53,24 @@ public class ServiceConnectionFailedException : Exception
} }
} }
/// <summary>
/// PUSH 서비스 중 데이터 사용에 문제가 발생했을시
/// </summary>
public class PushInvalidException : Exception
{
public PushInvalidException(string message) : base(message)
{
}
}
/// <summary>
/// 값이 있어야 하는데 NULL인 경우
/// </summary>
public class OutNULLException : Exception
{
public OutNULLException(string message) : base(message)
{
}
}

21
Program/V1/Models/Log.cs Normal file
View File

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Security.Cryptography.X509Certificates;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace AcaMate.V1.Models;
[Table("log_push")]
public class LogPush
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int lid { get; set; }
public string bid {get; set;}
public string pid {get; set;}
public DateTime create_date {get; set;}
public DateTime? update_date {get; set;}
public string create_uid {get; set;}
public string? update_uid {get; set;}
public string log { get; set; }
}

View File

@ -1,11 +1,115 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace AcaMate.V1.Models; 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
{
public string bid { get; set; }
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 string bid { get; set; }
public DateTime send_date { get; set; }
public bool check_yn { get; set; }
}
public class PushRequest
{
public string bid { get; set; }
public List<string> uids { get; set; }
public string pid { get; set; }
}
public class Payload public class Payload
{ {
public Aps aps { get; set; } public Aps aps { get; set; }
public string pid { get; set; }
public string bid { get; set; }
public string content { get; set; }
// public string customKey { get; set; } 이런식으로 추가도 가능 // public string customKey { get; set; } 이런식으로 추가도 가능
public string ToJson() public string ToJson()
@ -41,7 +145,7 @@ public class Alert
} }
/// <summary> /// <summary>
/// 푸시 등록하기 위한 여러 데이터 목록 /// 푸시 등록하기 위한 apns 여러 데이터 목록
/// </summary> /// </summary>
public class PushFileSetting public class PushFileSetting
{ {
@ -51,8 +155,19 @@ public class PushFileSetting
public string apnsTopic { get; set; } 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; } public Payload payload { get; set; }
} }
public class CreatePush
{
public string bid { get; set; }
public string title { get; set; }
public string? subtitle { get; set; }
public string body { get; set; }
public bool alert_yn { get; set; } = true;
public string category { get; set; }
public string? content { get; set; }
}

View File

@ -59,6 +59,7 @@ public class User
[Required(ErrorMessage = "필수 항목 누락")] [Required(ErrorMessage = "필수 항목 누락")]
public DateTime login_date { get; set; } 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 bool auto_login_yn { get; set; }
public DateTime login_date { get; set; } public DateTime login_date { get; set; }
public string? push_token { get; set; }
[Required(ErrorMessage = "필수 항목 누락")] [Required(ErrorMessage = "필수 항목 누락")]
public string email { get; set; } public string email { get; set; }

View File

@ -9,28 +9,28 @@ namespace AcaMate.V1.Services;
public interface IPushQueue public interface IPushQueue
{ {
void Enqueue(PushRequest request); void Enqueue(PushData pushData);
Task<PushRequest> DequeueAsync(CancellationToken cancellationToken); Task<PushData> DequeueAsync(CancellationToken cancellationToken);
} }
public class InMemoryPushQueue: IPushQueue 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); private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);
public void Enqueue(PushRequest request) public void Enqueue(PushData pushData)
{ {
if( request is null ) if( pushData is null )
throw new ArgumentNullException(nameof(request)); throw new ArgumentNullException(nameof(pushData));
_queue.Enqueue(request); _queue.Enqueue(pushData);
_signal.Release(); _signal.Release();
} }
public async Task<PushRequest> DequeueAsync(CancellationToken cancellationToken) public async Task<PushData> DequeueAsync(CancellationToken cancellationToken)
{ {
await _signal.WaitAsync(cancellationToken); await _signal.WaitAsync(cancellationToken);
_queue.TryDequeue(out var request); _queue.TryDequeue(out var pushData);
return request; return pushData;
} }
} }
@ -49,10 +49,10 @@ public class PushBackgroundService : BackgroundService
{ {
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{ {
var pushRequest = await _queue.DequeueAsync(stoppingToken); var pushData = await _queue.DequeueAsync(stoppingToken);
try try
{ {
await _pushService.SendPushNotificationAsync(pushRequest.deviceToken, pushRequest.payload); await _pushService.SendPushNotificationAsync(pushData.pushToken, pushData.payload);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -5,18 +5,23 @@ using AcaMate.Common.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using System.Security.Claims;
using AcaMate.V1.Models;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.VisualBasic; using Microsoft.VisualBasic;
using System.Linq.Expressions;
using System.Security.Claims;
using System.Reflection;
using AcaMate.V1.Models;
namespace AcaMate.V1.Services; namespace AcaMate.V1.Services;
public interface IRepositoryService 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<ValidateToken> ValidateToken(string token, string refresh);
Task<bool> SaveData<T>(T entity, Expression<Func<T, object>> key = null) where T : class;
Task<bool> DeleteData<T>(T entity, Expression<Func<T, object>> key = null) where T : class;
String ReadSummary(Type type, String name);
} }
public class RepositoryService: IRepositoryService public class RepositoryService: IRepositoryService
@ -31,58 +36,6 @@ public class RepositoryService: IRepositoryService
_logger = logger; _logger = logger;
_jwtTokenService = jwtTokenService; _jwtTokenService = jwtTokenService;
} }
public async Task<bool> SaveData<T, K> (T entity, Expression<Func<T, K>> key) where T : class
{
try
{
var value = key.Compile()(entity);
// x 라 함은 Expression 으로 생성되는 트리에서 T 타입으의 매개변수를 지칭함
var parameter = Expression.Parameter(typeof(T), "x");
var invokedExpr = Expression.Invoke(key, parameter);
var constantExpr = Expression.Constant(value, key.Body.Type);
var equalsExpr = Expression.Equal(invokedExpr, constantExpr);
var predicate = Expression.Lambda<Func<T, bool>>(equalsExpr, parameter);
var dbSet = _dbContext.Set<T>();
var entityData = await dbSet.FirstOrDefaultAsync(predicate);
if (entityData != null)
{
_logger.LogInformation($"[{typeof(T)}] 해당 PK 존재 [{value}]: 계속");
var entry = _dbContext.Entry(entityData);
entry.CurrentValues.SetValues(entity);
if (entry.Properties.Any(p => p.IsModified))
{
_logger.LogInformation($"[{typeof(T)}] 변경사항 존재: 계속");
}
else
{
_logger.LogInformation($"[{typeof(T)}] 변경사항 없음: 종료");
return true;
}
}
else
{
_logger.LogInformation($"[{typeof(T)}] 처음등록");
dbSet.Add(entity);
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"[{typeof(T)}] DB 저장 완료: 종료");
return true;
}
catch (Exception ex)
{
_logger.LogInformation($"[{typeof(T)}] 알 수 없는 오류: 종료 {ex}");
return false;
}
}
//토큰 태울때는 인코딩 된 걸로 태워야지 원본꺼 태우면 데이터에 손상옵니다. //토큰 태울때는 인코딩 된 걸로 태워야지 원본꺼 태우면 데이터에 손상옵니다.
/// <summary> /// <summary>
/// 실제로 엑세스 토큰과 리프레시 토큰으로 접근 하기 위한 메서드 /// 실제로 엑세스 토큰과 리프레시 토큰으로 접근 하기 위한 메서드
@ -130,7 +83,7 @@ public class RepositoryService: IRepositoryService
{ {
refreshToken = _jwtTokenService.GenerateRefreshToken(uid); refreshToken = _jwtTokenService.GenerateRefreshToken(uid);
_logger.LogInformation("리프레시 토큰 만료"); _logger.LogInformation("리프레시 토큰 만료");
await SaveData<RefreshToken, string>(refreshToken, rt => rt.uid); // await SaveData<RefreshToken, string>(refreshToken, rt => rt.uid);
return new ValidateToken return new ValidateToken
{ {
token = token, token = token,
@ -141,4 +94,146 @@ public class RepositoryService: IRepositoryService
} }
} }
public async Task<bool> SaveData<T>(T entity, Expression<Func<T, object>> key = null) where T : class
{
try
{
if (key != null)
{
// key를 가지고 EF 로 돌리는게 아니라 내가 조건을 넣어서 하는 경우에 사용함
var value = key.Compile()(entity);
var parameter = Expression.Parameter(typeof(T), "x");
var invokedExpr = Expression.Invoke(key, parameter);
var constantExpr = Expression.Constant(value, key.Body.Type);
var equalsExpr = Expression.Equal(invokedExpr, constantExpr);
var predicate = Expression.Lambda<Func<T, bool>>(equalsExpr, parameter);
var entityData = await _dbContext.Set<T>().FirstOrDefaultAsync(predicate);
if (entityData != null)
{
_logger.LogInformation($"[{typeof(T)}] 해당 PK 존재 = [{value}]: 계속");
_dbContext.Entry(entityData).CurrentValues.SetValues(entity);
if (!(_dbContext.Entry(entityData).Properties.Any(p => p.IsModified)))
{
_logger.LogInformation($"[{typeof(T)}] 변경 사항 없음");
return true;
}
_logger.LogInformation($"[{typeof(T)}] 변경 사항이 존재");
}
else
{
_logger.LogInformation($"[{typeof(T)}] 처음등록");
_dbContext.Set<T>().Add(entity);
}
}
else
{
// EF 로 직접 키를 잡아서 사용 (관계키나 이런거 할때도 노상관됨)
// 모델이 존재하지 않거나 기본 키 정의가 되지 않은 오류가 발생할 건데 그건 운영 단계에서는 오류 나면 안되는거니
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.LogInformation($"[{typeof(T)}] 저장 중 알 수 없는 오류 발생: {ex}");
return false;
}
}
public async Task<bool> DeleteData<T>(T entity, Expression<Func<T, object>> key = null) where T : class
{
try
{
if (key != null)
{
// key를 통해 조건식을 만들어 삭제할 엔티티를 찾는 경우
var value = key.Compile()(entity);
var parameter = Expression.Parameter(typeof(T), "x");
var invokedExpr = Expression.Invoke(key, parameter);
var constantExpr = Expression.Constant(value, key.Body.Type);
var equalsExpr = Expression.Equal(invokedExpr, constantExpr);
var predicate = Expression.Lambda<Func<T, bool>>(equalsExpr, parameter);
var entityData = await _dbContext.Set<T>().FirstOrDefaultAsync(predicate);
if (entityData == null)
{
_logger.LogInformation($"[{typeof(T)}] 삭제 대상 데이터가 존재하지 않습니다. (값 = {value})");
return false;
}
_logger.LogInformation($"[{typeof(T)}] 조건에 맞는 데이터 발견 (값 = {value}): 삭제 진행");
_dbContext.Set<T>().Remove(entityData);
}
else
{
// key가 없는 경우 EF Core 메타데이터를 사용하여 기본 키를 통한 삭제
var entityType = _dbContext.Model.FindEntityType(typeof(T));
if (entityType == null)
{
throw new InvalidOperationException($"Entity type '{typeof(T).Name}'이 모델에 존재하지 않습니다.");
}
var primaryKey = entityType.FindPrimaryKey();
if (primaryKey == null)
{
throw new InvalidOperationException($"Entity type '{typeof(T).Name}'에 기본 키가 정의되어 있지 않습니다.");
}
var keyProperties = primaryKey.Properties;
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)})에 해당하는 삭제 대상 데이터가 존재하지 않습니다.");
return false;
}
_logger.LogInformation($"[{typeof(T)}] 기본 키 값({string.Join(", ", keyValues)})에 해당하는 데이터 발견: 삭제 진행");
_dbContext.Set<T>().Remove(existingEntity);
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"[{typeof(T)}] DB에서 삭제 완료");
return true;
}
catch (Exception ex)
{
_logger.LogError($"[{typeof(T)}] 삭제 중 오류 발생: {ex}");
return false;
}
}
public string ReadSummary(Type type, string name)
{
var method = type.GetMethod(name) ?? throw new OutNULLException("swagger summary Load ERROR: NULL");
var att = method.GetCustomAttribute<CustomOperationAttribute>() ?? throw new OutNULLException("swagger summary Load ERROR: NULL");
return att.Summary;
}
} }