Merge pull request 'main' (#35) from seonkyu.kim/AcaMate_API:main into debug
All checks were successful
Back/pipeline/head This commit looks good
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:
commit
ace8fe8623
28
Diary/25.03.md
Normal file
28
Diary/25.03.md
Normal 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] 삭제 로직 통일하기
|
|
@ -25,11 +25,27 @@ 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; }
|
||||
|
||||
|
||||
|
||||
|
||||
//MARK: LOG
|
||||
public DbSet<LogPush> LogPush { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<User_Academy>()
|
||||
.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();
|
||||
}
|
||||
}
|
|
@ -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.V1.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using AcaMate.V1.Services;
|
||||
|
||||
namespace AcaMate.V1.Controllers;
|
||||
|
||||
|
@ -14,86 +21,327 @@ 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()]
|
||||
[CustomOperation("푸시 확인", "저장된 양식을 확인 할 수 있다..", "푸시")]
|
||||
public IActionResult GetPushData()
|
||||
[CustomOperation("푸시 확인", "저장된 양식을 확인 할 수 있다.", "푸시")]
|
||||
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>
|
||||
/// 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>
|
||||
/// <response code="500">Internal server error occurred.</response>
|
||||
/// <response code="999">Service unavailable.</response>
|
||||
[HttpPost("send")]
|
||||
[CustomOperation("푸시전송", "저장된 양식으로, 사용자에게 푸시를 전송한다.(로컬 테스트 불가)", "푸시")]
|
||||
[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)
|
||||
{
|
||||
string summary = String.Empty;
|
||||
try {
|
||||
summary = _repositoryService.ReadSummary(typeof(PushController), "SendPush");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(deviceToken) || payload == null)
|
||||
return BadRequest(APIResponse.InvalidInputError());
|
||||
if (!ModelState.IsValid) 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,
|
||||
payload = payload
|
||||
aps = new Aps
|
||||
{
|
||||
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("[푸시] 접수 완료");
|
||||
|
||||
//
|
||||
//
|
||||
// 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());
|
||||
// }
|
||||
if (payload != null) payload.aps.badge = badge + 1;
|
||||
|
||||
var pushData = new PushData
|
||||
{
|
||||
pushToken = pushToken,
|
||||
payload = payload ?? throw new PushInvalidException("payload is NULL")
|
||||
};
|
||||
await _repositoryService.SaveData<PushCabinet>(pushCabinet);
|
||||
|
||||
_pushQueue.Enqueue(pushData);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
return Ok(APIResponse.Send("000", "정상", Empty));
|
||||
|
||||
}
|
||||
catch (PushInvalidException ex)
|
||||
{
|
||||
_logger.LogError(ex.Message);
|
||||
return Ok(APIResponse.Send("001", $"[{summary}]: 푸시 송신 중 문제 발생",Empty));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"[푸시] {ex.Message}");
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -114,7 +114,8 @@ public class UserController : ControllerBase
|
|||
var refreshToken = _jwtTokenService.GenerateRefreshToken(uid);
|
||||
_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
|
||||
{
|
||||
|
@ -172,12 +173,12 @@ public class UserController : ControllerBase
|
|||
catch (TokenException tokenEx)
|
||||
{
|
||||
_logger.LogInformation($"[로그인] : {tokenEx}");
|
||||
return Ok(APIResponse.Send("001", "로그인 진행: 토큰에 문제가 있음",Empty));
|
||||
return Ok(APIResponse.Send("001", "[로그인] : 토큰에 문제가 있음",Empty));
|
||||
}
|
||||
catch (RefreshRevokeException refreshEx)
|
||||
{
|
||||
_logger.LogInformation($"[로그인] : {refreshEx}");
|
||||
return Ok(APIResponse.Send("001", "로그인 진행: 리프레시 토큰 폐기",Empty));
|
||||
return Ok(APIResponse.Send("001", "[로그인] : 폐기된 리프레시 토큰",Empty));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -205,7 +206,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
|
||||
{
|
||||
|
@ -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<Permission, string>(permission, p => p.uid);
|
||||
await _repositoryService.SaveData<Contact, string>(contact, c => c.uid);
|
||||
await _repositoryService.SaveData<Login>(login);
|
||||
await _repositoryService.SaveData<Permission>(permission);
|
||||
await _repositoryService.SaveData<Contact>(contact);
|
||||
}
|
||||
|
||||
// TO-DO: jwt 토큰 만들어서 여기서 보내는 작업을 해야 함
|
||||
var token = _jwtTokenService.GenerateJwtToken(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
|
||||
{
|
||||
|
@ -267,7 +270,7 @@ public class UserController : ControllerBase
|
|||
if (refreshToken != null)
|
||||
{
|
||||
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));
|
||||
}
|
||||
else
|
||||
|
|
|
@ -42,6 +42,7 @@ public class FileContentNotFoundException : Exception
|
|||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
21
Program/V1/Models/Log.cs
Normal 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; }
|
||||
}
|
|
@ -1,11 +1,115 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
|
||||
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 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 ToJson()
|
||||
|
@ -41,7 +145,7 @@ public class Alert
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 푸시 등록하기 위한 여러 데이터 목록
|
||||
/// 푸시 등록하기 위한 apns 여러 데이터 목록
|
||||
/// </summary>
|
||||
public class PushFileSetting
|
||||
{
|
||||
|
@ -51,8 +155,19 @@ 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; }
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -5,18 +5,23 @@ using AcaMate.Common.Models;
|
|||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
using System.Security.Claims;
|
||||
using AcaMate.V1.Models;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.VisualBasic;
|
||||
|
||||
using System.Linq.Expressions;
|
||||
using System.Security.Claims;
|
||||
using System.Reflection;
|
||||
|
||||
using AcaMate.V1.Models;
|
||||
|
||||
namespace AcaMate.V1.Services;
|
||||
|
||||
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> 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
|
||||
|
@ -31,58 +36,6 @@ public class RepositoryService: IRepositoryService
|
|||
_logger = logger;
|
||||
_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>
|
||||
/// 실제로 엑세스 토큰과 리프레시 토큰으로 접근 하기 위한 메서드
|
||||
|
@ -130,7 +83,7 @@ public class RepositoryService: IRepositoryService
|
|||
{
|
||||
refreshToken = _jwtTokenService.GenerateRefreshToken(uid);
|
||||
_logger.LogInformation("리프레시 토큰 만료");
|
||||
await SaveData<RefreshToken, string>(refreshToken, rt => rt.uid);
|
||||
// await SaveData<RefreshToken, string>(refreshToken, rt => rt.uid);
|
||||
return new ValidateToken
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user