http://msdn.microsoft.com/ko-kr/magazine/cc163511.aspx

Data Points

Smartphone에서의 RSS 피드

John Papa

코드 다운로드 위치: DataPoints2006_12.exe (172 KB)
:Track('ctl00_ContentPlaceHolder1_ctl00|ctl00_ContentPlaceHolder1_ctl01',this);" href="http://msdn.microsoft.com/ko-kr/magazine/cc164533.aspx">Browse the Code Online

목차

모바일 기능 개발 준비 작업
뉴스 읽기 응용 프로그램
비동기 피드 요청
DataSet 로드
피드 목록 로드
익명 대리자를 사용한 정렬
WebBrowser 컨트롤을 사용하여 표시
Smartphone에 배포
결론

모바일 응용 프로그램은 다른 응용 프로그램과 거의 비슷합니다. 데이터를 사용하고, 연결을 통해 데이터를 전달하며, 데이터 관리가 필요합니다. 모바일 응용 프로그램이 정교해지면서 이를 개발하기 위한 도구 역시 정교해지고 있습니다. 사실 Microsoft® .NET Compact Framework 2.0으로 데이터 중심 모바일 응용 프로그램을 개발할 때 사용하는 기술은 Windows® Forms 응용 프로그램 개발에 사용되는 기술과 동일한 부분이 많습니다. 개발자는 .NET Compact Framework를 통해 ADO.NET 및 기타 익숙한 라이브러리를 사용하여 모바일 장치의 데이터를 관리할 수 있습니다.

그러나 Smartphone용 응용 프로그램을 개발할 때는 작은 화면 크기를 감안하는 등 새로운 수준의 디자인을 고려해야 합니다. 이러한 응용 프로그램은 데이터 송수신 기능 덕분에 웹 서버와 통신하는 응용 프로그램을 호스팅하는 경우가 많으므로 기본 제공되는 인터넷 연결도 응용 프로그램 디자인에 영향을 줍니다.

이번 달에는 RSS 피드를 읽고 이를 ADO.NET DataSet으로 로드하는 Windows Mobile® 5.0 응용 프로그램을 작성하는 방법을 설명하겠습니다. 먼저 URL에서 RSS 피드를 비동기적으로 요청하는 방법을 설명한 다음 RSS 피드를 읽는 방법, 그리고 이를 DataSet으로 로드하는 방법을 살펴보겠습니다. 다음으로 DataSet의 계층 구조를 탐색하여 각 피드 항목을 포함하는 적절한 데이터를 찾는 방법을 알아보고 익명 대리자를 사용하여 데이터를 정렬하는 방법, ADO.NET을 사용하여 RSS 데이터에 필드가 있는지 확인하는 방법, 그리고 WebBrowser 컨트롤을 사용하여 URL을 탐색하거나 HTML을 표시하는 방법을 설명합니다. 마지막으로 CAB 프로젝트를 사용하여 응용 프로그램을 모바일 장치로 배포하는 방법을 알아보는 것으로 이번 달 칼럼을 마치겠습니다. 이 샘플 응용 프로그램의 전체 소스 코드는 MSDN®Magazine 웹 사이트에서 제공됩니다.

모바일 기능 개발 준비 작업

.NET Compact Framework 2.0과 Visual Studio® 2005를 사용하여 모바일 응용 프로그램을 개발하는 작업은 ASP.NET 또는 Windows Forms 응용 프로그램을 개발하는 작업과 많은 부분 유사합니다. 그러나 코드 작성을 시작하기 전에 몇 가지 준비해야 할 것이 있습니다. Visual Studio 2005는 물론 다음과 같은 항목이 설치되어 있어야 합니다. 필요한 경우 Microsoft 웹 사이트에서 다운로드하십시오.

개발 과정에서 실제 Smartphone 장치 대신 에뮬레이터를 사용할 계획이라도 ActiveSync® 최신 버전을 설치해야 합니다. ActiveSync®는 응용 프로그램을 에뮬레이터에 배포하는 작업, 에뮬레이터를 인터넷에 연결하는 작업 등에 사용됩니다. Windows Mobile SDK 5.0 for Smartphone은 Visual Studio 2005에 Smartphone 개발 전용의 몇 가지 프로젝트 유형을 추가합니다(Pocket PC 개발용 SDK도 있음). 마지막으로, 대상 장치와 가장 비슷한 에뮬레이터를 설치해야 합니다. 필자의 응용 프로그램은 320×240 가로 화면을 가진 Smartphone 장치를 대상으로 합니다.

에뮬레이터에 대해 한마디 덧붙이자면, 사용 중인 모바일 장치가 있는 경우에는 전적으로 에뮬레이터에 의존하기보다는 해당 장치에서 응용 프로그램을 테스트하는 것이 더 좋습니다. 에뮬레이터는 훌륭한 도구이지만 최선의 테스트 방법은 실제 장치에서 응용 프로그램을 실행하는 것입니다. 필자는 모바일 응용 프로그램을 개발할 때 에뮬레이터를 사용하지만 특정 검사점마다 실제 장치에 응용 프로그램을 배포하여 두 환경 모두에서 아무 문제 없이 실행되는지 확인합니다.

코드를 실행하기 전에 Visual Studio의 도구 메뉴에서 장치 에뮬레이터 관리자를 시작해야 합니다. 그러면 사용 가능한 에뮬레이터 목록이 표시됩니다. 320×240 (Landscape) Windows Mobile 5.0 Smartphone Emulator를 선택합니다. 이 옵션을 다운로드하지 않은 경우 다른 Windows Mobile 5.0 Smartphone 에뮬레이터를 대신 사용할 수 있습니다. 이 칼럼의 코드는 320×240 가로 화면을 기준으로 작성되었지만 다른 해상도에 맞게 손쉽게 수정할 수 있습니다.

선택한 에뮬레이터를 마우스 오른쪽 단추로 클릭하고 연결을 선택합니다. 그러면 에뮬레이터에서 Smartphone 부팅이 시작됩니다. Smartphone이 에뮬레이터에서 실행되면 다시 장치 관리자에서 해당 항목을 마우스 오른쪽 단추로 클릭하고 크레들에 놓기를 선택합니다. 에뮬레이터의 Smartphone을 크레들에 놓으면 Smartphone이 ActiveSync와 통신하여 네트워크에 연결하고 이를 통해 인터넷에 액세스하여 RSS 피드를 받을 수 있습니다. 마지막으로 Visual Studio의 장치 도구 모음에 있는 드롭다운 목록에서 320×240 에뮬레이터를 선택합니다. 이렇게 하면 Visual Studio에서 디버그 및 배포 시에 사용할 에뮬레이터 이미지가 지정됩니다.

뉴스 읽기 응용 프로그램

코드를 살펴보기 전에 응용 프로그램이 어떤 작업을 수행하는지 전체적으로 살펴보겠습니다. 응용 프로그램이 실행되면 각 해당 웹 사이트에 비동기적으로 RSS 피드를 요청하고 ListView 컨트롤을 로드합니다. ListView 컨트롤은 각 피드의 제목 및 각 피드가 반환하는 항목의 수를 제공합니다(그림 1 참조).

그림 1 RSS 피드 목록

그림 2 피드 항목 목록

그림 3 항목 설명

피드를 선택한 다음 Smartphone 중앙에 있는 Enter 소프트 키를 클릭하거나 Menu | Select를 선택하면 선택한 피드의 항목 목록이 다른 양식의 목록 보기에 최신 항목이 가장 위에 오도록 정렬되어 표시됩니다. RSS 피드에 항목의 게시 날짜(RSS 스키마의 선택적인 요소)가 포함되어 있는 경우 목록 보기의 두 번째 열에 표시됩니다(그림 2 참조). 게시 날짜가 없으면 두 번째 열은 생략되고 항목의 제목만 표시됩니다.

항목을 선택하면 다른 화면에 해당 항목에 대한 설명이 표시됩니다. 항목의 설명 요소에 있는 내용은 WebBrowser 컨트롤에 표시됩니다(그림 3 참조).

항목 설명은 RSS 피드에서 설명 요소를 읽으므로 하이퍼링크나 이미지를 포함할 수도 있습니다. URL이 포함된 경우 URL을 클릭하면 해당 주소로 이동할 수 있습니다. 항목 설명 화면에서 소프트 키로 Select 메뉴 항목을 클릭하면 화면이 전환되고 선택한 항목의 URL로 이동하며 해당 게시물 전체를 볼 수 있도록 WebBrowser 컨트롤에 내용이 로드됩니다. 사용자는 언제든지 Back 소프트 키를 클릭하여 이전 화면으로 돌아갈 수 있습니다. Feed Item List 화면에서 Left 소프트 키를 사용하여 메뉴에서 Done을 선택하면 응용 프로그램을 종료할 수 있습니다.

이 응용 프로그램은 이러한 종류의 응용 프로그램에 필요한 기본적인 기능을 보여 줍니다. 응용 프로그램을 더욱 정교하게 만들려면 진행률 표시줄, 아이콘, 피드 목록 관리 기능과 같은 추가 기능을 포함할 수 있습니다. 코드를 다운로드하여 자신만의 기능을 추가해 보십시오.

비동기 피드 요청

지금까지 응용 프로그램의 동작 방식은 살펴보았습니다. 이제부터는 코드를 세부적으로 분석해 보겠습니다. 응용 프로그램이 로드되면 먼저 BeginScanFeeds 메서드를 호출하여 등록된 URL을 반복하여 각 URL에 대한 피드를 요청하는 방식으로 RSS 피드 3개를 요청합니다. 코드를 간단히 하기 위해 여기에서는 이러한 URL을 List<string>에 하드코딩했습니다.

코드 복사

   List<string> urlList = new List<string>();
   urlList.Add(
     "http://msdn.microsoft.com/msdnmag/rss/        rss.aspx?Sub=Data+Points");
   urlList.Add("http://blogs.msdn.com/MainFeed.aspx");
   urlList.Add("http://blogs.technet.com/MainFeed.aspx");

   ClearList();
   foreach (string url in urlList) BeginScanFeeds(url);
 

BeginScanFeeds 메서드는 지정한 URL에 대한 HttpWebRequest 클래스 인스턴스를 생성합니다. 그러면 HttpWebRequest의 BeginGetResponse 메서드를 통해 지정된 URL에 대한 요청이 비동기적으로 생성됩니다. 이 메서드는 비동기 콜백 대리자(이 경우 EndScanFeeds 메서드) 및 요청을 받습니다.

코드 복사

   public void BeginScanFeeds(string url)
   {
       HttpWebRequest request = 
          (HttpWebRequest)WebRequest.Create(url);
       IAsyncResult asyncResult = request.BeginGetResponse(
           new AsyncCallback(EndScanFeeds), request);
   }
   

각 요청이 전송되며 응답이 반환되면 EndScanFeeds 메서드가 호출됩니다. EndScanFeeds 메서드는 IAsyncResult 형식의 단일 인수를 가지며 피드를 검색하는 데 이 인수를 사용할 수 있습니다. 그림 4는 StreamReader 개체를 사용하여 결과 HttpWebResponse 개체에서 feedData라는 문자열로 응답을 보내는 이 메서드 코드의 일부를 보여 줍니다. feedData 문자열은 RSS 피드의 XML을 포함하며 LoadFeedList 메서드로 전달됩니다. 이 메서드는 피드 데이터를 ListView 컨트롤에 로드합니다. 피드 읽기가 비동기적으로 수행되고 LoadFeedList 메서드는 ListView 컨트롤에 액세스하므로 이 메서드는 Control.Invoke 메서드를 사용하여 호출해야 합니다.

Figure 4 피드 읽기

코드 복사

public void EndScanFeeds(IAsyncResult result)
{
    string feedData;
    HttpWebRequest request = (HttpWebRequest)result.AsyncState;
    using(HttpWebResponse response = 
        (HttpWebResponse)request.EndGetResponse(result))
    using(StreamReader streamReader = 
        new StreamReader(response.GetResponseStream()))
    {
        feedData = streamReader.ReadToEnd();
    }

    Invoke(new LoadFeedListDelegate(LoadFeedList), feedData);
}

DataSet 로드

피드를 가져온 다음에는 해당 피드의 내용을 구문 분석해야 합니다. XML 문자열을 읽는 방법에는 여러 가지가 있지만 여기에서는 다음 코드를 사용하여 DataSet으로 데이터를 로드하는 방법을 사용했습니다.

코드 복사

   DataSet ds = new DataSet();
   XmlTextReader xmlRdr = 
      new XmlTextReader(new StringReader(rssXmlData));
   ds.ReadXml(xmlRdr);

이 방법을 선택한 이유는 열의 존재 여부를 확인하고 RSS 피드의 계층 구조를 탐색하는 기능을 사용할 수 있기 때문입니다.

피드 내용을 StringReader 개체로 로드한 다음 이 개체를 사용하여 XmlTextReader를 만듭니다. 그런 다음 피드의 내용을 DataSet으로 로드하는 DataSet의 ReadXml 메서드로 XmlTextReader를 읽습니다. RSS 스키마의 계층 구조에는 RSS, 채널, 항목의 세 가지 주 요소가 있습니다. 일부 스키마의 경우 이미지 요소와 같은 추가 요소를 포함하기도 합니다.

RSS 요소는 하나 이상의 채널 요소를 포함하는 RSS 피드의 루트입니다. 채널 요소는 피드를 나타내며, 제목, 링크, 설명, 항목 등 피드를 설명하는 일련의 자식 요소를 포함할 수 있습니다. 항목 요소는 채널 요소의 필수 자식 요소입니다(이 항목은 주 항목 요소와는 별개임). 항목 요소는 피드의 각 항목을 나타냅니다. 예를 들어 MSDN Blog에서 채널은 MSDN Blog를, 항목은 게시물을 나타냅니다.

RSS 피드가 DataSet으로 로드될 때 주 요소(RSS, 채널, 항목)는 테이블로 로드됩니다. 계층 구조의 각 수준은 DataTable이 되며 DataTable의 기능을 사용하여 이러한 수준을 읽고 탐색할 수 있습니다. 각 주 요소를 설명하는 간단한 자식 요소는 해당 DataTable에 열로 로드됩니다. 예를 들어 채널 DataTable에는 항목, 설명, 링크 요소(이 세 가지 요소는 모두 필수임)에 대한 DataColumn이 포함됩니다.

마지막으로 DataTable 채널과 채널에 0개 이상의 항목이 어떻게 포함되는지를 나타내는 DataTable 항목 간에 DataRelation이 생성됩니다. 필자는 이 관계를 활용하여 사용자가 특정 피드를 선택할 때 응용 프로그램에서 특정 채널의 항목을 찾을 수 있도록 했습니다.

피드 목록 로드

피드가 DataSet으로 로드된 다음에는 채널 테이블을 찾아 채널의 제목과 각 채널에 포함된 항목의 수를 확인했습니다. 이 정보를 배열에 저장하고 피드를 수신할 때 이를 목록 보기로 로드합니다(그림 5 참조).

Figure 5 피드 목록 로드

코드 복사

foreach (DataRow channelRow in ds.Tables["channel"].Rows)
{
    string title = channelRow["title"].ToString();
    int itemCount = int.Parse(channelRow.GetChildRows(
        "channel_item").Length.ToString());

    string[] displayInfo = new string[2];
    displayInfo[0] = title;
    displayInfo[1] = itemCount.ToString();
    
    ListViewItem item = new ListViewItem(displayInfo); 
    item.Tag = channelRow;
    lvFeed.Items.Add(item); 
}

채널의 제목은 DataRow 열에서 값을 읽는 표준 ADO.NET 구문으로 액세스합니다. 각 채널의 총 항목 수를 얻기 위해 DataRow의 GetChildRows 메서드를 사용하여 현재 채널 행의 항목에 대한 DataRow 개체 배열을 반환하도록 했습니다. GetChildRows 메서드는 DataRelation의 이름을 받습니다. DataRelation은 기본적으로 DataTable 채널 이름과 DataTable 항목 이름을 밑줄로 연결한 형식입니다. ListViewItem의 태그에 채널의 DataRow에 대한 참조를 저장한 것을 볼 수 있습니다. 이렇게 하면 사용자가 특정 피드를 선택할 때 피드의 정보를 쉽게 가져올 수 있습니다.

익명 대리자를 사용한 정렬

사용자가 피드를 선택하면 DataRow가 FeedItemList 폼으로 전달되며 이 폼은 항목의 목록을 ListView 컨트롤에 로드합니다. 항목의 제목과 함께 게시 날짜(제공되는 경우)도 로드합니다. pubDate 요소는 RSS 2.0 스키마에서 필수 요소가 아니므로 먼저 항목 DataTable에 열이 있는지 여부를 확인합니다. 피드의 항목을 나타내는 DataRow 개체 배열이 있으므로 이를 사용하여 DataRow의 DataTable 개체를 찾은 다음 DataTable.Columns.Contains 메서드를 사용하여 pubDate 열이 있는지 여부를 확인할 수 있습니다. 복잡하게 보이지만 다음과 같이 코드는 매우 간단합니다.

코드 복사

bool containsPubDate = (itemRows[0].Table.Columns.Contains("pubDate"));

pubDate의 존재 여부를 확인한 다음에는 목록 보기에서 항목을 어떻게 정렬 및 표시할지 결정할 수 있습니다. pubDate가 있으면 항목을 날짜별 내림차순으로 정렬하며 pubDate가 없으면 항목을 제목별로 정렬하고 목록 보기에서 게시 날짜 열을 숨깁니다.

pubDate가 있으면 다음 코드가 실행됩니다.

코드 복사

colNewsItemName.Width = 170;
colPubDate.Width = 140;

Array.Sort(itemRows, delegate(DataRow row1, DataRow row2)
{
    DateTime pubDate1 = ParseDateTime(row1["pubDate"].ToString());
    DateTime pubDate2 = ParseDateTime(row2["pubDate"].ToString());
    return pubDate2.CompareTo(pubDate1);
});

이 코드는 항목의 제목과 게시 날짜가 모두 표시하도록 열의 너비를 설정합니다. 그런 다음 Array.Sort 메서드를 사용하여 DataRow 개체 배열을 정렬합니다. 날짜를 정렬하려면 먼저 날짜를 유효한 DateTime으로 변환해야 합니다. 변환한 다음 사용자 지정 비교 대리자를 사용하여 DataRow 배열을 정렬합니다.

사용자 지정 비교 대리자는 간편하고 뛰어난 정렬 루틴 코딩 방법입니다. 정렬을 처리하는 명시적인 루틴을 만들 수도 있지만 코드 줄 수를 최소화하는 데는 사용자 지정 비교 대리자가 좋은 선택입니다. 배열의 각 요소 비교에 대해 날짜 비교가 실행됩니다. 이 예에서는 두 DataRow 개체를 받아 pubDate 열의 값을 계산하여 비교하는 익명 대리자를 만듭니다.

pubDate 열이 없으면 목록 보기에서 두 번째 열을 숨기고 첫 번째 열을 화면 너비에 맞게 확장합니다. 그런 다음 DataRow 배열을 항목 제목별 오름차순으로 정렬합니다. 이번에는 문자열 값을 비교한다는 점을 제외하면 사용된 기법은 동일합니다.

코드 복사

colNewsItemName.Width = 310;
colPubDate.Width = 0;

Array.Sort(itemRows, delegate(DataRow row1, DataRow row2)
{
    return row1["title"].ToString().CompareTo(row2["title"].ToString());
});

비교자 기능을 재사용할 필요가 없는 경우 무명 메서드는 코드의 양을 줄일 수 있는 유용한 방법입니다.

DataRow 배열을 정렬한 다음에는 그림 6과 같이 항목을 반복하면서 제목과 pubDate(있는 경우)를 가져옵니다. pubDate가 있으면 적절히 서식을 지정하고 제목과 pubDate를 문자열 배열로 로드한 다음 이를 사용하여 목록 보기를 로드합니다. 이 시점에서 목록은 그림 2와 같이 표시됩니다.

Figure 6 항목 목록 로드

코드 복사

foreach (DataRow itemRow in itemRows)
{
    string title = itemRow["title"].ToString();
    string[] displayInfo = new string[2];
    displayInfo[0] = title;
    if (containsPubDate)
    {
        string pubDateString = 
            itemRow.Table.Columns.Contains("pubDate") ? 
                itemRow["pubDate"].ToString() : string.Empty;
        DateTime pubDate = ParseDateTime(pubDateString);
        displayInfo[1] = pubDate.ToString("MM/dd/yyyy HH:mm");
    }

    ListViewItem item = new ListViewItem(displayInfo);
    item.Tag = itemRow;
    lvFeed.Items.Add(item);
}

WebBrowser 컨트롤을 사용하여 표시

사용자가 항목을 선택하면 선택한 ListViewItem을 검사하고 해당 Tag 속성에서 선택한 항목의 DataRow를 가져옵니다. 선택한 항목을 포함하는 DataRow는 선택한 항목의 설명을 표시하는 ItemSummary 폼의 생성자로 전달됩니다. 필자는 설명을 표시하는 데 레이블 컨트롤을 사용하지 않았는데 이것은 RSS 피드의 항목 설명에 포함된 HTML이 있는 경우가 많기 때문입니다. 따라서 WebBrowser 컨트롤을 사용하여 설명을 표시했습니다.

그림 3은 WebBrowser 컨트롤에서 항목 설명의 요약을 보여 줍니다. 요약 설명에 하이퍼링크 및 기타 HTML 요소가 포함된 것을 볼 수 있습니다. HTML을 렌더링하기 위해 필자는 다음과 같이 WebBrowser 컨트롤의 DocumentText 속성을 항목의 설명으로, 폼의 Text 속성을 항목의 제목으로 설정했습니다.

코드 복사

this.Text = itemRow["title"].ToString();
webSummary.DocumentText = itemRow["description"].ToString();

이 응용 프로그램에서 WebBrowser 컨트롤은 두 가지 목적으로 사용됩니다. ItemSummary 폼에서는 이 컨트롤을 사용하여 HTML을 포함하거나 포함하지 않은 항목의 설명을 표시하며 항목의 DataRow에 지정된 링크로 이동하는 Item 폼에서도 이 컨트롤을 사용합니다. 다음은 WebBrowser 컨트롤을 사용하는 좀 더 일반적인 방법입니다.

코드 복사

this.Text = itemRow["title"].ToString();
webItem.Navigate(new Uri(itemRow["link"].ToString()));

여기에서 Item 폼의 제목을 항목의 제목으로 설정하고 WebBrowser 컨트롤에서 Navigate 메서드를 사용하여 항목의 URL로 이동합니다. 링크의 위치를 찾은 다음에는 페이지가 WebBrowser 컨트롤에 로드됩니다.

Smartphone에 배포

응용 프로그램을 빌드한 다음에는 실제 Smartphone 장치에 배포해야 합니다. 이 시점에서 필자는 Smartphone으로 배포하는 과정을 전체적으로 테스트하기 위해 보통 에뮬레이터 창과 장치 에뮬레이터 관리자를 종료합니다.

응용 프로그램 배포에서 몇 가지 알아야 할 사항이 있습니다. 먼저 Smartphone Device CAB 프로젝트를 만들고 이를 현재 솔루션에 추가해야 합니다. 또한 .NET Compact Framework 2.0을 사용하여 작성한 응용 프로그램을 실행하려면 모바일 장치에 .NET Compact Framework 2.0이 설치되어 있어야 합니다. 이 두 가지 단계를 처리한 다음에는 CAB 파일을 모바일 장치에 복사하고 응용 프로그램을 직접 설치할 수 있습니다.

필자는 MSDN200611로 명명된 CAB 프로젝트를 만들고 콘텐츠 파일과 기본 출력을 추가했습니다. 콘텐츠 파일은 필자가 Smartphone 장치 응용 프로그램에서 사용한 아이콘 파일을 가져옵니다. 기본 출력에 대한 바로 가기를 만들고 CAB 파일 프로젝트에 Start Menu 폴더를 추가한 다음 Start Menu 폴더에 바로 가기를 끌어서 놓습니다. 이렇게 하면 Smartphone의 Start Menu에 있는 응용 프로그램 목록에 해당 응용 프로그램이 추가됩니다. 또한 바로 가기 이름을 좀 더 친숙하고 짧은 이름으로 변경했습니다.

결론

.NET Compact Framework 2.0으로 데이터 중심 모바일 응용 프로그램을 개발할 때 사용하는 기술은 Windows Forms 응용 프로그램 개발에 사용하는 기술과 많은 부분 동일합니다. 이번 달에는 ADO.NET을 사용하여 RSS 피드를 로드하고 피드 및 해당 항목을 탐색하며 RSS 정보의 존재 여부를 확인하는 방법에 대해 알아보았습니다. 무명 메서드 및 비동기 호출도 개발에 도움이 되었지만 이 응용 프로그램의 핵심은 여전히 RSS 피드, DataSet, DataRow 개체 배열 또는 항목 설명에 대한 WebBrowser 컨트롤에 렌더링되는 HTML 중 어떤 것이든지 데이터 자체입니다

+ Recent posts