첨부 실행 코드는 나눔고딕코딩 폰트를 사용합니다.
본 블로그는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 블로그 콘텐츠 향상을 위해 쓰여집니다.

728x90
반응형
728x170

TestProject.zip
다운로드

▶ Database/TestDB.sql

CREATE TABLE dbo.[User]
(
    ID         INT          NOT NULL IDENTITY(1, 1) PRIMARY KEY
   ,UserID     NVARCHAR(50) NOT NULL
   ,[Password] NVARCHAR(50) NOT NULL
)
GO

CREATE PROCEDURE dbo.WriteUser
    @UserID   NVarChar(50),
    @Password NVarChar(50)
AS
    INSERT INTO dbo.[User] VALUES (@UserID, @Password);
GO

CREATE PROCEDURE dbo.ListUsers
AS
    SELECT
        ID
       ,UserID
       ,[Password]
    FROM dbo.[User]
    ORDER BY ID DESC;
GO

CREATE PROCEDURE dbo.ViewUser
    @ID Int
AS
    SELECT
        ID
       ,UserID
       ,[Password]
    FROM dbo.[User]
    Where ID = @ID;
GO

CREATE PROCEDURE dbo.ModifyUser
    @UserID   NVARCHAR(50)
   ,@Password NVARCHAR(50)
   ,@ID       INT
AS
    UPDATE dbo.[User]
    SET
        UserID     = @UserID,
        [Password] = @Password
    WHERE ID = @ID;
GO

CREATE PROCEDURE dbo.DeleteUser
    @ID INT
AS
    DELETE FROM dbo.[User] WHERE ID = @ID;
GO

CREATE PROCEDURE dbo.SearchUsers
    @SearchField NVarChar(50),
    @SearchQuery NVarChar(50)
AS
    DECLARE @SQL NVarChar(2000);

    SET @SQL = 'SELECT * FROM dbo.[User] WHERE ' + @SearchField + ' LIKE ''%' + @SearchQuery + '%''';

    EXECUTE @SQL;
GO

 

728x90

 

▶ appsettings.json

{
    "ConnectionStrings" :
    {
        "DefaultConnection" : "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=TestDB;Integrated Security=True"
    },
    "Logging" :
    {
        "LogLevel" :
        {
            "Default"                    : "Information",
            "Microsoft"                  : "Warning",
            "Microsoft.Hosting.Lifetime" : "Information"
        }
    },
    "AllowedHosts" : "*",
    "MainSettings" :
    {
        "SiteAdministrator" : "admin"
    }
}

 

300x250

 

▶ Settings/MainSettings.cs

namespace TestProject.Settings
{
    /// <summary>
    /// 메인 설정
    /// </summary>
    public class MainSettings
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 사이트 관리자 - SiteAdministrator

        /// <summary>
        /// 사이트 관리자
        /// </summary>
        public string SiteAdministrator { get; set; }

        #endregion
    }
}

 

▶ Models/UserModel.cs

using System.ComponentModel.DataAnnotations;

namespace TestProject.Models
{
    /// <summary>
    /// 사용자 모델
    /// </summary>
    public class UserModel
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region ID - ID

        /// <summary>
        /// ID
        /// </summary>
        public int ID { get; set; }

        #endregion
        #region 사용자 ID - UserID

        /// <summary>
        /// 사용자 ID
        /// </summary>
        [Display(Name = "사용자 ID")]
        [Required(ErrorMessage = "사용자 ID를 입력해 주시기 바랍니다.")]
        [StringLength(50, MinimumLength = 5, ErrorMessage = "사용자 ID는 5자 이상 50자 이하로 입력해 주시기 바랍니다.")]
        public string UserID { get; set; }

        #endregion
        #region 패스워드 - Password

        /// <summary>
        /// 패스워드
        /// </summary>
        [Display(Name = "패스워드")]
        [Required(ErrorMessage = "패스워드를 입력해 주시기 바랍니다.")]
        [StringLength(50, MinimumLength = 4, ErrorMessage = "패스워드는 4자 이상 50자 이하로 입력해 주시기 바랍니다.")]
        public string Password { get; set; }

        #endregion
    }
}

 

▶ Models/IUserRepository.cs

namespace TestProject.Models
{
    /// <summary>
    /// 사용자 저장소
    /// </summary>
    public interface IUserRepository
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 사용자 추가하기 - AddUser(userID, password)

        /// <summary>
        /// 사용자 추가하기
        /// </summary>
        /// <param name="userID">사용자 ID</param>
        /// <param name="password">패스워드</param>
        void AddUser(string userID, string password);

        #endregion
        #region 사용자 수정하기 - UpdateUser(id, userID, password)

        /// <summary>
        /// 사용자 수정하기
        /// </summary>
        /// <param name="id">ID</param>
        /// <param name="userID">사용자 ID</param>
        /// <param name="password">패스워드</param>
        void UpdateUser(int id, string userID, string password);

        #endregion
        #region 사용자 구하기 - GetUser(userID)

        /// <summary>
        /// 사용자 구하기
        /// </summary>
        /// <param name="userID">사용자 ID</param>
        /// <returns>사용자</returns>
        UserModel GetUser(string userID);

        #endregion
        #region 사용자 검증하기 - ValidateUser(userID, password)

        /// <summary>
        /// 사용자 검증하기
        /// </summary>
        /// <param name="userID">사용자 ID</param>
        /// <param name="password">패스워드</param>
        /// <returns>사용자 검증 여부</returns>
        bool ValidateUser(string userID, string password);

        #endregion
    }
}

 

▶ Models/UserRepository.cs

using Microsoft.Extensions.Configuration;
using System.Data;
using System.Data.SqlClient;

namespace TestProject.Models
{
    /// <summary>
    /// 사용자 저장소
    /// </summary>
    public class UserRepository : IUserRepository
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 구성
        /// </summary>
        private IConfiguration configuration;

        /// <summary>
        /// 연결
        /// </summary>
        private SqlConnection connection;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - UserRepository(configuration)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="configuration">구성</param>
        public UserRepository(IConfiguration configuration)
        {
            this.configuration = configuration;

            string connectionString = this.configuration.GetSection("ConnectionStrings").GetSection("DefaultConnection").Value;

            this.connection = new SqlConnection(connectionString);
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 사용자 추가하기 - AddUser(userID, password)

        /// <summary>
        /// 사용자 추가하기
        /// </summary>
        public void AddUser(string userID, string password)
        {
            SqlCommand command = new SqlCommand();

            command.Connection  = this.connection;
            command.CommandType = CommandType.StoredProcedure;
            command.CommandText = "WriteUser";

            command.Parameters.AddWithValue("@UserID"  , userID  );
            command.Parameters.AddWithValue("@Password", password);

            try
            {
                this.connection.Open();

                command.ExecuteNonQuery();
            }
            finally
            {
                this.connection.Close();
            }
        }

        #endregion
        #region 사용자 수정하기 - UpdateUser(id, userID, password)

        /// <summary>
        /// 사용자 수정하기
        /// </summary>
        /// <param name="id">ID</param>
        /// <param name="userID">사용자 ID</param>
        /// <param name="password">패스워드</param>
        public void UpdateUser(int id, string userID, string password)
        {
            SqlCommand command = new SqlCommand();

            command.Connection  = this.connection;
            command.CommandType = CommandType.StoredProcedure;
            command.CommandText = "ModifyUser";

            command.Parameters.AddWithValue("@UserID"  , userID  );
            command.Parameters.AddWithValue("@Password", password);
            command.Parameters.AddWithValue("@UID"     , id      );

            try
            {
                this.connection.Open();

                command.ExecuteNonQuery();
            }
            finally
            {
                this.connection.Close();
            }
        }

        #endregion
        #region 사용자 구하기 - GetUser(userID)

        /// <summary>
        /// 사용자 구하기
        /// </summary>
        /// <param name="userID">사용자 ID</param>
        /// <returns>사용자</returns>
        public UserModel GetUser(string userID)
        {
            SqlCommand command = new SqlCommand();

            command.Connection  = this.connection;
            command.CommandType = CommandType.Text;
            command.CommandText = "SELECT * FROM dbo.[User] WHERE UserID = @UserID";

            command.Parameters.AddWithValue("@UserID", userID);

            UserModel user = new UserModel();

            try
            {
                this.connection.Open();

                IDataReader reader = command.ExecuteReader();

                if(reader.Read())
                {
                    user.ID       = reader.GetInt32(0);
                    user.UserID   = reader.GetString(1);
                    user.Password = reader.GetString(2);
                }
            }
            finally
            {
                this.connection.Close();
            }

            return user;
        }

        #endregion
        #region 사용자 검증하기 - ValidateUser(userID, password)

        /// <summary>
        /// 사용자 검증하기
        /// </summary>
        /// <param name="userID">사용자 ID</param>
        /// <param name="password">패스워드</param>
        /// <returns>사용자 검증 여부</returns>
        public bool ValidateUser(string userID, string password)
        {
            SqlCommand command = new SqlCommand();

            command.Connection  = this.connection;
            command.CommandType = CommandType.Text;
            command.CommandText = "SELECT * FROM dbo.[User] WHERE UserID = @UserID AND Password = @Password";

            command.Parameters.AddWithValue("@UserID"  , userID  );
            command.Parameters.AddWithValue("@Password", password);

            bool result = false;

            try
            {
                this.connection.Open();

                SqlDataReader dataReader = command.ExecuteReader();

                if(dataReader.Read())
                {
                    result = true;
                }
            }
            finally
            {
                this.connection.Close();
            }

            return result;
        }

        #endregion
    }
}

 

▶ Controllers/UserController.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

using TestProject.Models;

namespace DotNetNote.Controllers
{
    /// <summary>
    /// 사용자 컨트롤러
    /// </summary>
    public class UserController : Controller
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 저장소
        /// </summary>
        private IUserRepository repository;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - UserController(repository)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="repository">저장소</param>
        public UserController(IUserRepository repository)
        {
            this.repository = repository;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 인덱스 페이지 처리하기 - Index()

        /// <summary>
        /// 인덱스 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과</returns>
        [Authorize]
        public IActionResult Index()
        {
            return View();
        }

        #endregion
        #region 등록 페이지 처리하기 - Register()

        /// <summary>
        /// 등록 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과</returns>
        [HttpGet]
        public IActionResult Register()
        {
            return View();
        }

        #endregion
        #region 등록 페이지 처리하기 - Register(user)

        /// <summary>
        /// 등록 페이지 처리하기
        /// </summary>
        /// <param name="user">사용자</param>
        /// <returns>액션 결과</returns>
        [HttpPost]
        public IActionResult Register(UserModel user)
        {
            if(ModelState.IsValid)
            {
                if(this.repository.GetUser(user.UserID).UserID != null)
                {
                    ModelState.AddModelError("", "등록된 사용자 입니다.");

                    return View(user);
                }
            }

            if(!ModelState.IsValid)
            {
                ModelState.AddModelError("", "사용자 데이터에 오류가 있습니다.");

                return View(user);
            }
            else
            {
                this.repository.AddUser(user.UserID, user.Password);

                return RedirectToAction("Index");
            }
        }

        #endregion
        #region 로그인 페이지 처리하기 - Login(returnURL)

        /// <summary>
        /// 로그인 페이지 처리하기
        /// </summary>
        /// <param name="returnURL">반환 URL</param>
        /// <returns>액션 결과</returns>
        [HttpGet]
        [AllowAnonymous]
        public IActionResult Login(string returnURL = null)
        {
            ViewData["ReturnURL"] = returnURL;

            return View();
        }

        #endregion
        #region 로그인 페이지 처리하기 - Login(user, returnURL)

        /// <summary>
        /// 로그인 페이지 처리하기
        /// </summary>
        /// <param name="user">사용자</param>
        /// <param name="returnURL">반환 URL</param>
        /// <returns>액션 결과 태스크</returns>
        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> Login(UserModel user, string returnURL = null)
        {
            if(ModelState.IsValid)
            {
                if(this.repository.ValidateUser(user.UserID, user.Password))
                {
                    List<Claim> claimList = new List<Claim>()
                    {
                        new Claim("UserID", user.UserID),
                        new Claim(ClaimTypes.Role, "Users")
                    };

                    ClaimsIdentity claimsIdentity = new ClaimsIdentity(claimList, user.Password);

                    await HttpContext.SignInAsync("CookieAuthentication", new ClaimsPrincipal(claimsIdentity));

                    return LocalRedirect("/User/Index");
                }
            }

            return View(user);
        }

        #endregion
        #region 로그아웃 페이치 처리하기 - Logout()

        /// <summary>
        /// 로그아웃 페이치 처리하기
        /// </summary>
        /// <returns>액션 결과 태스크</returns>
        public async Task<IActionResult> Logout()
        {
            await HttpContext.SignOutAsync("CookieAuthentication");

            return Redirect("/User/Index");
        }

        #endregion
        #region 사용자 정보 페이지 처리하기 - UserInformation()

        /// <summary>
        /// 사용자 정보 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과</returns>
        [Authorize]
        public IActionResult UserInformation()
        {
            return View();
        }

        #endregion
        #region 인사말 페이지 처리하기 - Greetings()

        /// <summary>
        /// 인사말 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과</returns>
        public IActionResult Greetings()
        {
            if(User.Identity.IsAuthenticated == false)
            {
                return new ChallengeResult();
            }

            return View();
        }

        #endregion
        #region 접근 거부 페이지 처리하기 - Forbidden()

        /// <summary>
        /// 접근 거부 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과</returns>
        public IActionResult Forbidden()
        {
            return View();
        }

        #endregion
    }
}

 

▶ Views/Shared/_LoginPartial.cs

@using TestProject.Models
@inject Microsoft.Extensions.Options.IOptions<TestProject.Settings.MainSettings> option
@if(User.Identity.IsAuthenticated)
{
    <form asp-controller="User" asp-action="Logout" method="post" 
        id="logoutForm" class="navbar-right">
        <ul class="nav navbar-nav navbar-right">
            @if(User.IsInRole("Users") && User.FindFirst("UserID").Value == option.Value.SiteAdministrator)
            {
                <li>
                    <a asp-controller="Admin" asp-action="Index"><i class="fa fa-gear"></i> 대시보드</a>
                </li>
                <li>&nbsp;</li>
            }
            <li>
                <a asp-controller="User" asp-action="UserInformation">
                    @User.FindFirst("UserId").Value
                </a>
            </li>
            <li>
                <button type="submit" class="btn btn-link navbar-btn navbar-link">
                    로그아웃
                </button>
            </li>
        </ul>
    </form>
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li><a asp-controller="User" asp-action="Register">회원 가입</a></li>
        <li>&nbsp;</li>
        <li><a asp-controller="User" asp-action="Login">로그인</a></li>
    </ul>
}

 

▶ Views/User/Index.cshtml

<h1>메인 페이지</h1>
@if(User.Identity.IsAuthenticated)
{
    <span>@User.FindFirst("UserID").Value 님, 반갑습니다.</span>
    <a href="/User/Logout">로그아웃</a>
}
else
{
    <a href="/User/Login">로그인</a>
    <a href="/User/Register">회원 가입</a>
}

 

▶ Views/User/Register.cshtml

@model UserModel
@{
    ViewData["Title"] = "회원 가입 페이지";
}
<h2>@ViewData["Title"]</h2>
<div class="row">
    <div class="col-md-8">
        <section>
            <form asp-controller="User" asp-action="Register" method="post" class="form-horizontal">
            <h4>아래 항목을 입력해 주시기 바랍니다.</h4>
            <hr />
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label class="col-sm-2 control-label" asp-for="UserID"></label>
                <div class="col-sm-6">
                    <input type="text" class="form-control" asp-for="UserID" placeholder="사용자 ID">
                    <span asp-validation-for="UserID" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2 control-label" asp-for="Password"></label>
                <div class="col-sm-6">
                    <input type="password" class="form-control" asp-for="Password" placeholder="패스워드">
                    <span asp-validation-for="Password" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-offset-2 col-sm-6">
                    <input type="submit" value="가입" class="btn btn-primary btn-lg" />
                    <a asp-controller="User" asp-action="Index" 
                       class="btn btn-default btn-sm">취소</a>
                </div>
            </div>
            </form>
        </section>
    </div>
    <div class="col-md-4">
    </div>
</div>
@section Scripts {
    @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

 

▶ Views/User/Login.cshtml

@model UserModel
@{
    ViewData["Title"] = "로그인 페이지";
}
<h2>@ViewData["Title"]</h2>
<div class="row">
    <div class="col-md-8">
        <section>
        <form asp-controller="User" asp-action="Login" asp-route-returnurl="@ViewData["ReturnURL"]"
            method="post"
            class="form-horizontal">
        <h4>사용자 ID와 패스워드를 입력해 주시기 바랍니다.</h4>
        <hr />
        <div asp-validation-summary="All" class="text-danger"></div>
        <div class="form-group">
            <label asp-for="UserID" class="col-md-2 control-label"></label>
            <div class="col-md-6">
                <input asp-for="UserID" class="form-control" placeholder="사용자 ID" />
                <span asp-validation-for="UserID" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Password" class="col-md-2 control-label"></label>
            <div class="col-md-6">
                <input type="password" asp-for="Password" class="form-control" placeholder="패스워드" />
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-6">
                <button type="submit" class="btn btn-primary btn-lg">로그인</button>
                <a asp-action="Register" asp-route-returnurl="@ViewData["ReturnURL"]" 
                    class="btn btn-default btn-sm">
                    회원 가입
                </a>
            </div>
        </div>
        </form>
        </section>
    </div>
    <div class="col-md-4">
    </div>
</div>
@section Scripts {
    @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

 

▶ Views/User/Logout.cshtml

<h2>로그아웃 페이지</h2>

 

▶ Views/User/UserInformation.cshtml

<h2>회원 정보 페이지</h2>
<h4>@User.FindFirst("UserID").Value 님의 정보는 아래와 같습니다.</h4>
<hr />

 

▶ Views/User/Greetings.cshtml

<h2>인증된 사용자만 볼 수 있는 페이지</h2>

 

▶ Views/User/Forbidden.cshtml

<h2>접근 권한이 없습니다.</h2>

 

▶ Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using TestProject.Models;
using TestProject.Settings;

namespace TestProject
{
    /// <summary>
    /// 시작
    /// </summary>
    public class Startup
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 구성 - Configuration

        /// <summary>
        /// 구성
        /// </summary>
        public IConfiguration Configuration { get; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - Startup(configuration)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="configuration">구성</param>
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 서비스 컬렉션 구성하기 - ConfigureServices(services)

        /// <summary>
        /// 서비스 컬렉션 구성하기
        /// </summary>
        /// <param name="services">서비스 컬렉션</param>
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IConfiguration>(Configuration);

            services.Configure<MainSettings>(Configuration.GetSection(nameof(MainSettings)));

            services.AddTransient<IUserRepository, UserRepository>();

            services.AddAuthentication("CookieAuthentication")
                .AddCookie
                (
                    "CookieAuthentication",
                    options =>
                    {
                        options.Cookie.Name      = "UserLoginCookie";
                        options.LoginPath        = new PathString("/User/Login");
                        options.AccessDeniedPath = new PathString("/User/Forbidden");
                    }
                );

            services.AddAuthorization
            (
                options =>
                {
                    options.AddPolicy("Users", policy => policy.RequireRole("Users"));

                    options.AddPolicy
                    (
                        "Administrators",
                        policy => policy.RequireRole("Users").RequireClaim
                        (
                            "UserID",
                            Configuration.GetSection(nameof(MainSettings)).GetSection("SiteAdministrator").Value
                        )
                    );
                }
            );

            services.AddControllersWithViews();
        }

        #endregion
        #region 구성하기 - Configure(app, environment)

        /// <summary>
        /// 구성하기
        /// </summary>
        /// <param name="app">애플리케이션 빌더</param>
        /// <param name="environment">웹 호스트 환경</param>
        public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
        {
            if(environment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");

                app.UseHsts();
            }

            app.UseHttpsRedirection();

            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();

            app.UseAuthorization();

            app.UseEndpoints
            (
                endpoints =>
                {
                    endpoints.MapControllerRoute
                    (
                        name    : "default",
                        pattern : "{controller=Home}/{action=Index}/{id?}"
                    );
                }
            );
        }

        #endregion
    }
}

※ Database 폴더에 있는 TestDB.mdf를 SQL Server나 Local DB에 연결해야 한다.

※ Startup 클래스의 ConfigureServices 메소드에서 Administrators 역할을 추가했으나 사용하지는 않았다.

728x90
반응형
그리드형
Posted by 사용자 icodebroker

댓글을 달아 주세요