'2016/11'에 해당되는 글 6건

Programming/.NET

ASP.NET 에서 Expressions 는 지정된 데이터 값을 반환하기 위해 런타임에서 파싱되는 특정 구문에 해당합니다. 이를 테면 SqlDataSource 컨트롤 사용시 ConnectionString 속성에 DB연결정보 제공을 위해서 다음과 같은 형태의 값을 지정하곤 했는데 이것이 바로 Expressions 에 해당합니다.

<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:new_connection %>" SelectCommand="SELECT Top 100 * FROM [Person].[PersonPhone]"></asp:SqlDataSource>

ASP.NET 은 aspx 페이지를 파싱할때 SqlDataSource 컨트롤에서 <%$ %> 내용이 포함된 구문을 발견하면 Web.config 로 부터 데이터베이스 연결정보를 가져옵니다. 이때 Expressions 에 사용된 ConnectionStrings 라는 접두사는 ConnectionStringsExpressionBuilder 클래스를 사용해 해당 Expressions 을 파싱할것을 알려주게 됩니다.

ConnectionStringsExpressionBuilder 클래스는 Expression 을 파싱하는데 사용되는 Expression Builders 중 하나로서 ASP.NET 은 이러한 Expression Builders 를 다수 포함하고 있습니다. 그 중에서 Web.config 의 DB 연결정보값을 가져오기 위한 것중 하나가 바로 ConnectionStringsExpressionBuilder 에 해당합니다.

또한 Expressions 에서 AppSettings 접두사를 사용하면 이것은 Web.config 파일의 appSettings 섹션 에서 지정한 값을 가져와야 한다는 것을 의미하는 것으로 AppSettingsExpressionBuilder 클래스를 사용해 파싱을 시도합니다.

<appSettings>
 <add key="test" value="테스트" />
</appSettings>

▶Web.config 의 appSettings 설정

<asp:Label ID="Label1" runat="server" Text="<%$ AppSettings:test %>"></asp:Label>

▶ <코드 1-1>

<코드 1-1>은 Web.config 의 appSettings 에서 지정한 Key 에 해당하는 값을 가져옵니다.

Resources 접두사는 웹프로젝트의 로컬리소스를 가져오며 ResourceExpressionBuilder 클래스를 사용해 파싱됩니다.

<asp:Label ID="Label1" runat="server"Text="<%$ Resource: LocalResources.test %>"></asp:Label>

뿐만 아니라 ASP.NET 은 위에서 처럼 정해진 Expressions 이외에 System.Web.Compilation.ExpressionB
uilder 클래스를 기반으로 개발자 자신만의 Expression 을 직접 구현하여 사용할 수도 있습니다.

[ExpressionPrefix("mEx")][ExpressionEditor("myExpressionEditor")]
public class myExpression : System.Web.Compilation.ExpressionBuilder
{
    public override System.CodeDom.CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
    {
        return new CodeCastExpression("String", new CodePrimitiveExpression("Hello"));
    }
}

▶ <코드 1-2>

<코드 1-2>는 ExpressionBuilder 클래스에서 파생된 myExpression 클래스를 정의한 것으로서 새로운 ExpressionBuilder 클래스를 생성한 것입니다. 클래스에서 지정된 ExpressionPrefix 와 ExpressionEditor 속성은 .NET 으로 하여금 해당 클래스가 Expression 에 사용되는 클래스임을 알려주기 위한 것으로 실제 Expression 이 파싱될때 그에 맞는 적절한 ExpressionBuilder 클래스를 찾아낼 수 있도록 합니다.

myExpression 클래스안에서 오버라이드(재정의)된 GetCodeExpression 메소드는 네임스페이스 상으로 CodeDom 하위에 속합니다. CodeDom 은 ASP.NET 이 런타임시에 코드를 동적으로 생성하고 생성된 코드를 실행한 결과를 반환하는데 사용되는 기반클래스입니다. Expression 에서 CodeDom 이 언급되는 이유는 직접 구현한 ExpressionBuilder 클래스 자체가 코드가 되어 동작하는 것이 아니라 런타임에 .NET 으로 하여금 어떠한 코드를 생성하고 실행해야 하는지를 알려주는 이정표같은 역활을 하기 때문입니다. GetCodeExpression 안에서 작성된 CodeCastExpression 메소드는 형변환을 수행하기 위한 코드를 생성해야 함을 나타내며 실제 런타임에서 C#의 경우 ((Int64)(1000)) 과 같은 코드를 생성해 내고 동작을 수행하게 되는 것입니다.

GetCodeExpression 메소드는 대략 3가지 정도의 파라메터를 가지고 있는데 이 중 두개의 파라메터에 주목해 주세요. 첫번째 파라메터인 entry 는 Expression 이 사용될때 어느 속성에 연결되어 있는지를 알아내기 위한 것입니다. 예를 들면 <코드 1-1>에서 Label 의 Text 속성에 Expression 이 사용되었는데 이때 entry 는 Label 컨트롤및 여기에 바인딩된 Text 속성에 관한 정보를 가지게 됩니다.

두번째 파라메터인 parseData 는 ParseExpression 메소드에 의해 파싱되는 데이터를 포함하고 있습니다. 사실 <코드 1-2>와 같이 ExpressionBuilder 를 생성하고 다음과 같이 Expression 을 바인딩했다고 했을때

<asp:Label ID="Label1" runat="server" Text="<%$ mEx:hi %>"></asp:Label>

이때 mEx 접두사 다음에 사용된 hi라는 데이터값을 읽고 저장하는 파라메터가 바로 parseData 에 해당합니다.

다만 실제로 동작결과를 살펴보면 parseData 에는 아무런 값도 없고(null) 그저 Hello 이라는 값만이 나오게 됩니다. parseData 파라메터를 제대로 활용하려면 우선 값을 파싱하기 위한 ParseExpression 메소드를 오버라이드해야 하며 parseData 에 따른 값을 반환시키려면 <코드 1-2>에서 Hello 이라는 값 대신 parseData 로 바꿔줘야 합니다.

[ExpressionPrefix("mEx")][ExpressionEditor("myExpressionEditor")]
public class myExpression : System.Web.Compilation.ExpressionBuilder
{
    public override System.CodeDom.CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
    {
        return new CodeCastExpression("String", new CodePrimitiveExpression(parsedData));
    }

    public override object ParseExpression(string expression, Type propertyType, ExpressionBuilderContext context)
    {
        return expression;
    }
}

만약 aspx 페이지의 Page 지시에서 CompilationMode 속성이 Naver 등으로 설정되어 페이지가 no-compile 모드로 동작하는 경우에는 위 방법 대신에 SupportsEvaluate 속성과 EvaluateExpression 메소드를 오버라이드해서 Expression 의 값을 가져와야 합니다.

[ExpressionPrefix("mEx")][ExpressionEditor("myExpressionEditor")]
public class myExpression : System.Web.Compilation.ExpressionBuilder
{
    public override System.CodeDom.CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
    {
        return new CodeCastExpression("String", new CodePrimitiveExpression(parsedData));
    }

    public override object ParseExpression(string expression, Type propertyType, ExpressionBuilderContext context)
    {
        return expression;
    }

    public override bool SupportsEvaluate
    {
        get { return true; }
    }

    public override object EvaluateExpression(object target, BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
    {
        return parsedData;
    }
}

SupportsEvaluate 속성을 통해 Evaluate 사용을 허가하고 EvaluateExpression 메소드를 재정의하면 이 메소드가 페이지가 no-compile 모드일때 GetCodeExpression 메소드대신 Expression 값을 가져오는데 사용됩니다.

마지막으로 ExpressionBuilder 를 생성하고 나면 web.config 파일의 expressionBuilders 노드에 해당 클래스의 존재와 접두사를 추가함으로서 ASP.NET 이 런타임에 Builder 클래스를 찾을 수 있도록 해야 합니다.

<system.web>
  <compilation debug="true" targetFramework="4.0">
    <expressionBuilders>
      <add expressionPrefix="mEx" type="Webtest.App_Code.myExpression" />
    </expressionBuilders>
  </compilation>
</system.web>

0 0
Develop

WebUITester는 반복적으로 진행되는 웹입력단 테스트를 자동화하는 UI입력 테스트 프로그램입니다. 여기 한가지 사례를 들어 WebUITester를 활용할 수 있는 방안을 알아보고자 합니다.

 

 

이것은 한 웹사이트의 회원가입양식입니다. 실제 이런모양의 회원가입양식이 존재하지는 않지만 테스트를 위해 임의로 HTML을 코딩해서 만든것이니 그렇구나 하고 넘어가도록 합시다.

 

개발자는 위 양식을 기본으로 입력처리가 잘 되는지 테스트를 진행해야 합니다. 테스트는 프로그램에  다양한 입력형태(값의 변화에 따른)로 필요한 내용를 입력하여 테스트를 진행해야 하며 혹시라도 입력양식이 변경되면 변경이후의 테스트도 수행해야 합니다.

 

문제는 그때마다 개발자는 각각의 입력칸에 내용를 입력하고 '확인'버튼을 눌러 입력한 데이터가 목적한바 대로 잘 처리가 되는지 확인하는 절차를 반복해야 한다는 것입니다.

 

이제 WebUITester를 활용하여 위 입력양식에 대응하는 테스트를 진행해 보고자 합니다.

 

 

 

프로그램을 실행하고 Identity에 입력요소에 대한 식별자와 Value에 해당 입력요소에 들어갈 값을 입력합니다. 이때 해당 Identity가 ID로 구분되는지 NAME으로 구분되는지를 지정해야 합니다.

 

 

입력이 완료되면 'Add'버튼을 눌러 항목을 추가합니다. 참고로 예제로 제시된 HTML입력폼에서 이름요소의 ID는 txt_name이며 생년월일은 txt_birthday입니다.

 

 

 

위 이미지는 이름과 생년월일까지의 입력상황을 처리한 '예'입니다. 같은 방법으로 테스트를 진행할 모든 UI요소에 대한 입력을 완료한뒤 URL에 테스트를 진행할 페이지주소를 입력합니다.

 

 

참고로 입력된 요소중 rdo_sex_m은 '남자'로 표시된 요소이며 input type이 radio입니다. radio와 checkbox요소는 입력값이 0이면 False로 판단하고 checked속성을 제거하며 0이외에 모든 값은 True로 간주하고 checked속성을 추가한 후 선택상태로 두게 됩니다.

 

input type이 text, hidden, password 인 요소와 textarea 요소는 Value로 지정된 값을 그대로 넣지만 input type이 button, reset, submit, image 와 같은 버튼형태의 요소는 Value값을 무시하고 무조건 클릭이벤트를 유발합니다.

 

예제에서의 지역번호, 휴대 전화번호 앞자리, 휴직/재직 처리와 같은 select요소는 Value에 지정된 값에 해당하는 value항목을 선택하도록 하며 HTML button요소는 input type의 button과 동일한 처리를 수행합니다.

 

예제를 테스트해볼 준비가 끝났습니다. 이렇게 입력이 완료되면 'Go'버튼을 눌러줍니다.

 

프로그램은 테스트가 진행되면 각 요소의 Status에 상태에 대한 변화가 생길것입니다. 모든 테스트가 완료될때까지 기다려 주시면 해당 페이지를 브라우저로 띄우고 입력이 완료된 상태를 표시할 것입니다.

 

 

입력완료된 화면을 보니 전화번호에는 값이 들어가지 않았습니다.

 

 

상태값(Status)에도 전화번호 요소가 Failed로 표시되었습니다. 원인을 파악해보니 Identity입력값이 잘못들어 갔습니다. 각각의 요소는 ID가 ddl_로 시작하는게 아니라 txt_로 시작합니다.

 

따라서 해당 요소의 ID를 수정해줘야 하는데 이때는 바꾸고자 하는 Cell에 커서를 위치시키고 바꾸고자 하는 내용을 그대로 입력하면 됩니다.

 

 

내용을 변경했으므로 이제 'Go'를 다시 눌러 테스트를 시작합니다.

 

 

모든 입력이 완료되었습니다.

 

 

 

더불어 WebUITester에도 모든 요소가 Success처리되었습니다. 이처럼 UI입력에 필요한 모든 요소를 Web UI Tester에 등록하고 테스트를 진행하면 됩니다.

 

만약 입력처리와 동시에 '확인'등과 같은 button입력처리도 같이 수행하려면 해당 요소를 추가해주기만 하면 됩니다.

 

 

만약 특정 요소를 테스트에서 제외하려면 해당 요소에 있는 Remove 버튼을 눌러 해당요소항목을 삭제하시면 되며 일시적 제외라면 'Remove'대신에 'Skip'버튼을 활용할 수 있습니다. 참고로 Web UI Tster는 위에서 아래순서로 테스트를 진행하는데 이 순서를 바꾸시려면 특정 요소를 리스트에서 선택하신뒤 원하는 위치로 끌어다 놓으시면 됩니다.

 

마지막으로 설정내용과 등록된 요소는 'Save'버튼을 눌러 파일형태로 저장이 가능하며 필요할때 'Load'버튼을 통하여 언제든지 불러올 수 있습니다.

 

WebUITester.exe

 

Web UI Tester는 프로그램 실해에 .NET Framework 4.6 이 필요합니다.

'Develop' 카테고리의 다른 글

수학 수식입력기 (Math Editor)  (0) 2017.06.09
웹 UI 테스트 - WebUITester  (0) 2016.11.22
test, UI, web
0 0
Programming/.NET

ASP.NET의 Membership API를 이용하면 현재 접속자수를 손쉽게 파악할 수 있습니다.

 

Response.Write(string.Format("현재 접속자 {0} 명", Membership.GetNumberOfUsersOnline().ToString())); .

 

만약 현재가 아닌 최근 몇분 동안의 접속자를 파악하려면 web.config에서 다음과 같이 membership 요소의 userIsOnlineTimeWindow 속성에 원하는 시간을 설정하면 됩니다.

 

<membership userIsOnlineTimeWindow="5"></membership>

 

이 설정은 최근 5분 동안의 로그인 사용자를 표시할 것입니다.

 

Membership API이므로 웹사이트의 프로젝트 회원관리자체가 Membership API를 통해 처리되는 방식이어야 합니다.

0 0
Programming/.NET

System.IO.File은 파일 입출력 전용의 FileStream클래습니다. 이 클래스는 파일 처리를 위한 여러 정적메소드를 제공하는데 이들 메소드를 활용하면 간단한 파일처리작업을 손쉽게 구현할 수 있습니다.

 

우선 파일을 여는 메소드부터 살펴보겠습니다.

 

using (System.IO.FileStream fs = System.IO.File.Open(@"C:\text.txt", System.IO.FileMode.Open)) {
    
}

 

파일을 열어 FileStream형식으로 반환합니다. 이 후 FileStream과 동일하게 취급할 수 있습니다.

 

using (System.IO.StreamReader ss = System.IO.File.OpenText(@"C:\text.txt")) {
    
}

 

파일을 열어 StreamReader형식으로 반환합니다. 이 후 StreamReader형식과 동일하게 취급할 수 있습니다.

 

using (System.IO.FileStream fs = System.IO.File.OpenRead(@"C:\text.txt")) {
    
}

 

파일을 읽기전용으로 오픈합니다.

 

using (System.IO.FileStream fs = System.IO.File.OpenWrite(@"C:\text.txt")) {
    
}

 

파일을 쓰기전용으로 오픈합니다.

 

string sdata = System.IO.File.ReadAllText(@"C:\text.txt");

파일 전체를 읽고 문자열로 반환합니다.

 

System.IO.File.AppendAllText(@"C:\text.txt", "abc");

파일에 특정 내용을 추가합니다.

 

System.IO.File.WriteAllText(@"C:\text.txt", "abc");

 

파일에 특정 내용을 작성합니다. 신규로 파일을 만들며 존재하는 파일이면 덮어 씁니다.

 

다음 내용은 좀 재미있는데 LINQ를 통해 파일의 특정 라인에 접근하는 방법을 보여주고 있습니다.

 

var sdata = from line in System.IO.File.ReadLines(@"C:\test.txt")
            where line.Contains("tmp")
            select line;


이 방법은 ReadAllLine으로 파일의 모든 행을 읽고 그 결과를 배열로 순회하면서 찾는것과 같은 결과를 보여주지만 성능은 상당한 차이가 있습니다.

 

참고로 File 클래스의 ReadLines메소드는 .NET Framework 4.5이상에만 존재합니다.

0 0
OS/Windows

Windows 10 (1607) 최신 업데이트버전 부터는 우분투의 bash쉘을 지원하기 시작했습니다. 그런데 현재 배포판기준이 10.04 버전입니다. 이걸 최신판으로 업데이트하는 방법을 알아보고자 합니다.

 

우선 아래 명령으로 sources.list파일을 열어봅니다.

 

vim /etc/apt/sources.list

 

그러면 대략 아래와 같거나 비슷한 내용이 나올것입니다.

 

 

이 파일의 내용을 다음과 같이 수정합니다.

 

deb http://kr.archive.ubuntu.com/ubuntu yakkety main restricted universe multiverse
deb http://kr.archive.ubuntu.com/ubuntu yakkety-updates main restricted universe multiverse
deb http://kr.security.archive.ubuntu.com/ubuntu yakkety-security main restricted universe multiverse

 

우산 기존 저장소의 주소를 대한민국미러로 바꾸고 trusty를 yakkety로 바꾸었습니다. trusty는 14.04버전의 코드네임이고 yakkety는 16.10버전의 코드네임입니다. 버전별 코드네임을 확인하려면 아래 주소를 참고하시면 됩니다.

 

https://wiki.ubuntu.com/DevelopmentCodeNames

 

해당 내용으로 변경 후 파일을 저장하고 아래 명령을 내려줍니다.

 

apt update

apt full-upgrade

 

 

 

업데이트 설치가 완료되었습니다.

 

다만 gcc나 다른 내장유틸은 업데이트되는대로 동작하기는 하지만 쉘자체는 그렇지 않은듯 합니다.

 

참고로 삭제는 cmd에서 아래 명령으로 처리할 수 있습니다.

 

lxrun /uninstall /full

 

확인해 보니 프로그램 추가/제거 에서 삭제가 가능하기는 한데 완전히 삭제되지는 않았습니다. 차라리 위 명령으로 삭제하는것이 나을것입니다.

'OS > Windows' 카테고리의 다른 글

[Ubuntu bash] prompt 색상 변경  (0) 2017.02.14
[Ubuntu bash] 최신배포판 업데이트및 완전삭제  (0) 2016.11.09
0 0
Programming/.NET

Box라는 제품에 대한 개체를 List로 반환해주는 다음 클래스가 있습니다.

 

public class Box

{

    public string Name
    {
        get;
        set;
    }

    public int Size
    {
        get;
        set;
    }

    public int Price

    {

        get;
        set;
    }
}

public class Boxes

    public List<Box> B
    {
        get;
        set;
    }
}

 

Boxes 클래스는 외부에서 공급받은 클래스가 소스가 없다고 가정해 보겠습니다. 그런데 이 클래스에 현재 존재하는 각 제품의 가격합계를 모두 구하는 메서드를 작성해야 한다면 이때 확장 메서드를 유용하게 사용할 수 있습니다.

 

public static class MyBoxes
{
    public static int TotalPrice(this Boxes mybox)
    {
        int total_price = 0;

        foreach (Box b in mybox.B)
            total_price += b.Price;

        return total_price;
    }
}

 

위 클래스에서 this로 시작하는 매개변수에 주목해 주세요. this는 이 메서드가 확장메서드로 사용될 것임을 알려주고 있으며 Boxes라는 클래스에 확장메서드가 적용될것임을 알 수 있습니다. 또한 매개변수로 전달되는 Boxes개체를 사용하면 각 Box제품의 속성에도 접근할 수 있습니다.

 

Boxes bx = new Boxes { B = new List<Box> { new Box { Name = "Big", Size = 100, Price = 1500 },

 new Box { Name = "Normal", Size = 50, Price = 1000 },

 new Box { Name = "small", Size = 50, Price = 500 } } };

int p = bx.TotalPrice();

 

이처럼 확장메서드를 사용하면 본래 클래스에 있었던 메서드처럼 추가적인 메서드를 자연스럽게 구현할 수 있습니다. 이를 응용하면 확장메서드를 클래스가 아닌 인터페이스에 적용하여 같은 인터페이스를 구현하는 모든 개체에 동일한 확장메서드를 구현할 수도 있습니다.

 

public class Boxes : IEnumerable<Box>

    public List<Box> B
    {
        get;
        set;
    }

    public IEnumerator<Box> GetEnumerator()
    {
        return B.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

 

Boxes 클래스가 Ienumerable 인터페이스를 구현하도록 하였습니다.

 

public static class MyBoxes
{
    public static int TotalPrice(this IEnumerable<Box> mybox)
    {
        int total_price = 0;

        foreach (Box b in mybox)
            total_price += b.Price;

        return total_price;
    }       
}

 

위와 같이 클래스를 인터페이스로 바꿔주기만 하면 해당 인터페이스(IEnumerable)에 메서드가 구현됩니다. 예제에서 mybox는 기존의 .B가 제거되었는데 이는 IEnumerable인터페이스에 의해 자체적으로 배열을 순회할 수 있는 기능을 가지게 되었기 때문입니다.

 

IEnumerable<Box> bx = new Boxes { B = new List<Box> { 

new Box { Name = "Big", Size = 100, Price = 1500 }, 

new Box { Name = "Normal", Size = 50, Price = 1000 }, 

new Box { Name = "small", Size = 50, Price = 500 } } };
int p = bx.TotalPrice();

Box[] bb = { new Box { Name = "Big", Size = 100, Price = 1500 }, 

new Box { Name = "Normal", Size = 50, Price = 1000 }, 

new Box { Name = "small", Size = 50, Price = 500 } };
int pp = bb.TotalPrice();

 

Box 개체에 대한 배열에도 확장메서드를 구현하고 있다는것에 주목해 주세요. IEnumerable 인터페이스 자체에 확장메서드를 구현했으므로 이 IEnumerable 인터페이스를 구현하고 있는 배열(Box[] bb)까지도 ToalPrice라는 메서드를 호출할 수 있게 되었다는걸 알 수 있습니다.

 

또한 기존에는 컬렉션처럼 Add메서드를 추가해 초기화를 수행하려면 반드시 클래스가 ICollection<T> 인터페이스를 상속받아야 했는데 C# 6.0부터는 Add메서드가 호출되면 확장메서드에서 Add메서드를 찾는 기능이 추가되어 굳이 ICollection<T> 인터페이스를 상속할 필요가 없어졌습니다.

 

public static class BoxesExtention
{
    public static void Add(this Boxes instanceint num)
    {
        //
    }
}

 

Boxes b = new Boxes() { 1, 2, 3 };

이것만 봐도 확장메서드는 상당히 높은 확장성을 제공하고 있는데 위에서 처럼 IEnumerable 인터페이스에 확장메서드를 적용하여 이번에는 배열안의 특정요소를 필터링하는 메서드를 구현해 보도록 하겠습니다.

 

아래는 말씀드린 기능 구현을 위해 MyBoxes클래스에 추가한 확장메서드입니다.

 

public static IEnumerable<Box> Filter(this IEnumerable<Box> mybox, int selectBoxbySize)
{
    foreach (Box b in mybox) {
        if (b.Size == selectBoxbySize)
            yield return b;
    }
}

 

이 확장메서드는 Box의 Size값을 받는 변수를 매개변수로 받고 있는데 배열을 순회하면서 지정한 Size와 일치하는 Box개체를 IEnumerable형식으로 반환하도록 하고 있습니다.

 

IEnumerable<Box> result = bb.Filter(50);

 

참고로 위와 같은 필터링방식을 좀더 효휼적이고 유연하게 바꾸고자 한다면 람다식을 응용해 다음과 같이 구현할 수 있습니다.

 

public static IEnumerable<Box> Filter(this IEnumerable<Box> mybox, Func<Boxbool> selectBox)
{
    foreach (Box b in mybox) {
        if (selectBox(b))
            yield return b;
    }
}

 

우선 확장메서드를 Func형식으로 바꾼 후

 

IEnumerable<Box> result = bb.Filter(b => b.Size == 50);

IEnumerable<Box> result = bb.Filter(b => b.Size == 50 || b.Price > 500);

 

이와 같이 람다식을 적용하면 특정 필터링조건에 제한되지 않고 자유롭게 필터형식을 지정할 수 있습니다.

0 0
1
블로그 이미지

클리엘