첨부 소스 코드는 나눔고딕코딩 폰트를 사용합니다.
728x90
반응형
728x170

TestSolution.zip
0.17MB

[TestServer 프로젝트]

▶ Properties/launchSettings.json

{
    "$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"
            }
        }
    }
}

 

728x90

 

▶ Models/Part.cs

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

 

300x250

 

▶ Models/PartFactory.cs

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

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

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

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

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

<?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

<?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

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

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

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

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

<?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

namespace TestClient;

/// <summary>
/// 부품 페이지
/// </summary>
public partial class PartPage : ContentPage
{
    //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
    ////////////////////////////////////////////////////////////////////////////////////////// Public

    #region 생성자 - PartPage()

    /// <summary>
    /// 생성자
    /// </summary>
    public PartPage()
    {
        InitializeComponent();

        BindingContext = new PartViewModel();
    }

    #endregion
}

 

▶ PAGE/AddPartPage.xaml

<?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/AddPartPage.xaml.cs

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

<?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

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

<?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

namespace TestClient;

/// <summary>
/// 앱
/// </summary>
public partial class App : Application
{
    //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
    ////////////////////////////////////////////////////////////////////////////////////////// Public

    #region 생성자 - App()

    /// <summary>
    /// 생성자
    /// </summary>
    public App()
    {
        InitializeComponent();

        MainPage = new AppShell();
    }

    #endregion
}

 

▶ MauiProgram.cs

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 포트 포워딩을 설정했다.

728x90
반응형
그리드형(광고전용)
Posted by icodebroker

댓글을 달아 주세요