diff --git a/Diary/25.03.md b/Diary/25.03.md new file mode 100644 index 0000000..709dc8b --- /dev/null +++ b/Diary/25.03.md @@ -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] 삭제 로직 통일하기 \ No newline at end of file diff --git a/Program/Common/Data/AppDbContext.cs b/Program/Common/Data/AppDbContext.cs index 3fddb4f..68ddc60 100644 --- a/Program/Common/Data/AppDbContext.cs +++ b/Program/Common/Data/AppDbContext.cs @@ -25,11 +25,27 @@ public class AppDbContext: DbContext public DbSet Location { get; set; } public DbSet Contact { get; set; } + //MARK: PUSH + public DbSet DBPayload { get; set; } + public DbSet PushCabinet { get; set; } + + + + + //MARK: LOG + public DbSet LogPush { get; set; } - protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasKey(ua => new { ua.uid, ua.bid }); + + modelBuilder.Entity() + .HasKey(c => new { c.uid, c.bid, c.pid }); + + modelBuilder.Entity() + .HasKey(p => new { p.bid, p.pid }); + + // modelBuilder.Entity().HasNoKey(); } } \ No newline at end of file diff --git a/Program/V1/Controllers/PushController.cs b/Program/V1/Controllers/PushController.cs index 12c209e..78f3562 100644 --- a/Program/V1/Controllers/PushController.cs +++ b/Program/V1/Controllers/PushController.cs @@ -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 _logger; private readonly IPushQueue _pushQueue; - public PushController(ILogger logger, IPushQueue pushQueue) + private readonly AppDbContext _dbContext; + private readonly IRepositoryService _repositoryService; + public PushController(ILogger logger, IPushQueue pushQueue, AppDbContext dbContext, IRepositoryService repositoryService) { _logger = logger; _pushQueue = pushQueue; + _dbContext = dbContext; + _repositoryService = repositoryService; } [HttpGet()] - [CustomOperation("푸시 확인", "저장된 양식을 확인 할 수 있다..", "푸시")] - public IActionResult GetPushData() + [CustomOperation("푸시 확인", "저장된 양식을 확인 할 수 있다.", "푸시")] + public async Task 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 pushData = new List(); + + 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()); + } } - - + + + + + /// /// Sends a push notification to the specified device token with the provided payload. /// - /// The device token to send the notification to. - /// The payload of the push notification. + /// /// An IActionResult indicating the result of the operation. /// Push notification sent successfully. /// Invalid input parameters. /// Internal server error occurred. /// Service unavailable. [HttpPost("send")] - [CustomOperation("푸시전송", "저장된 양식으로, 사용자에게 푸시를 전송한다.(로컬 테스트 불가)", "푸시")] + [CustomOperation("푸시 발송", "저장된 양식으로, 사용자에게 푸시를 송신한다.", "푸시")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus))] - public async Task SendPush(string deviceToken, [FromBody] Payload payload) + public async Task 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 + { + 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, + }; + + 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); + + _pushQueue.Enqueue(pushData); + } + + } + }); + + return Ok(APIResponse.Send("000", "정상", Empty)); + + } + catch (PushInvalidException ex) { - deviceToken = deviceToken, - payload = payload - }; - _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()); - // } + _logger.LogError(ex.Message); + return Ok(APIResponse.Send("001", $"[{summary}]: 푸시 송신 중 문제 발생",Empty)); + } + catch (Exception ex) + { + _logger.LogError($"[푸시] {ex.Message}"); + return StatusCode(500, APIResponse.UnknownError()); + } } -} \ No newline at end of file + + [HttpPost("set")] + [CustomOperation("[푸시 변경]", "저장된 양식을 변경한다.", "푸시")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus))] + public async Task 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, p => p.pid)) + if (await _repositoryService.SaveData(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))] + public async Task 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 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(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)) + _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))] + public async Task 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(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)) + _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 + + + + + diff --git a/Program/V1/Controllers/UserController.cs b/Program/V1/Controllers/UserController.cs index 00b68a6..eba1297 100644 --- a/Program/V1/Controllers/UserController.cs +++ b/Program/V1/Controllers/UserController.cs @@ -114,7 +114,8 @@ public class UserController : ControllerBase var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); _logger.LogInformation($"{uid}: {accessToken}, {refreshToken}"); - await _repositoryService.SaveData(refreshToken, rt => rt.uid); + // await _repositoryService.SaveData(refreshToken, rt => rt.uid); + await _repositoryService.SaveData(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, u => u.uid)) + if (await _repositoryService.SaveData(user)) { - await _repositoryService.SaveData(login, l => l.sns_id); - await _repositoryService.SaveData(permission, p => p.uid); - await _repositoryService.SaveData(contact, c => c.uid); + await _repositoryService.SaveData(login); + await _repositoryService.SaveData(permission); + await _repositoryService.SaveData(contact); } // TO-DO: jwt 토큰 만들어서 여기서 보내는 작업을 해야 함 var token = _jwtTokenService.GenerateJwtToken(uid); var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); - await _repositoryService.SaveData(refreshToken, rt => rt.uid); + + await _repositoryService.SaveData(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, rt => rt.uid); + await _repositoryService.SaveData(refreshToken); return Ok(APIResponse.Send("000", "로그아웃 정상", Empty)); } else diff --git a/Program/V1/Models/AcaException.cs b/Program/V1/Models/AcaException.cs index 2fac8b1..c282b34 100644 --- a/Program/V1/Models/AcaException.cs +++ b/Program/V1/Models/AcaException.cs @@ -42,6 +42,7 @@ public class FileContentNotFoundException : Exception { } } + /// /// 외부 서비스에 연결시 연결 실패시 /// @@ -50,5 +51,26 @@ public class ServiceConnectionFailedException : Exception public ServiceConnectionFailedException(string message) : base(message) { + } +} + +/// +/// PUSH 서비스 중 데이터 사용에 문제가 발생했을시 +/// +public class PushInvalidException : Exception +{ + public PushInvalidException(string message) : base(message) + { + + } +} +/// +/// 값이 있어야 하는데 NULL인 경우 +/// +public class OutNULLException : Exception +{ + public OutNULLException(string message) : base(message) + { + } } \ No newline at end of file diff --git a/Program/V1/Models/Log.cs b/Program/V1/Models/Log.cs new file mode 100644 index 0000000..58e538f --- /dev/null +++ b/Program/V1/Models/Log.cs @@ -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; } +} \ No newline at end of file diff --git a/Program/V1/Models/PushPayload.cs b/Program/V1/Models/PushPayload.cs index eac6d51..06a378f 100644 --- a/Program/V1/Models/PushPayload.cs +++ b/Program/V1/Models/PushPayload.cs @@ -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 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 } /// -/// 푸시 등록하기 위한 여러 데이터 목록 +/// 푸시 등록하기 위한 apns 여러 데이터 목록 /// 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; } } \ No newline at end of file diff --git a/Program/V1/Models/User.cs b/Program/V1/Models/User.cs index 2eab154..6e08fba 100644 --- a/Program/V1/Models/User.cs +++ b/Program/V1/Models/User.cs @@ -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; } diff --git a/Program/V1/Services/InMemoryPushQueue.cs b/Program/V1/Services/InMemoryPushQueue.cs index 2489771..ab7cbd8 100644 --- a/Program/V1/Services/InMemoryPushQueue.cs +++ b/Program/V1/Services/InMemoryPushQueue.cs @@ -9,28 +9,28 @@ namespace AcaMate.V1.Services; public interface IPushQueue { - void Enqueue(PushRequest request); - Task DequeueAsync(CancellationToken cancellationToken); + void Enqueue(PushData pushData); + Task DequeueAsync(CancellationToken cancellationToken); } public class InMemoryPushQueue: IPushQueue { - private readonly ConcurrentQueue _queue = new ConcurrentQueue(); + private readonly ConcurrentQueue _queue = new ConcurrentQueue(); 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 DequeueAsync(CancellationToken cancellationToken) + public async Task 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) { diff --git a/Program/V1/Services/RepositoryService.cs b/Program/V1/Services/RepositoryService.cs index 7b608cb..d4940ac 100644 --- a/Program/V1/Services/RepositoryService.cs +++ b/Program/V1/Services/RepositoryService.cs @@ -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 SaveData(T entity, Expression> key) where T : class; Task ValidateToken(string token, string refresh); + Task SaveData(T entity, Expression> key = null) where T : class; + Task DeleteData(T entity, Expression> 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 SaveData (T entity, Expression> 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>(equalsExpr, parameter); - - var dbSet = _dbContext.Set(); - - 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; - } - } - //토큰 태울때는 인코딩 된 걸로 태워야지 원본꺼 태우면 데이터에 손상옵니다. /// /// 실제로 엑세스 토큰과 리프레시 토큰으로 접근 하기 위한 메서드 @@ -130,7 +83,7 @@ public class RepositoryService: IRepositoryService { refreshToken = _jwtTokenService.GenerateRefreshToken(uid); _logger.LogInformation("리프레시 토큰 만료"); - await SaveData(refreshToken, rt => rt.uid); + // await SaveData(refreshToken, rt => rt.uid); return new ValidateToken { token = token, @@ -140,5 +93,147 @@ public class RepositoryService: IRepositoryService } } } + + public async Task SaveData(T entity, Expression> 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>(equalsExpr, parameter); + var entityData = await _dbContext.Set().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().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().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().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 DeleteData(T entity, Expression> 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>(equalsExpr, parameter); + + var entityData = await _dbContext.Set().FirstOrDefaultAsync(predicate); + if (entityData == null) + { + _logger.LogInformation($"[{typeof(T)}] 삭제 대상 데이터가 존재하지 않습니다. (값 = {value})"); + return false; + } + _logger.LogInformation($"[{typeof(T)}] 조건에 맞는 데이터 발견 (값 = {value}): 삭제 진행"); + _dbContext.Set().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().FindAsync(keyValues); + if (existingEntity == null) + { + _logger.LogInformation($"[{typeof(T)}] 기본 키 값({string.Join(", ", keyValues)})에 해당하는 삭제 대상 데이터가 존재하지 않습니다."); + return false; + } + _logger.LogInformation($"[{typeof(T)}] 기본 키 값({string.Join(", ", keyValues)})에 해당하는 데이터 발견: 삭제 진행"); + _dbContext.Set().Remove(existingEntity); + } + + await _dbContext.SaveChangesAsync(); + _logger.LogInformation($"[{typeof(T)}] DB에서 삭제 완료"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"[{typeof(T)}] 삭제 중 오류 발생: {ex}"); + return false; + } + } -} \ No newline at end of file + + public string ReadSummary(Type type, string name) + { + var method = type.GetMethod(name) ?? throw new OutNULLException("swagger summary Load ERROR: NULL"); + var att = method.GetCustomAttribute() ?? throw new OutNULLException("swagger summary Load ERROR: NULL"); + return att.Summary; + } +}