■ REST WEB API 서비스를 사용하는 방법을 보여준다. (ANDROID) (UWP)
[TestServer 프로젝트]
▶ Properties/launchSettings.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
{ "$schema" : "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication" : false, "anonymousAuthentication" : true, "iisExpress" : { "applicationUrl" : "http://localhost:46375", "sslPort" : 44369 } }, "profiles": { "PartServer" : { "commandName" : "Project", "dotnetRunMessages" : true, "launchBrowser" : true, "launchUrl" : "swagger", "applicationUrl" : "https://localhost:7210;http://localhost:5210", "environmentVariables" : { "ASPNETCORE_ENVIRONMENT" : "Development" } }, "IIS Express" : { "commandName" : "IISExpress", "launchBrowser" : true, "launchUrl" : "swagger", "environmentVariables" : { "ASPNETCORE_ENVIRONMENT" : "Development" } } } } |
▶ Models/Part.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
namespace TestServer.Models { /// <summary> /// 부품 /// </summary> public class Part { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 부품 ID - PartID /// <summary> /// 부품 ID /// </summary> public string PartID { get; set; } #endregion #region 부품명 - PartName /// <summary> /// 부품명 /// </summary> public string PartName { get; set; } #endregion #region 공급자 리스트 - SupplierList /// <summary> /// 공급자 리스트 /// </summary> public List<string> SupplierList { get; set; } #endregion #region 부품 이용 가능일 - PartAvailableDate /// <summary> /// 부품 이용 가능일 /// </summary> public DateTime PartAvailableDate { get; set; } #endregion #region 부품 타입 - PartType /// <summary> /// 부품 타입 /// </summary> public string PartType { get; set; } #endregion } } |
▶ Models/PartFactory.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
namespace TestServer.Models { /// <summary> /// 부품 팩토리 /// </summary> public static class PartFactory { //////////////////////////////////////////////////////////////////////////////////////////////////// Field ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Public #region Field /// <summary> /// 부품 딕셔너리 /// </summary> public static Dictionary<string, Tuple<DateTime, List<Part>>> PartDictionary = new Dictionary<string, Tuple<DateTime, List<Part>>>(); #endregion //////////////////////////////////////////////////////////////////////////////// Private #region Field /// <summary> /// 잠금자 /// </summary> private static object _locker = new object(); /// <summary> /// 난수 발생기 /// </summary> private static readonly Random _random = new Random(); #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Private #region 디폴트 부품 열거 가능형 - DefaultPartEnumerable /// <summary> /// 디폴트 부품 열거 가능형 /// </summary> private static IEnumerable<Part> DefaultPartEnumerable { get { yield return new Part { PartID = "0545685192", PartName = "Large motherboard", SupplierList = new List<string> { "A. Datum Corporation", "Allure Bays Corp", "Awesome Computers" }, PartAvailableDate = new DateTime(2019, 10, 1), PartType = "Circuit Board", }; yield return new Part { PartID = "0553801473", PartName = "RISC processor", SupplierList = new List<string> { "Allure Bays Corp", "Contoso Ltd", "Parnell Aerospace" }, PartAvailableDate = new DateTime(2021, 07, 12), PartType = "CPU", }; yield return new Part { PartID = "0544272994", PartName = "CISC processor", SupplierList = new List<string> { "Fabrikam, Inc", "A. Datum Corporation", "Parnell Aerospace" }, PartAvailableDate = new DateTime(2020, 9, 4), PartType = "CPU", }; yield return new Part { PartID = "141971189X", PartName = "High resolution card", SupplierList = new List<string> { "Awesome Computers" }, PartAvailableDate = new DateTime(2019, 11, 10), PartType = "Graphics Card", }; yield return new Part { PartID = "1256324778", PartName = "240V/110V switchable", SupplierList = new List<string> { "Reskit" }, PartAvailableDate = new DateTime(2021, 10, 21), PartType = "PSU", }; } } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Public #region 초기화하기 - Initialize(authorizationToken) /// <summary> /// 초기화하기 /// </summary> /// <param name="authorizationToken">권한 토큰</param> public static void Initialize(string authorizationToken) { lock(_locker) { PartDictionary.Add(authorizationToken, Tuple.Create(DateTime.UtcNow.AddHours(1), DefaultPartEnumerable.ToList())); } } #endregion #region 오래된 데이터 지우기 - ClearStaleData() /// <summary> /// 오래된 데이터 지우기 /// </summary> public static void ClearStaleData() { lock(_locker) { List<string> keyList = PartDictionary.Keys.ToList(); foreach(string key in keyList) { if(PartDictionary.TryGetValue(key, out Tuple<DateTime, List<Part>> result) && result.Item1 < DateTime.UtcNow) { PartDictionary.Remove(key); } } } } #endregion #region 부품 ID 생성하기 - CreatePartID() /// <summary> /// 부품 ID 생성하기 /// </summary> /// <returns>부품 ID</returns> public static string CreatePartID() { char[] characterArray = new char[10]; for(int i = 0; i < 10; i++) { characterArray[i] = (char)('0' + _random.Next(0, 9)); } return new string(characterArray); } #endregion } } |
▶ BaseController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
using System.Net; using Microsoft.AspNetCore.Mvc; using TestServer.Models; namespace TestServer.Controllers; /// <summary> /// 베이스 컨트롤러 /// </summary> [Route("api/[controller]")] [ApiController] public class BaseController : ControllerBase { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Protected #region 사용자 부품 리스트 - UserPartList /// <summary> /// 사용자 부품 리스트 /// </summary> protected List<Part> UserPartList { get { if(string.IsNullOrWhiteSpace(AuthorizationToken)) { return null; } if(!PartFactory.PartDictionary.ContainsKey(AuthorizationToken)) { return null; } Tuple<DateTime, List<Part>> tuple = PartFactory.PartDictionary[AuthorizationToken]; return tuple.Item2; } } #endregion #region 권한 토큰 - AuthorizationToken /// <summary> /// 권한 토큰 /// </summary> protected string AuthorizationToken { get { string authorizationToken = string.Empty; HttpContext context = HttpContext; if(context != null) { authorizationToken = context.Request.Headers["Authorization"].ToString(); } return authorizationToken; } } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Protected #region 권한 체크하기 - CheckAuthorization() /// <summary> /// 권한 체크하기 /// </summary> /// <returns>권한 체크 결과</returns> protected bool CheckAuthorization() { PartFactory.ClearStaleData(); try { HttpContext context = HttpContext; if(context != null) { if(string.IsNullOrWhiteSpace(AuthorizationToken)) { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; return false; } } else { return false; } if(!PartFactory.PartDictionary.ContainsKey(AuthorizationToken)) { return false; } return true; } catch { } return false; } #endregion } |
▶ LoginController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
using Microsoft.AspNetCore.Mvc; using TestServer.Models; namespace TestServer.Controllers; /// <summary> /// 로그인 컨트롤러 /// </summary> [Route("api/[controller]")] [ApiController] public class LoginController : BaseController { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 구하기 - Get() /// <summary> /// 구하기 /// </summary> /// <returns>액션 결과</returns> [HttpGet] public ActionResult Get() { try { string authorizationToken = Guid.NewGuid().ToString(); PartFactory.Initialize(authorizationToken); return new JsonResult(authorizationToken); } catch(Exception exception) { return new JsonResult(exception.Message); } } #endregion } |
▶ PartController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
using System.Net; using System.Text.Json; using Microsoft.AspNetCore.Mvc; using TestServer.Models; namespace TestServer.Controllers; /// <summary> /// 부품 컨트롤러 /// </summary> [Route("api/[controller]")] [ApiController] public class PartController : BaseController { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 조회하기 - Get() /// <summary> /// 조회하기 /// </summary> /// <returns>액션 결과</returns> [HttpGet] public ActionResult Get() { bool authorized = CheckAuthorization(); if(!authorized) { return Unauthorized(); } Console.WriteLine("GET /api/part"); return new JsonResult(UserPartList); } #endregion #region 조회하기 - Get(partID) /// <summary> /// 조회하기 /// </summary> /// <param name="partID">부품 ID</param> /// <returns>액션 결과</returns> [HttpGet("{partid}")] public ActionResult Get(string partID) { bool authorized = CheckAuthorization(); if(!authorized) { return Unauthorized(); } if(string.IsNullOrEmpty(partID)) { return BadRequest(); } partID = partID.ToUpperInvariant(); Console.WriteLine($"GET /api/part/{partID}"); List<Part> userPartList = UserPartList; Part part = userPartList.SingleOrDefault(x => x.PartID == partID); if(part == null) { return NotFound(); } else { return Ok(part); } } #endregion #region 추가하기 - Post(part) /// <summary> /// 추가하기 /// </summary> /// <param name="part">부품</param> /// <returns>액션 결과</returns> [HttpPost] public ActionResult Post([FromBody] Part part) { try { bool authorized = CheckAuthorization(); if(!authorized) { return Unauthorized(); } if(!string.IsNullOrWhiteSpace(part.PartID)) { return BadRequest(); } Console.WriteLine($"POST /api/part"); Console.WriteLine(JsonSerializer.Serialize(part)); part.PartID = PartFactory.CreatePartID(); if(!ModelState.IsValid) { return BadRequest(); } List<Part> userPartList = UserPartList; if(userPartList.Any(x => x.PartID == part.PartID)) { return this.Conflict(); } userPartList.Add(part); return this.Ok(part); } catch(Exception) { return this.Problem("Internal server error"); } } #endregion #region 수정하기 - Put(partID, part) /// <summary> /// 수정하기 /// </summary> /// <param name="partID">부품 ID</param> /// <param name="part">부품</param> /// <returns>응답 메시지</returns> [HttpPut("{partid}")] public HttpResponseMessage Put(string partID, [FromBody] Part part) { try { bool authorized = CheckAuthorization(); if(!authorized) { return new HttpResponseMessage(HttpStatusCode.Unauthorized); } if (!ModelState.IsValid) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } if(string.IsNullOrEmpty(part.PartID)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } Console.WriteLine($"PUT /api/part/{partID}"); Console.WriteLine(JsonSerializer.Serialize(part)); List<Part> userPartList = UserPartList; Part existingPart = userPartList.SingleOrDefault(x => x.PartID == partID); if(existingPart != null) { existingPart.SupplierList = part.SupplierList; existingPart.PartType = part.PartType; existingPart.PartAvailableDate = part.PartAvailableDate; existingPart.PartName = part.PartName; } return new HttpResponseMessage(HttpStatusCode.OK); } catch(Exception) { return new HttpResponseMessage(HttpStatusCode.InternalServerError); } } #endregion #region 삭제하기 - Delete(partID) /// <summary> /// 삭제하기 /// </summary> /// <param name="partID">부품 ID</param> /// <returns>응답 메시지</returns> [HttpDelete] [Route("{partid}")] public HttpResponseMessage Delete(string partID) { try { bool authorized = CheckAuthorization(); if(!authorized) { return new HttpResponseMessage(HttpStatusCode.Unauthorized); } List<Part> userPartList = UserPartList; Part existingPart = userPartList.SingleOrDefault(x => x.PartID == partID); if(existingPart == null) { return new HttpResponseMessage(HttpStatusCode.NotFound); } Console.WriteLine($"POST /api/part/{partID}"); userPartList.RemoveAll(x => x.PartID == partID); return new HttpResponseMessage(HttpStatusCode.OK); } catch(Exception) { return new HttpResponseMessage(HttpStatusCode.InternalServerError); } } #endregion } |
▶ Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.WebHost.UseUrls("https://*:7210", "http://*:5210"); // 외부에서 접속을 가능하게 해준다. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); WebApplication application = builder.Build(); application.UseSwagger(); application.UseSwaggerUI(); //application.UseHttpsRedirection(); // http만 접속시 주석 처리한다. application.UseAuthorization(); application.MapControllers(); application.Run(); |
[TestClient 프로젝트]
▶ Platforms/Android/Resources/xml/network_security_config.xml
1 2 3 4 5 6 7 8 9 |
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">125.33.218.112</domain> <domain includeSubdomains="true">asuscomm.com</domain> </domain-config> </network-security-config> |
※ 상기 파일을 추가하는 것은 테스트를 위해 http 접속을 허용하기 위한 것이다.
▶ Platforms/Android/AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true" android:networkSecurityConfig="@xml/network_security_config" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> </manifest> |
※ application 태그의 android:networkSecurityConfig 특성을 설정하는 것은 테스트를 위해 http 접속을 허용하기 위한 것이다.
▶ DATA/Part.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
using System.ComponentModel; using System.Text; namespace TestClient; /// <summary> /// 부품 /// </summary> [Serializable] public class Part : INotifyPropertyChanged { //////////////////////////////////////////////////////////////////////////////////////////////////// Event ////////////////////////////////////////////////////////////////////////////////////////// Public #region 속성 변경시 - PropertyChanged /// <summary> /// 속성 변경시 /// </summary> public event PropertyChangedEventHandler PropertyChanged; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Field ////////////////////////////////////////////////////////////////////////////////////////// Private #region Field /// <summary> /// 부품 ID /// </summary> private string partID; /// <summary> /// 부품명 /// </summary> private string partName; /// <summary> /// 부품 타입 /// </summary> private string partType; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 부품 ID - PartID /// <summary> /// 부품 ID /// </summary> public string PartID { get => this.partID; set { if(this.partID == value) { return; } this.partID = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PartID))); } } #endregion #region 부품명 - PartName /// <summary> /// 부품명 /// </summary> public string PartName { get => this.partName; set { if(this.partName == value) { return; } this.partName = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PartName))); } } #endregion #region 부품 타입 - PartType /// <summary> /// 부품 타입 /// </summary> public string PartType { get => this.partType; set { if(this.partType == value) { return; } this.partType = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PartType))); } } #endregion #region 공급자 리스트 - SupplierList /// <summary> /// 공급자 리스트 /// </summary> public List<string> SupplierList { get; set; } #endregion #region 공급자 리스트 2 - SupplierList2 /// <summary> /// 공급자 리스트 2 /// </summary> public string SupplierList2 { get { StringBuilder stringBuilder = new StringBuilder(); foreach(string supplier in SupplierList) { stringBuilder.Append($"{supplier}, "); } return stringBuilder.ToString().TrimEnd(','); } } #endregion #region 부품 이용 가능일 - PartAvailableDate /// <summary> /// 부품 이용 가능일 /// </summary> public DateTime PartAvailableDate { get; set; } #endregion } |
▶ DATA/PartManager.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
using System.Net.Http.Json; using Newtonsoft.Json; namespace TestClient { /// <summary> /// 부품 관리자 /// </summary> public static class PartManager { //////////////////////////////////////////////////////////////////////////////////////////////////// Field ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Private #region Field /// <summary> /// HTTP 클라이언트 /// </summary> private static HttpClient _client; /// <summary> /// 기본 주소 /// </summary> private static readonly string _baseAddress = "http://icodebroker.asuscomm.com:5210"; /// <summary> /// URL /// </summary> private static readonly string _url = $"{_baseAddress}/api/"; /// <summary> /// 권한 키 /// </summary> private static string _authorizationKey; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Public #region 부품 열거 가능형 구하기 (비동기) - GetPartEnumerableAsync() /// <summary> /// 부품 열거 가능형 구하기 (비동기) /// </summary> /// <returns>부품 열거 가능형 태스크</returns> public static async Task<IEnumerable<Part>> GetPartEnumerableAsync() { if(Connectivity.Current.NetworkAccess != NetworkAccess.Internet) { return new List<Part>(); } HttpClient client = await GetClient(); string result = await client.GetStringAsync($"{_url}part"); return JsonConvert.DeserializeObject<List<Part>>(result); } #endregion #region 추가하기 (비동기) - AddAsync(partName, partType, supplier) /// <summary> /// 추가하기 (비동기) /// </summary> /// <param name="partName">부품명</param> /// <param name="partType">부품 타입</param> /// <param name="supplier">공급자</param> /// <returns>부품 태스크</returns> public static async Task<Part> AddAsync(string partName, string partType, string supplier) { if(Connectivity.Current.NetworkAccess != NetworkAccess.Internet) { return new Part(); } Part part = new Part() { PartName = partName, SupplierList = new List<string>(new[] { supplier }), PartID = string.Empty, PartType = partType, PartAvailableDate = DateTime.Now.Date }; HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, $"{_url}part"); requestMessage.Content = JsonContent.Create<Part>(part); HttpResponseMessage responseMessage = await _client.SendAsync(requestMessage); responseMessage.EnsureSuccessStatusCode(); string json = await responseMessage.Content.ReadAsStringAsync(); Part partInserted = JsonConvert.DeserializeObject<Part>(json); return partInserted; } #endregion #region 수정하기 비동기) - UpdateAsync(part) /// <summary> /// 수정하기 비동기) /// </summary> /// <param name="part">부품</param> /// <returns>태스크</returns> public static async Task UpdateAsync(Part part) { if(Connectivity.Current.NetworkAccess != NetworkAccess.Internet) { return; } HttpRequestMessage requestMessage = new(HttpMethod.Put, $"{_url}part/{part.PartID}"); requestMessage.Content = JsonContent.Create<Part>(part); HttpClient client = await GetClient(); HttpResponseMessage responseMessage = await client.SendAsync(requestMessage); responseMessage.EnsureSuccessStatusCode(); } #endregion #region 삭제하기 (비동기) - DeleteAsync(partID) /// <summary> /// 삭제하기 (비동기) /// </summary> /// <param name="partID">부품 ID</param> /// <returns>태스크</returns> public static async Task DeleteAsync(string partID) { if(Connectivity.Current.NetworkAccess != NetworkAccess.Internet) { return; } HttpRequestMessage msg = new(HttpMethod.Delete, $"{_url}part/{partID}"); HttpClient client = await GetClient(); var response = await client.SendAsync(msg); response.EnsureSuccessStatusCode(); } #endregion //////////////////////////////////////////////////////////////////////////////// Private #region HTTP 클라이언트 구하기 - GetClient() /// <summary> /// HTTP 클라이언트 구하기 /// </summary> /// <returns>HTTP 클라이언트</returns> private static async Task<HttpClient> GetClient() { if(_client != null) { return _client; } _client = new HttpClient(); if(string.IsNullOrEmpty(_authorizationKey)) { _authorizationKey = await _client.GetStringAsync($"{_url}login"); _authorizationKey = JsonConvert.DeserializeObject<string>(_authorizationKey); } _client.DefaultRequestHeaders.Add("Authorization", _authorizationKey ); _client.DefaultRequestHeaders.Add("Accept" , "application/json"); return _client; } #endregion } } |
▶ VIEWMODEL/PartViewModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
using System.Collections.ObjectModel; using System.ComponentModel; using System.Windows.Input; namespace TestClient; /// <summary> /// 부품 뷰 모델 /// </summary> public class PartViewModel : INotifyPropertyChanged { //////////////////////////////////////////////////////////////////////////////////////////////////// Event ////////////////////////////////////////////////////////////////////////////////////////// Public #region 속성 변경시 - PropertyChanged /// <summary> /// 속성 변경시 /// </summary> public event PropertyChangedEventHandler PropertyChanged; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Field ////////////////////////////////////////////////////////////////////////////////////////// Private #region Field /// <summary> /// 갱신 여부 /// </summary> private bool isRefreshing = false; /// <summary> /// 실행 여부 /// </summary> private bool isBusy = false; /// <summary> /// 부품 컬렉션 /// </summary> private ObservableCollection<Part> partCollection; /// <summary> /// 선택 부품 /// </summary> private Part selectedPart; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 갱신 여부 - IsRefreshing /// <summary> /// 갱신 여부 /// </summary> public bool IsRefreshing { get => this.isRefreshing; set { if(this.isRefreshing == value) { return; } this.isRefreshing = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRefreshing))); } } #endregion #region 실행 여부 - IsBusy /// <summary> /// 실행 여부 /// </summary> public bool IsBusy { get => this.isBusy; set { if(this.isBusy == value) { return; } this.isBusy = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsBusy))); } } #endregion #region 부품 컬렉션 - PartCollection /// <summary> /// 부품 컬렉션 /// </summary> public ObservableCollection<Part> PartCollection { get => this.partCollection; set => this.partCollection = value; } #endregion #region 선택 부품 - SelectedPart /// <summary> /// 선택 부품 /// </summary> public Part SelectedPart { get => this.selectedPart; set { if(this.selectedPart == value) { return; } this.selectedPart = value; PropertyChanged.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedPart))); } } #endregion #region 데이터 로드 명령 - LoadDataCommand /// <summary> /// 데이터 로드 명령 /// </summary> public ICommand LoadDataCommand { get; private set; } #endregion #region 부품 선택시 명령 - PartSelectedCommand /// <summary> /// 부품 선택시 명령 /// </summary> public ICommand PartSelectedCommand { get; private set; } #endregion #region 신규 부품 추가 명령 - AddNewPartCommand /// <summary> /// 신규 부품 추가 명령 /// </summary> public ICommand AddNewPartCommand { get; private set; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - PartViewModel() /// <summary> /// 생성자 /// </summary> public PartViewModel() { this.partCollection = new ObservableCollection<Part>(); LoadDataCommand = new Command(async () => await ProcessLoadData()); PartSelectedCommand = new Command(async () => await ProcessPartSelected()); AddNewPartCommand = new Command(async () => await Shell.Current.GoToAsync("addpart")); MessagingCenter.Subscribe<AddPartViewModel>(this, "refresh", async (sender) => await ProcessLoadData()); Task.Run(ProcessLoadData); } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 데이터 로드 처리하기 - ProcessLoadData() /// <summary> /// 데이터 로드 처리하기 /// </summary> /// <returns>태스크</returns> private async Task ProcessLoadData() { if(IsBusy) { return; } try { IsRefreshing = true; IsBusy = true; IEnumerable<Part> partEnumerable = await PartManager.GetPartEnumerableAsync(); MainThread.BeginInvokeOnMainThread ( () => { PartCollection.Clear(); foreach(Part part in partEnumerable) { PartCollection.Add(part); } } ); } finally { IsRefreshing = false; IsBusy = false; } } #endregion #region 부품 선택시 처리하기 - ProcessPartSelected() /// <summary> /// 부품 선택시 처리하기 /// </summary> /// <returns>태스크</returns> private async Task ProcessPartSelected() { if(SelectedPart == null) { return; } Dictionary<string, object> parameterDictionary = new Dictionary<string, object>() { { "part", SelectedPart } }; await Shell.Current.GoToAsync("addpart", parameterDictionary); MainThread.BeginInvokeOnMainThread(() => SelectedPart = null); } #endregion } |
▶ VIEWMODEL/AddPartViewModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
using System.ComponentModel; using System.Windows.Input; namespace TestClient; /// <summary> /// 부품 추가 뷰 모델 /// </summary> public class AddPartViewModel : INotifyPropertyChanged { //////////////////////////////////////////////////////////////////////////////////////////////////// Event ////////////////////////////////////////////////////////////////////////////////////////// Public #region 속성 변경시 - PropertyChanged /// <summary> /// 속성 변경시 /// </summary> public event PropertyChangedEventHandler PropertyChanged; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Field ////////////////////////////////////////////////////////////////////////////////////////// Private #region Field /// <summary> /// 부품 ID /// </summary> private string partID; /// <summary> /// 부품명 /// </summary> private string partName; /// <summary> /// 공급자 리스트 /// </summary> private string supplierList; /// <summary> /// 부품 타입 /// </summary> private string partType; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 부품 ID - PartID /// <summary> /// 부품 ID /// </summary> public string PartID { get => this.partID; set { if(this.partID == value) { return; } this.partID = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PartID))); } } #endregion #region 부품명 - PartName /// <summary> /// 부품명 /// </summary> public string PartName { get => this.partName; set { if(this.partName == value) { return; } this.partName = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PartName))); } } #endregion #region 공급자 리스트 - SupplierList /// <summary> /// 공급자 리스트 /// </summary> public string SupplierList { get => this.supplierList; set { if(this.supplierList == value) { return; } this.supplierList = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SupplierList))); } } #endregion #region 부품 타입 - PartType /// <summary> /// 부품 타입 /// </summary> public string PartType { get => this.partType; set { if(this.partType == value) { return; } this.partType = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PartType))); } } #endregion #region 편집 완료시 명령 - DoneEditingCommand /// <summary> /// 편집 완료시 명령 /// </summary> public ICommand DoneEditingCommand { get; private set; } #endregion #region 저장 명령 - SaveCommand /// <summary> /// 저장 명령 /// </summary> public ICommand SaveCommand { get; private set; } #endregion #region 삭제 명령 - DeleteCommand /// <summary> /// 삭제 명령 /// </summary> public ICommand DeleteCommand { get; private set; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - AddPartViewModel() /// <summary> /// 생성자 /// </summary> public AddPartViewModel() { DoneEditingCommand = new Command(async () => await ProcessEditingDone()); SaveCommand = new Command(async () => await ProcessSave()); DeleteCommand = new Command(async () => await ProcessDelete()); } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Private #region 부품 삽입하기 - InsertPart() /// <summary> /// 부품 삽입하기 /// </summary> /// <returns>태스크</returns> private async Task InsertPart() { await PartManager.AddAsync(PartName, PartType, SupplierList); MessagingCenter.Send(this, "refresh"); await Shell.Current.GoToAsync(".."); } #endregion #region 부품 수정하기 - UpdatePart() /// <summary> /// 부품 수정하기 /// </summary> /// <returns>태스크</returns> private async Task UpdatePart() { Part partToSave = new() { PartID = PartID, PartName = PartName, PartType = PartType, SupplierList = SupplierList.Split(",").ToList() }; await PartManager.UpdateAsync(partToSave); MessagingCenter.Send(this, "refresh"); await Shell.Current.GoToAsync(".."); } #endregion #region 편집 완료시 처리하기 - ProcessEditingDone() /// <summary> /// 편집 완료시 처리하기 /// </summary> /// <returns>태스크</returns> private async Task ProcessEditingDone() { await Shell.Current.GoToAsync(".."); } #endregion #region 저장 처리하기 - ProcessSave() /// <summary> /// 저장 처리하기 /// </summary> /// <returns>태스크</returns> private async Task ProcessSave() { if(string.IsNullOrWhiteSpace(PartID)) { await InsertPart(); } else { await UpdatePart(); } } #endregion #region 삭제 처리하기 - ProcessDelete() /// <summary> /// 삭제 처리하기 /// </summary> /// <returns>태스크</returns> private async Task ProcessDelete() { if(string.IsNullOrWhiteSpace(PartID)) { return; } await PartManager.DeleteAsync(PartID); MessagingCenter.Send(this, "refresh"); await Shell.Current.GoToAsync(".."); } #endregion } |
▶ PAGE/PartPage.xaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<?xml version="1.0" encoding="utf-8" ?> <ContentPage x:Class="TestClient.PartPage" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"> <VerticalStackLayout> <Button Margin="20,10,20,10" Text="신규 부품 추가하기" Command="{Binding AddNewPartCommand}" /> <RefreshView x:Name="refreshView" IsRefreshing="{Binding IsRefreshing}" Command="{Binding LoadDataCommand}"> <CollectionView Margin="30,20,30,30" ItemsSource="{Binding PartCollection}" SelectedItem="{Binding SelectedPart, Mode=TwoWay}" SelectionChangedCommand="{Binding PartSelectedCommand}" SelectionMode="Single"> <CollectionView.ItemsLayout> <LinearItemsLayout Orientation="Vertical" ItemSpacing="20" /> </CollectionView.ItemsLayout> <CollectionView.ItemTemplate> <DataTemplate> <VerticalStackLayout Margin="10,5,10,5" Padding="15,10"> <Label Margin="0,0,0,20" FontSize="Title" Text="{Binding PartID, StringFormat='ID: {0}'}" /> <Label Text="{Binding PartName, StringFormat='부품명 : {0}'}"/> <Label Text="{Binding PartType, StringFormat='부품 타입 : {0}'}"/> </VerticalStackLayout> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </RefreshView> </VerticalStackLayout> </ContentPage> |
▶ PAGE/PartPage.xaml.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
namespace TestClient; /// <summary> /// 부품 페이지 /// </summary> public partial class PartPage : ContentPage { //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - PartPage() /// <summary> /// 생성자 /// </summary> public PartPage() { InitializeComponent(); BindingContext = new PartViewModel(); } #endregion } |
▶ PAGE/AddPagePart.xaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<?xml version="1.0" encoding="utf-8" ?> <ContentPage x:Class="TestClient.AddPartPage" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" Title="부품 편집"> <Grid RowDefinitions="*,Auto" ColumnDefinitions="*,*,*" ColumnSpacing="5"> <TableView Grid.Row="0" Grid.ColumnSpan="3" Intent="Data"> <TableRoot> <TableSection Title="부품 정보"> <EntryCell Label="부품 ID" Text="{Binding PartID}" IsEnabled="False" /> <EntryCell Label="부품명" Text="{Binding PartName}" /> <EntryCell Label="부품 타입" Text="{Binding PartType}" /> <EntryCell Label="공급자" Text="{Binding SupplierList}" /> </TableSection> </TableRoot> </TableView> <Button Grid.Row="1" Grid.Column="0" Margin="10" Text="저장" Command="{Binding SaveCommand}" /> <Button Grid.Row="1" Grid.Column="1" Margin="10" Text="삭제" Command="{Binding DeleteCommand}"/> <Button Grid.Row="1" Grid.Column="2" Margin="10" Text="취소" Command="{Binding DoneEditingCommand}" /> </Grid> </ContentPage> |
▶ PAGE/AddPagePart.xaml.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
namespace TestClient; /// <summary> /// 파트 추가 페이지 /// </summary> [QueryProperty("PartToDisplay", "part")] public partial class AddPartPage : ContentPage { //////////////////////////////////////////////////////////////////////////////////////////////////// Field ////////////////////////////////////////////////////////////////////////////////////////// Private #region Field /// <summary> /// 뷰 모델 /// </summary> private AddPartViewModel viewModel; /// <summary> /// 표시할 부품 /// </summary> private Part partToDisplay; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 표시할 부품 - PartToDisplay /// <summary> /// 표시할 부품 /// </summary> public Part PartToDisplay { get => this.partToDisplay; set { if(this.partToDisplay == value) { return; } this.partToDisplay = value; this.viewModel.PartID = this.partToDisplay.PartID; this.viewModel.PartName = this.partToDisplay.PartName; this.viewModel.SupplierList = this.partToDisplay.SupplierList2; this.viewModel.PartType = this.partToDisplay.PartType; } } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - AddPartPage() /// <summary> /// 생성자 /// </summary> public AddPartPage() { InitializeComponent(); this.viewModel = new AddPartViewModel(); BindingContext = this.viewModel; } #endregion } |
▶ AppShell.xaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?xml version="1.0" encoding="UTF-8" ?> <Shell x:Class="TestClient.AppShell" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:TestClient" Shell.FlyoutBehavior="Disabled"> <TabBar> <Tab> <ShellContent Title="부품 리스트" Route="listparts" ContentTemplate="{DataTemplate local:PartPage}" /> </Tab> </TabBar> </Shell> |
▶ AppShell.xaml.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
namespace TestClient; /// <summary> /// 앱 셸 /// </summary> public partial class AppShell : Shell { //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - AppShell() /// <summary> /// 생성자 /// </summary> public AppShell() { InitializeComponent(); Routing.RegisterRoute("addpart", typeof(AddPartPage)); } #endregion } |
▶ App.xaml
1 2 3 4 5 6 |
<?xml version = "1.0" encoding = "UTF-8" ?> <Application x:Class="TestClient.App" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" /> |
▶ App.xaml.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
namespace TestClient; /// <summary> /// 앱 /// </summary> public partial class App : Application { //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - App() /// <summary> /// 생성자 /// </summary> public App() { InitializeComponent(); MainPage = new AppShell(); } #endregion } |
▶ MauiProgram.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
namespace TestClient; /// <summary> /// MAUI 프로그램 /// </summary> public static class MauiProgram { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Public #region MAUI 앱 생성하기 - CreateMauiApp() /// <summary> /// MAUI 앱 생성하기 /// </summary> /// <returns>MAUI 앱</returns> public static MauiApp CreateMauiApp() { MauiAppBuilder builder = MauiApp.CreateBuilder(); builder.UseMauiApp<App>() .ConfigureFonts ( fontCollection => { fontCollection.AddFont("OpenSans-Regular.ttf" , "OpenSansRegular" ); fontCollection.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); } ); return builder.Build(); } #endregion } |
※ 외부 접속을 위해 개인적으로 서버 방화벽 및 무선 AP 포트 포워딩을 설정했다.