2016년 초 블로그를 개설만 해놓고 들여다 보지도 않던때
개발업무를 하면서 구글링을 하다보면 티스토리 블로그가 눈에띄게 많았다.
그리고 글에 붙어있는 광고.

저 광고 뭐지? 애드센스? 돈 좀 되나? 나도 해볼까?

시작은 아무생각 없었다.
그냥 블로그에 복습겸, 취미겸 코딩에 대한 글을 올릴생각 이였고, 광고가 붙으면 뭔가 뿌듯할거 같았다.

그래서 애드센스에 대해 구글링을 하기 시작했다.
쉽게만 생각했던 애드센스는 "애드고사"라는 별명이 있었다.
별생각 없이 블로그를 운영하고 싶었던 나는 글 3개? 정도를 올리고 애드센스 신청을 했고 당연히 거절됐다.
그리고 글이 6개가 되었을때 다시한번 애드센스를 신청했는데...?

됐다.!!!
(승인이 2018년 8월인건 안비밀... 2년 반동안 글 6개 밖에 안올렸다.. ㅋㅋ)

 

"애드고사"라는 말이 무색했다. 무슨원리인지는 모르겠지만 통과한 기쁨에 얼른 광고를 게재해봤고.
내 블로그에 광고가 뜨는게 너무 신기해 5분동안 턱괴고 화면만 보았었다. ㅋㅋㅋ

그리고 현재 1년 반동안 글 6개를 더 게시 하여 총 12개다.
(그냥 심심해서 하는 걸로...)

그렇게 1년 반동안 애드센스 수익은 10.95$

수익을 위해 열심히 글을 올리고 싶은 생각은 없다.
애초에 시작도 호기심과 재미였으며, 복습을 위해서 였다.
그래도 신경안쓰다 어쩌다 한번 보면 1$씩 올라가있는 잔고가 왠지 뿌듯하다..ㅋㅋ

최근 Winform을 이용하여 네이버 카페에 자동으로 글을 등록하는 프로그램을 개발하게 되어 기술과 소스를 공유하고자 작성한다.

 

로그인 / 글쓰기 테스트

 

1. 네이버 자동 로그인

네아로(네이버 아이디로 로그인)을 활용하여 자동 로그인을 구현하려 했다.

Winform의 WebBrowser 혹은 ASP.NET MVC or ASP.NET Webform을 활용하여 네이버 로그인을 구현한 경험은 있지만, WebBrowser없이 로그인을 구현하는 경우는 처음이라 많이 삽질을 했다. 구글에 WebBrowser를 띄우지 않고 로그인하는 방법은 많지 않았고, 있다고 하더라고 POST 패킷 분석 로그인 방법이였다.

 

패킷 분석 로그인 방법은 두 곳을 참조 하였다.

[참조]

- http://non-sponsor.tistory.com/6 (네이버 bvsd 로그인)

- https://it-raccoon.tistory.com/15 (위 코드 기반으로 최신업데이트)

 

 

* bvsd : 네이버 로그인시 전송하는 파라미터의 key이다. 위 [참조] 사이트 들에서는 bvsd를 생성하여 로그인하는 방법을 안내하고 있다. 로그인 성공 이후 Cookie를 저장하여 로그인을 유지한다.

 

위 두 코드를 활용하여 bvsd 로그인 시 OAuth 로그인이 되도록 수정하였다.

할땐 골치 아프더니 다 하고 나니 생각보다 간단

 

1-1. 네이버 로그인 OAuth 구현

보통 네이버 로그인 OAuth 가이드에 따라

[네이버 OAuth 로그인 Url] https://nid.naver.com/oauth2.0/authorize?client_id={client_id}&response_type=code&redirect_uri={callback_url}&state={state} 

경로를 브라우저에 띄워 로그인을 유도한다. 하지만, 원하는건 브라우저 혹은 WebBrowser없이 코드로 로그인을 하는것이다.

 

##

추가적으로 response_type=token으로 할 경우와 code로 할 경우 응답 값이 다르다.

둘 다 로그인 구현은 가능하지만 응답값이 다르다.

token의 경우 응답값으로 Access Token을 받을 수 있으며, Refresh Token을 받을 수 없다.

code의 경우 응답값으로 code와 state를 받아 Access Token과 Refresh Token을 발급 받아야 한다.

##

 

HttpWebRequest로 해당 Url에 bvsd와 OAuth 파라미터를 추가하여 로그인 시도를 한다.

* logintp=oauth2, url=https://nid.naver.com/oauth2.0/authorize?client_id={client_id}&response_type=code&redirect_uri={callback_url}&state={state}을 넣어주면된다.

추가적으로, svctype, smart_LEVEL은 PC마다 다르게 나올수도 있다. 해당값을 바꿔줘야 하는지는 잘 모르겠다.

위 이미지의 파란박스가 bvsd이다.

 

소스에서는 위와 같이 작성되어 있다.

 

이렇게 해주면 bvsd를 활용하여 OAuth 로그인을 성공할 수 있다.

이후 작업은 OAuth 로직에 맞춰 진행되며, 위에서 언급한 code와 state로 Access Token과 Refresh Token을 발급 받아 네이버 API 이용할 수 있다.

 

Refesh Token을 저장하여 추후 ID, PW입력없이 API 사용이 가능하다.

 

1-2. 카페 API 사용

소스는 네이버 개발자 카페 글쓰기 명세에서 그대로 가져와서 썼다.

아주 친절하게 잘 작성되어있다.

 

카페 글쓰기 API 사용시 ClubID와 MenuID를 알아야 글을 작성 할 수 있다.

ClubID와 MenuID는 카페 메뉴에서 오른쪽 클릭 후 "새 창에서 열기" or "새 탭에서 열기"를 하면 확인 가능하다.

https://cafe.naver.com/블라블라?iframe_url=/ArticleList.nhn%3Fsearch.clubid=카페아이디%26search.menuid=메뉴아이디%26search.boardtype=L

 

 

textbox에 미리 값을 채워서 사용해주세요. (안그럼 먹통 인건 안비밀)

 

 

// 급하게 해서 소스가 더러워도 이해해주세여ㅠㅠ

NaverCafeTest.zip
0.02MB

'Programming > C#' 카테고리의 다른 글

[C#]ini file 사용법 & 소스 공유  (540) 2017.01.03
[C#]Log4Net 사용법  (587) 2017.01.03
[C#]Snappy 사용법  (143) 2016.12.30
[C#]DataTable Sorting 간단 사용법  (971) 2016.12.30

WebForms 개발시 Repeater를 자주 사용하게 된다. (회사마다 다르겠지만)
Repeater 사용시 중첩으로 사용하고 싶은 경우들이 있다.

예를들어.

A사 (Key: A)  
  A-1 계열사 (FKey: A)
B사 (Key: B)  
  B-1 계열사 (FKey: B)
  B-2 계열사 (FKey: B)

위와 같은 테이블을 만들기 위해 중첩 Repeater를 사용할 수 있다. (물론 DB조회를 잘 해와서 하나의 Repeater로도 충분히 표현 가능하다. 예시일뿐.)

사용하려는 방법은 DataSet의 Relations를 활용한 방법이다.
* MS Docs DataRelation 참고
Relations를 활용하면 키값으로 두개의 DataTable을 묶어서 사용할 수 있다.
구글링을 하여 찾아보면, OnItemDataBound에서 한번 더 쿼리를 실행해 바인딩(부모의 매 Row마다 실행)하는 방법을 많이 볼 수 있다.
하지만 DataRelation을 사용하면 한번의 쿼리 실행으로 원하는 결과를 만들 수 있다.

코드를 확인하며 이해해보자

[aspx]
Repeater 두개를 중첩하여 작성하였다.
OnItemDataBound에서 Relation정보를 찾아와 SubCompanyList에 바인딩 한다.
    * MS Docs DataBinder.Eval 참고

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<table>
    <tbody>
        <asp:Repeater ID="CompanyList" runat="server" OnItemDataBound="CompanyList_ItemDataBound">
            <ItemTemplate>
                <tr>
                    <td><%# Eval("Name") %> (Key: <%# Eval("Key") %>)</td>
                    <td>&nbsp;</td>
                </tr>
                <asp:Repeater ID="SubCompanyList" runat="server">
                    <ItemTemplate>
                        <tr>
                            <td>&nbsp;</td>
                            <td><%# DataBinder.Eval(Container.DataItem, "[\"Name\"]") %> (FKey: <%# DataBinder.Eval(Container.DataItem, "[\"FKey\"]") %>)</td>
                        </tr>
                    </ItemTemplate>
                </asp:Repeater>
            </ItemTemplate>
        </asp:Repeater>
    </tbody>
</table>
cs

 

[aspx.cs]
1. Page_Load에서 dtCompany, dtSubCompany(임시로 만든 정보입니다. 현업에서는 DB를 읽어와 사용하겠죠?)를 불러와 DataSet을 생성한다.
2. DataSet.Relations를 추가 하여 관계를 생성한다. (관계의 이름은 "CompanyKey"이다.)
    * Relations의 파라미터로 "관계의 이름", "부모 키 컬럼", "자식 키 컬럼"을 넣어준다.

3. 최상단 Repeater에 부모 Table을 바인딩한다.
4. OnItemDataBound Event에서 자식 Table을 찾아 Sub Repeater에 바인딩한다. (GetChildRows 함수 사용)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
 
protected void Page_Load(object sender, EventArgs e)
{
    var dtCompany = InitCompanyData();
    var dtSubCompany = InitSubCompanyData();
    // 데이터셋 생성
    var ds = new DataSet();
    ds.Tables.Add(dtCompany);
    ds.Tables.Add(dtSubCompany);
    // 관계 설정
// DB 조회시에는 SP에서 테이블을 여러개 읽어와 Fill로 DataSet을 만든 후 Relation만 잡아주면 된다.
    ds.Relations.Add("CompanyKey", ds.Tables["CompanyTable"].Columns["Key"], ds.Tables["SubCompanyTable"].Columns["FKey"]);
    // 부모 테이블 바인딩
    CompanyList.DataSource = ds.Tables["CompanyTable"];
    CompanyList.DataBind();
}
 
private DataTable InitCompanyData()
{
    var dtCompany = new DataTable();
    dtCompany.TableName = "CompanyTable";
    dtCompany.Columns.Add("Key"typeof(string));
    dtCompany.Columns.Add("Name"typeof(string));
    var newRow = dtCompany.NewRow();
    newRow["Key"= "A";
    newRow["Name"= "A사";
    dtCompany.Rows.Add(newRow);
    newRow = dtCompany.NewRow();
    newRow["Key"= "B";
    newRow["Name"= "B사";
    dtCompany.Rows.Add(newRow);
    return dtCompany;
}
 
private DataTable InitSubCompanyData()
{
    var dtSubCompany = new DataTable();
    dtSubCompany.TableName = "SubCompanyTable";
    dtSubCompany.Columns.Add("FKey"typeof(string));
    dtSubCompany.Columns.Add("Name"typeof(string));
    var newRow = dtSubCompany.NewRow();
    newRow["FKey"= "A";
    newRow["Name"= "A-1 계열사";
    dtSubCompany.Rows.Add(newRow);
    newRow = dtSubCompany.NewRow();
    newRow["FKey"= "B";
    newRow["Name"= "B-1 계열사";
    dtSubCompany.Rows.Add(newRow);
    newRow = dtSubCompany.NewRow();
    newRow["FKey"= "B";
    newRow["Name"= "B-2 계열사";
    dtSubCompany.Rows.Add(newRow);
    return dtSubCompany;
}
 
protected void CompanyList_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
    {
        Repeater SubCompanyList = e.Item.FindControl("SubCompanyList"as Repeater;
        // 관계가 설정된 자식 테이블 얻어오기
// 이 부분에서 한번 더 쿼리를 실행할 필요 없이, 관계가 맺어진 정보를 읽어올 수 있다.
        var childRows = ((DataRowView)e.Item.DataItem).Row.GetChildRows("CompanyKey");
        SubCompanyList.DataSource = childRows;
        SubCompanyList.DataBind();
    }
}
cs

 

 

[결과]

직접 디버깅을 해보면 알겠지만 OnItemDataBound Event에서 부모 Table의 Row에 자식 테이블이 엮여 있는것을 볼 수 있다.

 

* DB조회시 까다로운 쿼리 혹은 전혀 관계 없는 데이터를 코드딴에서 묶어서 사용하고 싶을때도 유용하게 사용가능하다. 일자별로 묶어서 데이터를 처리한다던가 하는 그런것들 말이다.

 

다음번 글은 MS사의 Bot Framework와 Language Understanding(LUIS:루이스)을 작성해볼까 한다. (텔레그램, 스카이프 등등 연동도 간편하고 좋다. 단지 LUIS가 유료)

 

끗.

WebForm 개발시 UpdatePanel을 자주 사용하게 된다.


클라이언트(브라우저)딴에서 UpdatePanel을 이용하여 동적으로 화면을 그릴 경우 Load되는 타이밍을 잡아 이벤트바인딩, 데이터바인딩 등의 작업을 해야할 경우가 있다.

이때 Sys.Application을 사용하여 이벤트를 잡을 수 있다.


Sys.Application란, ASP.NET WebForm에서 클라이언트딴의 이벤트를 제어할 수 있도록 제공하는 클라이언트딴 클래스이다.

해당 클래스를 이용하여 Load 이벤트를 잡아보자.


* Sys.Application 클래스에는 Load 뿐 아니라 다양한 이벤트들이 존재합니다. 문서 확인 후 응용하시면 됩니다.


사용 예

[클라이언트]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<html>
<head>
    <script>
        function UpdatePanel_LoadEvent() {
            console.log($('#contents').text());
        }
    </script>
</head>
<body>
    <asp:ScriptManager ID="ScriptManager1" runat="server"></asp:ScriptManager>
    <asp:UpdatePanel ID="UpdatePanel1" runat="server">
        <ContentTemplate>
            <script>
                // Sys.Application 클래스를 이용하여 이벤트 등록
                Sys.Application.add_load(UpdatePanel_LoadEvent);
            </script>
            <asp:Label ID="contents" ClientIDMode="Static" runat="server" Text="내용입니다."></asp:Label>
        </ContentTemplate>
        <Triggers>
            <asp:AsyncPostBackTrigger ControlID="Button_Submit" EventName="Click" />
        </Triggers>
    </asp:UpdatePanel>
 
    <asp:Button ID="Button_Submit" runat="server" OnClick="Button_Submit_Click" Text="리로드" />
</body>
</html>
cs


[비하인드]

1
2
3
4
5
6
7
8
9
10
11
12
public partial class LoadEventTest : Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        
    }
 
    protected void Button_Submit_Click(object sender, EventArgs e)
    {
        contents.Text = "리로드되었나요?";
    }
}
cs


[참조]

https://docs.microsoft.com/ko-kr/dotnet/api/system.web.ui.updatepanel?view=netframework-4.7.2

https://docs.microsoft.com/en-us/previous-versions/bb383829%28v%3dvs.100%29

ASP.NET MVC에서 Linq to Sql 또는 EF(Entity Framework)를 이용해 DataTables를 개발하는 방법에 대해 작성하려 한다.
이 글에서는 Linq to Sql로 설명 하지만, 구현하는 방법은 EF도 크게 다르지 않을것 같다는 생각이 든다.


DataTables를 간단하게 설명하자면 데이터를 테이블로 표현할때 사용되는 jquery 라이브러리로 페이징, 정렬, 검색, 커스터마이징 등 다양한 기능을 지원하여 손쉽게 개발할 수 있다.


Linq to SqlEF의 아주 간단한 차이점을 말하자면 아래와 같다.

1. Linq to Sql은 MS-SQL만 지원하지만 EF는 대부분의 DB엔진을 지원한다.
2. Linq to Sql은 DB First이며, EF는 DB First, Code First 선택 가능하다. (DB First, Code First에 대한 개념은 인터넷을 찾아보세요.)


구현에 앞서 Linq to Sql의 간단한 사용법을 먼저 확인하고 넘어가자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// DB Context 객체 선언
dbDataContext db = new dbDataContext();
 
// 다음과 같은 Query를 Linq로 표현해보자.
// Account라는 테이블에서 전화번호 1111을 포함한 홍길동이라는 사람을 찾는다.
// SELECT * FROM Account WHERE name = '홍길동' AND phone like '%1111%'
// Linq를 사용하는 방법은 크게 두가지가 있다.
 
// 1. 쿼리 형식 (자세한 사용법은 인터넷을 찾아보세요.)
 
// 반환형은 IQueryable<Account>이다.
var data = from d in db.Account
           where d.name == "홍길동" && d.phone.Contains("1111")
           select d;
 
// 2. 함수 형식
// 반환형은 위와 같이 IQueryable<Account>이다.
var data = db.Account.where(a => a.name == "홍길동" && a.phone.Contains("1111"));
cs


DataTables 공식 홈페이지


1. Quick Start
    1) Html을 작성한다.
    2) DataTables의 Javascript, CSS 파일을 CDN 혹은 다운로드 하여 참조 한다.
    3) Javascript 소스에서 DataTables 초기화

* 아래 소스 참조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- DataTables CDN -->
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.19/css/jquery.dataTables.css">
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.js"></script>
 
<table id="table_1">
    <thead>
        <tr>
            <th>col1</th>
            <th>col2</th>
            <th>col3</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Row 1 Data 1</td>
            <td>Row 1 Data 2</td>
            <td>Row 1 Data 3</td>
        </tr>
        <tr>
            <td>Row 2 Data 1</td>
            <td>Row 2 Data 2</td>
            <td>Row 2 Data 3</td>
        </tr>
        <tr>
            <td>Row 3 Data 1</td>
            <td>Row 4 Data 2</td>
            <td>Row 5 Data 3</td>
        </tr>
    </tbody>
</table>
 
<script>
    $(document).ready( function () {
        $('#table_1').DataTable();
    });
</script>
cs


! DataTables 사용시 데이터를 어디서 처리하는지에 따라 Client-side와 Server-side로 기능을 구현할 수 있다.


2. Client-side processing
    1)  Client-side의 경우 Quick Start와 동일하다. 
        페이지가 열렸을때 Html Table을 작성한 뒤 Script로 초기화만 하면 끝난다.

* 아래 소스 참조 (ASP.NET MVC Razor 구문을 사용하였다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- DataTables CDN -->
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.19/css/jquery.dataTables.css">
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.js"></script>
 
<table id="table_1">
    <thead>
        <tr>
            <th>이름</th>
            <th>전화번호</th>
            <th>col3</th>
        </tr>
    </thead>
    <tbody>
        @foreach(var item in Model)
        {
            <tr>
                <td>item.name</td>
                <td>item.phone</td>
                <td>item.address</td>
            </tr>
        }
    </tbody>
</table>
 
<script>
    $(document).ready( function () {
        $('#table_1').DataTable();
    });
</script>
cs


3. Server-side processing (참조 https://datatables.net/manual/server-side)
    1) Server-side의 경우 DataTables의 옵션으로 쉽게 구현이 가능하다.
    2) Server딴에 데이터를 얻어오기 위한 REST를 구현해야한다.


* 구현시 주의 사항
    -> ajax 옵션에서 컬럼을 지정할 수 있다.
    -> 컬럼을 지정하지 않을 시 컬럼 순서대로 데이터가 매칭된다.

* 아래 소스 참조 (Client딴)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!-- DataTables CDN -->
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.19/css/jquery.dataTables.css">
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.js"></script>
 
<table id="table_1">
    <thead>
        <tr>
            <th>이름</th>
            <th>전화번호</th>
            <th>주소</th>
        </tr>
    </thead>
    <tbody>
    </tbody>
</table>
 
<script>
    $(document).ready( function () {
        $('#table_1').DataTable({
            serverSide: true// Server-side 활성화
            ajax: {
                url: '@Url.Action("GetAccounts")'// 데이터를 얻어오기 위한 EndPoint Url
                type: 'POST'
            },
            columns: [
                { "data""name" },
                { "data""phone" },
                { "data""address" }
            ]
        });
    });
</script>
 
cs


* 아래 소스 참조 (Server딴)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
[HttpPost]
public async JsonResult GetAccounts(DataTablesModel model)
{
    try
    {
        using(dbDataContext db = new dbDataContext())
        {
            var accounts = from d in db.Account
                           where d.name.Contains(model.search.value) && d.phone.Contains(model.search.value)
                           select new 
                           {
                                name = d.name,
                                phone = d.phone,
                                address = d.address
                           };
 
 
            //정렬은 직접 구현해보세요.
            // hint
            // 1. model.order[].column의 값이 정렬할 column 명입니다.
            // 2. model.order[].dir의 값이 정렬 방법입니다. desc or asc
            // 3. 멀티 정렬도 가능합니다.
            
            // 페이징 기능 구현
            int pageLength = model.length == -1 ? accounts.Count() : model.length;
            accounts = accounts.Skip(model.start).Take(pageLength);
 
            return Json(new
            {
                draw = model.draw,
                recordsTotal = db.Account.Count(),
                recordsFiltered = accounts.Count(),
                data = accounts, // 이때 DB에서 Query를 실행하여 데이터를 가져온다.
                error = false
            });
        }
    }
    catch(Exception ex)
    {
        return Json(new
        {
            draw = model.draw,
            recordsTotal = 0,
            recordsFiltered = 0,
            data = "",
            error = ex.ToString()
        });        
    }
}
 
#region DataTables Request Model
public class DataTablesModel
{
    public int draw { get; set; }
    public int start { get; set; } // 시작 페이지
    public int length { get; set; } // 페이지에 보여질 Row의 수
    public List<Column> columns { get; set; }
    public Search search { get; set; }
    public List<Order> order { get; set; }
}
 
public class Column
{
    public string data { get; set; }
    public string name { get; set; }
    public bool searchable { get; set; }
    public bool orderable { get; set; }
    public Search search { get; set; }
}
 
public class Search
{
    public string value { get; set; }
    public string regex { get; set; }
}
 
public class Order
{
    public int column { get; set; }
    public string dir { get; set; }
}
#endregion
cs


끗.

80040154 클래스가 등록되지 않았습니다. <- 이 에러는 무엇인가...

사용하려는 DLL의 비트수와 프로그램간의 비트수가 맞지 않아 발생하는 에러인듯 하다.


80040154 클래스가 등록되지 않았습니다. (예외가 발생한 HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG)) 오류로 인해 CLSID가 {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}인 구성 요소의 COM 클래스 팩터리를 검색하지 못했습니다.!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


빌드시 x86, 혹은 x64로 DLL과 동일한 비트수로 빌드를 하면 문제가 해결된다기에 해봤지만 IIS 에서는 해결방법이 되질 못했다.


IIS에서는 설정을 변경해줘야한다.



아래와 같은 순서로 설정을 변경하면 문제가 해결된다.
64비트 Windows Server 기준으로 IIS는 기본적으로 64비트로 동작한다.
내가 사용하려는 DLL이 32비트였던 것이다.....................


위와 같이 "32비트 응용 프로그램 사용"을 True로 변경하면 문제가 해결된다.


끝.

한동안 바빠 오랜만에 글을 쓰는것 같다.


얼마전 프로젝트를 통해 REST 통신시 데이터 인증을 하기 위해 HMAC SHA-256 알고리즘을 사용한다는 것을 알게 되었다.

일방향 암호화라 복호화가 불가능하다. 클라이언트와 서버가 동일한 키값을 이용하여 데이터에 대한 무결성을 확인한다.


HMAC SHA-256에 대한 자세한 내용 및 이론은 아래 링크를 참조

SHA, HMAC


소스로 확인해보자.


C# Code

* C# HMACSHA256 클래스에 대한 정의는 아래 링크를 참조

https://msdn.microsoft.com/ko-kr/library/system.security.cryptography.hmacsha256(v=vs.110).aspx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// System.Security.Cryptography <- using을 해야 C#의 HMACSHA256 클래스 사용이 가능하다.
using System.Security.Cryptography;
 
// HMAC 생성 함수
private string GenerateHMAC(string key, string payload)
{
    // 키 생성
    var hmac_key = Encoding.UTF8.GetBytes(key);
 
    // timestamp 생성
    var timeStamp = DateTime.UtcNow;
    var timeSpan = (timeStamp - new DateTime(197011000));
    var hmac_timeStamp = (long)timeSpan.TotalMilliseconds;
 
    // HMAC-SHA256 객체 생성
    using (HMACSHA256 sha = new HMACSHA256(hmac_key))
    {
        // 본문 생성
        // 한글이 포함될 경우 글이 깨지는 경우가 생기기 때문에 payload를 base64로 변환 후 암호화를 진행한다.
// 타임스탬프와 본문의 내용을 합하여 사용하는 경우가 일반적이다.
// 타임스탬프 값을 이용해 호출, 응답 시간의 차이를 구해 invalid를 하거나 accepted를 하는 방식으로 사용가능하다.
// 예시에서는 (본문 + 타임스탬프)이지만, 구글링을 통해 찾아보면 (본문 + "^" + 타임스탬프) 등의 방법을 취한다.
        var bytes = Encoding.UTF8.GetBytes(payload + hmac_timeStamp);
        string base64 = Convert.ToBase64String(bytes);
        var message = Encoding.UTF8.GetBytes(base64);
 
        // 암호화
        var hash = sha.ComputeHash(message);
 
        // base64 컨버팅
        return Convert.ToBase64String(hash);
    }
}
cs


Javascript Code

Javascript를 통해 HMAC SHA256 암호화를 하기 위해선 아래 두개의 라이브러리를 로드해야한다.

1
2
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/hmac-sha256.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/enc-base64-min.js"></script>
cs

소스 및 예제 실행은 아래 링크를 통해 확인.

Javascript HMAC SHA-256


Node.js Code

nodejs의 crypto를 이용해 HMAC SHA-256 암호화를 한다.

* nodejs crypto에 대한 정의는 아래 링크를 참조

https://nodejs.org/api/crypto.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var crypto = require('crypto');
 
function GenerateHMAC(key, payload) {
    // 암호화 객체 생성, sha256 알고리즘 선택
    var hmac = crypto.createHmac('sha256', key);
 
    // 암호화할 본문 생성
    var timestamp = new Date().getTime();
    var message = new Buffer(payload + timestamp).toString('base64');
 
    hmac.write(message);
    hmac.end();
 
    return hmac.read();
}
 
var hash = GenerateHMAC('hello world''sha256');
 
var encoded_hash  = new Buffer(hash).toString('base64');
 
console.log(encoded_hash);
cs


C#, Javascript, Node.js 예제 소스에 동일한 payload와 key, timestamp 입력시 동일한 hash값을 얻을 수 있었다.


끝.

'Programming > 기타' 카테고리의 다른 글

[C#]텔레그램 봇 만들기  (650) 2017.01.05
noVNC  (597) 2017.01.02

작년에 클라이언트 프로그램에서 이슈가 발생했을 때 사용자에게 문자를 보내줘야하는 기능을 개발 해야했는데 

SMS는 돈주고 사야하므로 유료이므로 난 그럴 능력이 없으므로 텔레그램 봇을 개발했었다. 


그때는 "NetTelegramBotApi"라는 라이브러리를 사용해 개발을 했었는데

지금 글을 쓰려고 다시 찾아보니 Nuget에 더 좋은 라이브러리가 있었다.


"NetTelegramBotApi"는 패스 


Nuget에서 telegram을 검색하면 제일 위에 나오는 "Telegram.Bot"라이브러리를 써보자.

 <- 요놈 받으면 된다.



일단 개발해보기에 앞서 Telegram Bot을 만들려면 API access 키를 발급 받아야 한다.

어려울거 하나 없다.


핸드폰 보랴 모니터 보랴 왔다갔다 하지말고 https://web.telegram.org/ 에 들어가서 봇을 추가하자.


1. BotFather를 찾아라.

BotFather를 클릭하면 아래와 같이 화면이 바뀐다.



Start 버튼을 클릭하자.


그럼, 봇아빠가 알아듣는 커맨드가 쭈루룩 나온다. 그걸 잘 읽어보면 봇을 만들 수 있다.


2. Bot을 추가해라.


봇아빠에게 /newbot 이라고 입력하자.

그럼 이름을 말하라고 한다.

원하는 봇의 이름을 입력하자.

주의할점은 끝에 _bot 또는 Bot이 꼭 들어가야 한다. 안그러면 안만들어쥼


이름을 잘 입력해서 만들면 access token을 바로 똭하고 준다.


/// 그 외 다양한 커맨드가 있으니 한번씩 해보세요. 재밌어요.


3. Access Token을 받아 개발을 해보자.

그럼 access token을 복사해서 봇을 만들어 보자.




* 참고 

Telegram 홈페이지 Telegram Bot API : Telegram API Doc

Telegram.Bot 라이브러리 도큐먼트 : Telegram.Bot dll Doc

Telegram.Bot 라이브러리 예제 : Telegram.Bot Example  << 예제를 보면 다양한 케이스가 많다. 사진, 음성, 위치접근, 연락처, 등등


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////만들어 보자


나는 C# 콘솔 응용 프로그램 으로 만들었다.

    class Program
    {
        static void Main(string[] args)
        {
            /// 봇 접근 함수 호출
            testAPIAsync();
            Console.ReadLine();
        }

        /// 비동기 봇 접근
        static async void testAPIAsync()
        {
            var Bot = new Telegram.Bot.TelegramBotClient("봇 아빠가 준 access token을 넣어요.");
            var me = await Bot.GetMeAsync();
            // 올바르게 접속이 되면 봇 이름이 출력된다.
            System.Console.WriteLine("Hello my name is " + me.FirstName);
        }
    }

위 소스를 실행 하면 다음과 같은 결과 화면을 볼 수 있다.


간단하게 봇을 실행 시키는 것까지 성공했다.


하지만 내가 해야할 것은 이벤트가 생겼을 때 telegram 봇이 사용자에게 메세지를 전송 하거나, 


사용자로 부터 명령을 받아 DB를 바꾼다던지, DB를 조회해 정보를 보여준다던지 등의 작업을 해야한다.


아래 소스를 하나씩 따라가보자.


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

///텔레그램 dll using
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.ReplyMarkups;

namespace telegram
{
    class Program
    {
        //봇 생성
        private static readonly TelegramBotClient Bot = new TelegramBotClient("봇 아빠가 준 access token을 넣어요.");

        //임시방편 유저 리스트
        private static List<User> Users = new List<User>();
        
        //telegram 사용자의 상태 저장 변수
        private static Dictionary<long, UserState> dicUserState = new Dictionary<long, UserState>();

        static void Main(string[] args)
        {
            ///임시방편 유저 리스트에 유저 추가
            Users.Add(new User("시나공공", 28)); //28살이다...ㅁㄴ아ㅓㄹ ㅓㅁㄴ아ㅣㄹ허 님
            Users.Add(new User("텔레그램", 30));
            Users.Add(new User("봇아빠", 55));

            ///봇 이벤트 추가
            Bot.OnMessage += Bot_OnMessage;
            Bot.OnMessageEdited += Bot_OnMessage;
            Bot.OnReceiveError += Bot_OnReceiveError;
        
            ///Me 획득 (Me가 뭔지 정확하게는.... 일단 눈치껏 보면 Username에 sinagonggongBot이 들어가는걸 보면 Bot 본인을 뜻하는 듯하다)
            var me = Bot.GetMeAsync().Result;

            Console.Title = me.Username;

            /// Recv Start
            Bot.StartReceiving();
            Console.ReadLine();
            /// Recv Stop
            Bot.StopReceiving();
        }

        /// Recv Error
        private static void Bot_OnReceiveError(object sender, Telegram.Bot.Args.ReceiveErrorEventArgs e)
        {
            Debugger.Break();
        }

        ///사용자로 부터 Message Recv
        private static async void Bot_OnMessage(object sender, Telegram.Bot.Args.MessageEventArgs messageEventArgs)
        {
            /// Message 객체
            var message = messageEventArgs.Message;

            /// 예외처리
            if (message == null || message.Type != MessageType.TextMessage) return;

            /// "/사용자추가" 라는 명령을 받음
            if (message.Text.StartsWith("/사용자추가"))
            {
                dicUserState[message.Chat.Id] = UserState.addUser;
                await Bot.SendTextMessageAsync(message.Chat.Id, 
                    @"사용자 이름과 나이를 입력해 주세요.
ex)시나공공,28");
            }

            /// "/사용자삭제" 라는 명령을 받음
            else if (message.Text.StartsWith("/사용자삭제"))
            {
                dicUserState[message.Chat.Id] = UserState.deleteUser;
                await Bot.SendTextMessageAsync(message.Chat.Id, "사용자 이름을 입력해 주세요.");
            }

            /// "/사용자목록" 라는 명령을 받음
            else if (message.Text.StartsWith("/사용자목록"))
            {
                dicUserState[message.Chat.Id] = UserState.none;

                string _message = string.Empty;
                Users.ForEach(x => _message += string.Format("이름 : {0}, 나이 : {1}\r\n", x.Name, x.Age) );

                await Bot.SendTextMessageAsync(message.Chat.Id, _message);
            }

            /// "/도움말" 라는 명령을 받음
            else if(message.Text.StartsWith("/도움말"))
            {
                var usage = @"
/사용자추가    - 사용자 추가
/사용자삭제 - 사용자 삭제
/사용자목록  - 사용자 목록
/도움말       - 도움말
                            ";

                await Bot.SendTextMessageAsync(message.Chat.Id, usage,
                    replyMarkup: new ReplyKeyboardHide());
            }
            /// 그 외 다른 말을 받을 경우 사용자 상태를 보고 적절하게 대응한다.
            else
            {
                /// 예외처리
                if(!dicUserState.ContainsKey(message.Chat.Id))
                {
                    await Bot.SendTextMessageAsync(message.Chat.Id, "먼 말인지 모르겠어요.");
                    return;
                }

                /// 사용자 상태가 사용자 추가일 경우
                if (dicUserState[message.Chat.Id] == UserState.addUser)
                {
                    /// 이름,나이 로 입력을 받을 것이기 때문에 , 로 tokenizing하자
                    /// 0번은 이름, 1번은 나이
                    string[] NameAndAge = message.Text.Split(',');

                    /// 쪼갰는데 개수가 2보다 작으면 잘못된 값을 받았다 판단
                    if (NameAndAge.Length < 2)
                    {
                        await Bot.SendTextMessageAsync(message.Chat.Id, 
                            @"다시 입력해 주세요.
ex)시나공공, 28");
                        return;
                    }

                    /// 이름 나이 셋팅
                    string _name = NameAndAge[0];
                    int _age = 0;
                    bool result = Int32.TryParse(NameAndAge[1], out _age);

                    /// 나이값이 정상이면 추가
                    if(result)
                    {
                        // DB작업을 해야한다면 여기서 하면 될것같다.
                        Users.Add(new User(_name, _age));
                        await Bot.SendTextMessageAsync(message.Chat.Id, message.Text + " 사용자를 추가 했어요.");
                        dicUserState[message.Chat.Id] = UserState.none;
                    }
                    /// 나이값이 이상하면 예외
                    else
                    {
                        await Bot.SendTextMessageAsync(message.Chat.Id,
                            @"나이가 이상해요.
다시 입력해 주세요.");
                    }
                }

                /// 사용자 상태가 사용자 삭제일 경우
                else if(dicUserState[message.Chat.Id] == UserState.deleteUser)
                {
                    /// SingleOfDefault로 사용자를 찾았는데 없으면 예외처리 있으면 삭제
                    var _user = Users.SingleOrDefault(x => x.Name == message.Text);
                    if (_user != null)
                    {
                        // DB작업을 해야한다면 여기서 하면 될것같다.
                        Users.Remove(_user);
                        await Bot.SendTextMessageAsync(message.Chat.Id, message.Text + " 사용자를 삭제 했어요.");
                        dicUserState[message.Chat.Id] = UserState.none;
                    }
                    else
                    {
                        await Bot.SendTextMessageAsync(message.Chat.Id, 
                            message.Text + @" 사용자가 없어요.
다시 입력해 주세요.");
                    }
                }

                /// 사용자 상태가 추가, 삭제가 아닌 경우 시치미 뚝
                else
                {
                    await Bot.SendTextMessageAsync(message.Chat.Id, "먼 말인지 모르겠어요.");
                }

                
            }
        }
    }

    /// <summary>
    /// 사용자를 상태를 나타내는 enum
    /// </summary>
    public enum UserState
    {
        addUser,
        deleteUser,
        none
    }

    /// <summary>
    /// 사용자 클래스, 이름과 나이
    /// </summary>
    public class User
    {
        string name = string.Empty;
        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        int age = 0;
        public int Age
        {
            get { return age; }
            set { age = value; }
        }

        public User() : this("none", 0) { }
        public User(string name, int age)
        {
            this.Name = name;
            this.Age = age;
        }
    }
}


이렇게 하면 아래와 같은 결과를 볼 수 있다.

봇에게 말을 걸려면 봇아빠에게 말을 걸었던것과 같이 하면 된다.

나의 경우 @sinagonggongBot을 검색해 대화를 시작했다.




Telegram Bot을 이용해 간단하게 요청을 하고 응답을 받는 서비스를 만들어봤다.

네이버나 다음의 API를 섞어서 사용하거나, 나름의 알고리즘으로 유용한 봇을 만들 수 있을 것 같다.


아 5초마다 이벤트 발생시켜서 사용자한테 메세지 보내는걸 까먹었네. 그건 패스할게요... 귀찮...

키포인트는 Message.Chat.Id를 잘 보관해야 한다.


작년 개발 당시 Message.Chat.Id를 로그인 DB에 컬럼을 추가해 같이 저장해 이용했다.

이슈가 생겼을 때 DB의 TelegramID를 조회해 sendMessage를 했었다.


Group Chat방에 메세지를 보낼땐 https://stackoverflow.com/a/45577773 참조 바랍니다. (Group Chat ID 얻어오기 Tip)



끝.

'Programming > 기타' 카테고리의 다른 글

HMAC SHA-256 암호화 (C#, Javascript, Nodejs)  (9) 2018.08.28
noVNC  (597) 2017.01.02

C# 응용 프로그램을 만들다보면 설정 파일이 필요한 경우가 있다.

이때 ini file을 사용하면 간편하다. 물론 xml로 된 config file도 있지만 ini file이 직관적이고 수정하기 편하다.

구글링을 해보면 C#에서 ini파일을 다루는 여러 내용들이 있지만, 효율적으로 ini file을 관리하고 수정이 용이하게 설명된 내용이 없어 직접 만든 내용을 공유하려 한다.

그렇다고 내가 만든게 짱짱맨 이런 의미는 아니다.


ini file의 구성은

크게 section과 key로 나뉜다.

예를 들면

[Database]

Address="localhost"

Id="root"

[User]

Name="홍길동"

Age="29"


이런 ini file이 있을때 [Database]와 [User]가 section이 되고 Address, id, Name, Age가 key가 된다.


아래 소스를 보면서 하나씩 짚어가보자

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
/// System.ComponentModel.DataAnnotations 는 참조에서 어셈블리를 추가해야 한다.
using System.ComponentModel.DataAnnotations;
using System.Runtime.InteropServices;
using System.Text;

namespace ConfigManager
{
	/// <summary>
	/// ini file의 정보를 담을 그릇(클래스)
	/// <typeparam name="K1"></typeparam>
	/// <typeparam name="K2"></typeparam>
	/// <typeparam name="V"></typeparam>
	[Serializable]
	public class MultiKeyDictionary<k1, k2, v> : Dictionary<k1, dictionary<k2, v>>
	{
		public V this[K1 key1, K2 key2]
		{
			get
			{
				if (!ContainsKey(key1) || !this[key1].ContainsKey(key2))
				{
					return default(V);
					//throw new ArgumentOutOfRangeException();
				}

				return base[key1][key2];
			}
			set
			{
				if (!ContainsKey(key1))
					this[key1] = new Dictionary<k2, v>();
				this[key1][key2] = value;
			}
		}

		public void Add(K1 key1, K2 key2, V value)
		{
			if (!ContainsKey(key1))
				this[key1] = new Dictionary<k2, v>();
			this[key1][key2] = value;
		}

		public bool ContainsKey(K1 key1, K2 key2)
		{
			return base.ContainsKey(key1) && this[key1].ContainsKey(key2);
		}

		public new IEnumerable<v> Values
		{
			get
			{
				return from baseDict in base.Values
					   from baseKey in baseDict.Keys
					   select baseDict[baseKey];
			}
		}
	}

	/// <summary>
	/// Section 구조
	/// 추가할 Section이 있을 경우 enum에 추가하고 section name만 정의해주면 된다.
	/// </summary>
	public enum SectionNames
	{
		/// [Display(Name = "Database")] 에서 Name에 들어가는 문자열이 ini file에 실질적으로 쓰여지는 Section의 이름이다.

		/// <summary>
		///  Section Name "Database"
		/// </summary>
		[Display(Name = "Database")]
		//해당 이름과 아래 선언된 enum의 이름이 일치해야한다.
		Database_Section,

		/// <summary>
		/// Section Name "User"
		/// </summary>
		[Display(Name = "User")]
		User_Section,
	}

	/// <summary>
	/// Database Section의 Key 나열
	/// 추가할 key가 있을 경우 enum에 추가하고 디폴트 값만 정의해주면 된다.
	/// </summary>
	public enum Database_Section
	{
		/// [Display(Description = "localhost")]에서 Description에 들어가는 문자열은 ini에 key가 없는 경우 디폴트로 들어가는 값이다.

		/// <summary>
		/// Default Value : localhost
		/// </summary>
		[Display(Description = "localhost")]
		// Address 그대로 ini file의 key값으로 쓰여진다. 
		Address,

		/// <summary>
		/// Default Value : root
		/// </summary>
		[Display(Description = "root")]
		// Id 그대로 ini file의 key값으로 쓰여진다.
		Id,
	}

	/// <summary>
	/// User Section의 Key 나열
	/// </summary>
	public enum User_Section
	{
		/// <summary>
		/// Default Value : 홍길동
		/// </summary>
		[Display(Description = "홍길동")]
		Name,

		/// <summary>
		/// Default Value : 29
		/// </summary>
		[Display(Description = "29")]
		Age,
	}

	public class ConfigData
	{
		//ini file의 경로 설정
		//원하는 경로와 이름으로 바꿔주면 된다.
		private string inifilepath = @"C:\Config.ini";

		//ini file의 정보를 담을 변수 선언
		private MultiKeyDictionary<sectionnames, string, object> _ConfigData = null;

		//ini file을 읽을때 사용하는 함수
		[DllImport("kernel32.dll")]
		private static extern int GetPrivateProfileString(
			String section, String key, String def, StringBuilder retVal, int Size, String filePath);

		//ini file을 쓸때 사용하는 함수
		[DllImport("kernel32.dll")]
		private static extern long WritePrivateProfileString(
			String Section, String key, String val, String filePath);

		public ConfigData()
		{
			_ConfigData = new MultiKeyDictionary<sectionnames, string, object>();
		}

		/// <summary>
		/// ConfigData Class내에서 ini file을 쓰기 위한 함수
		/// </summary>
		/// <param name="Section">
		/// <param name="Key">
		/// <param name="Value">
		private void IniWriteValue(String Section, String Key, String Value)
		{
			WritePrivateProfileString(Section, Key, Value, inifilepath);
		}

		/// <summary>
		/// ConfigData Class내에서 ini file을 읽기 위한 함수
		/// </summary>
		/// <param name="Section">
		/// <param name="Key">
		/// <returns></returns>
		private String IniReadValue(String Section, String Key)
		{
			StringBuilder temp = new StringBuilder(255);
			int i = GetPrivateProfileString(Section, Key, "", temp, 255, inifilepath);

			return temp.ToString();
		}

		/// <summary>
		/// 외부에서 ConfigData Class 객체를 이용해 Ini file의 값을 셋팅하기 위한 함수
		/// </summary>
		/// <typeparam name="KeyEnum"></typeparam>
		/// <param name="key">
		/// <param name="value">
		public void SetConfigData<keyenum>(KeyEnum key, string value)
		{
			DisplayAttribute keyAttr = typeof(KeyEnum).GetMember(key.ToString())
											.First()
											.GetCustomAttribute<displayattribute>();

			SectionNames sectionName = (SectionNames)Enum.Parse(typeof(SectionNames), typeof(KeyEnum).Name);

			_ConfigData[sectionName][key.ToString()] = value;
		}

		/// <summary>
		/// 외부에서 ConfigData Class 객체를 이용해 Ini file의 값을 가져오기 위한 함수
		/// </summary>
		/// <typeparam name="KeyEnum"></typeparam>
		/// <param name="key">
		/// <returns></returns>
		public string getConfigData<keyenum>(KeyEnum key)
		{
			DisplayAttribute keyAttr = typeof(KeyEnum).GetMember(key.ToString())
											.First()
											.GetCustomAttribute<displayattribute>();

			SectionNames sectionName = (SectionNames)Enum.Parse(typeof(SectionNames), typeof(KeyEnum).Name);

			return _ConfigData[sectionName][key.ToString()].ToString();
		}

		/// <summary>
		/// Ini file을 저장하기 위한 함수
		/// </summary>
		public void SaveConfigData()
		{
			for (int i = 0; i < _ConfigData.Count; i++)
			{
				for (int j = 0; j < _ConfigData.ElementAt(i).Value.Count; j++)
				{
					string sectionname = _ConfigData.ElementAt(i).Key.ToString();


					DisplayAttribute keyAttr = typeof(SectionNames).GetMember(sectionname)
														.First()
														.GetCustomAttribute<displayattribute>();

					string section = keyAttr.Name;
					string key = _ConfigData.ElementAt(i).Value.ElementAt(j).Key.ToString();
					string value = _ConfigData.ElementAt(i).Value.ElementAt(j).Value.ToString();
					IniWriteValue(
						section,    //section
						key,        //key
						value       //value
						);
				}
			}
		}

		/// <summary>
		/// Ini file을 읽기 위한 함수
		/// </summary>
		public void ReadConfigData()
		{
			foreach (string strSectionName in Enum.GetNames(typeof(SectionNames)))
			{
				SectionNames sectionName = (SectionNames)Enum.Parse(typeof(SectionNames), strSectionName);

				DisplayAttribute sectionAttr = typeof(SectionNames).GetMember(strSectionName)
													  .First()
													  .GetCustomAttribute<displayattribute>();

				if (!_ConfigData.ContainsKey(sectionName))
					_ConfigData.Add(sectionName, new Dictionary<string, object="">());

				Type keyType = Type.GetType(string.Format("ConfigManager.{0}", sectionName));

				foreach (string strKeyName in Enum.GetNames(keyType))
				{
					DisplayAttribute keyAttr = keyType.GetMember(strKeyName)
													  .First()
													  .GetCustomAttribute<displayattribute>();

					string value = IniReadValue(sectionAttr.Name, strKeyName);

					if (!_ConfigData[sectionName].ContainsKey(strKeyName)) // 키가 없는 경우 추가
					{
						if (!string.IsNullOrEmpty(value)) // ini File에 값이 있다면 ini File 값 입력
							_ConfigData[sectionName].Add(strKeyName, value);
						else // ini File에 값이 없다면 Default 값 입력
							_ConfigData[sectionName].Add(strKeyName, keyAttr.Description);
					}
					else // 키가 있는 경우 값 입력
					{
						if (!string.IsNullOrEmpty(value)) // ini File에 값이 있다면 ini File 값 입력
							_ConfigData[sectionName][strKeyName] = value;
						else // ini File에 값이 없다면 Default 값 입력
							_ConfigData[sectionName][strKeyName] = keyAttr.Description;
					}
				}
			}
		}
	}
}

//이렇게 class를 만들었다. 실질적으로 사용하는 방법은 이렇다
using ConfigManager;

ConfigData _config = new ConfigData();
// ini file read
_config.ReadConfigData();

// 각 key에서 값 가져오기
string address = _config.getConfigData<database_section>(Database_Section.Address);
string id = _config.getConfigData<database_section>(Database_Section.Id);
string name = _config.getConfigData<user_section>(User_Section.Age);
string age = _config.getConfigData<user_section>(User_Section.Name);

// 각 key에 값 할당
// 값을 셋팅만 하지 실질적으로 ini file에 쓰기 위해선 save를 해줘야한다.
string address_change = "127.0.0.1";
string id_change = "server";
string name_change = "시나공공";
string age_change = "28"; // 늙었다...

_config.SetConfigData<database_section>(Database_Section.Address, address_change);
_config.SetConfigData<database_section>(Database_Section.Id, id_change);
_config.SetConfigData<User_Section>(User_Section.Name, name_change);
_config.SetConfigData<User_Section>(User_Section.Age, age_change);

// ini file save
_config.SaveConfigData();


구조적으로, 프로그래밍 적으로 좋은 방법인지 아닌지는 객관적으로 잘 모르겠으나, 내 나름대로 쓰기에 나쁘지 않다고 생각하여 공유해본다.

조언 및 충고 해주시면 너무너무 감사하겠습니다.

'Programming > C#' 카테고리의 다른 글

[C#] 네이버 자동 로그인 / 카페 글쓰기 API  (899) 2019.11.01
[C#]Log4Net 사용법  (587) 2017.01.03
[C#]Snappy 사용법  (143) 2016.12.30
[C#]DataTable Sorting 간단 사용법  (971) 2016.12.30

log4net 사용 관련 참고 사이트 : http://hind.pe.kr/1199

log4net 설정 관련 참고 사이트 : http://egloos.zum.com/empty79/v/2956254


C#에서 logging을 할때 손쉽고 편하게 사용할 수 있는 라이브러리를 소개하려한다.

Log4Net이라는 라이브러리 이다.

이 라이브러리는 log4j라는 java라이브러리에서 따온것이라고 알고있다.


log4net에 대한 자세한 내용은 홈페이지를 참조 바랍니다.

https://logging.apache.org/log4net/


1. 사용 방법

 - Nuget으로 Log4Net을 검색하면 아파치 log4net이 제일 상위에 나타난다


 "설치"버튼을 클릭하면 사용준비 끝.


2. 설정 파일

 - 사실 설정 파일에 대해 정확하게 알지 못한다. 일단 구글링을 통해 알아본 기본적인 틀을 확인해보자.

   응용 프로그램의 경우 app.config 파일에 설정 내용을 입력 하지만 나는 asp.net에서 로깅을 하려하기 때문에

   별도의 LogConfig.xml 파일을 생성하였다. (web.config에 하면 되려나)

<log4net>
  <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
    <file value="C:\Log\"/>
    <appendtofile value="true"/>
    <datepattern value="yyyy-MM-dd'_Log.log'"/>
    <staticlogfilename value="false"/>
    <rollingstyle value="Date"/>
    <layout type="log4net.Layout.PatternLayout">
      <conversionpattern value="%d [%t] %-5p - %m%n"/>
    </layout>
  </appender>
  <root>
    <level value="ALL"/>
    <appender-ref ref="RollingFile"/>
  </root>
</log4net>

<appender> 태그는 로그를 어떤식으로 남길 것인지에 대한 큰 틀이라고 보면된다.

appender 태그 안에서 파일을 어느곳에 만들것인지 파일이름을 어떻게 할것인지 로그 패턴을 어떻게 할것인지를 정할 수 있다.

<root> 태그는 기본 logger 설정을 하며, level 태그는 ALL, Warn, 등등 으로 나뉘며 해당 레벨 이상의 로그만 로그파일에 쓰길 원할경우 설정한다.

appender-ref 태그에는 appender의 name을 입력하여 연결을 해준다.


3. 소스

//using 해준다.
using log4net;
using log4net.Config;

//로깅할 곳에 로그 변수 추가
private ILog _logger = null;
public readonly ILog Logger
{
  get { return _Logger; }
}

string strDirPath;
strDirPath = @"C:\Log";
DirectoryInfo dirInfo = new DirectoryInfo(strDirPath);
//log4net에서 폴더를 만들어주는지 안만들어주는지 몰라서.. 일단 혹시나해서 만듦

if (dirInfo.Exists == false) { if (dirInfo != null) dirInfo.Create(); } //LogConfig.xml 경로를 설정해준다. string AppPath = AppDomain.CurrentDomain.BaseDirectory; //log4net.config 하위의 xmlconfigurator에 설정 파일을 등록한다. XmlConfigurator.Configure(new System.IO.FileInfo(AppPath + @"bin\LogConfig.xml")); //설정파일의 appender의 name에 아까 입력한 RollingFile을 입력하여 logger를 추가한다. _logger = log4net.LogManager.GetLogger("RollingFile"); // ----------- 이렇게 해서 기본적인 변수 선언과 사용준비는 끝. // 사용 //error if(_logger.IsErrorEnabled) _logger.Error("Error!!!!!!!!!!!"); if(_logger.IsDebugEnabled) _logger.Debug("Debug!!!!!!!!!!!"); if(_logger.IsInfoEnabled) _logger.Info("Info!!!!!!!!!!!");

로그를 남길때 수준을 정할 수 있다.

Error, Fatal, Warn, Info, Debug


이렇게 해서 로그 파일을 확인해 보면 아래와 같이 로그가 남는 것을 확인 할 수 있다.



*log4net을 사용하다 보면 남겨야 하는 로그의 성격이 다른 것들이 있다. 이럴경우 로그파일을 나눠서 로그를 남기고 싶을 경우가 있다.

이럴 경우에는 설정파일을 살짝 바꿔서 사용하면된다.


아래와 같이 appender를 추가하고 logger를 추가해주면 끝. 알기쉽게 이름을 잘 지어주면 끝.

눈치가 빠른 사람들은 소스에서 어떻게 사용하는지도 대충은 파악했을 것이다.


<log4net> <appender name="Common" type="log4net.Appender.RollingFileAppender"> <file value="C:\Log\" /> <appendToFile value="true" /> <datePattern value="yyyy-MM-dd'_Common.log'" /> <staticLogFileName value="false" /> <rollingStyle value="Date" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%d [%t] %-5p - %m%n" /> </layout> </appender> <appender name="Socket" type="log4net.Appender.RollingFileAppender"> <file value="C:\Log\" /> <appendToFile value="true" /> <datePattern value="yyyy-MM-dd'_Socket.log'" /> <staticLogFileName value="false" /> <rollingStyle value="Date" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%d [%t] %-5p - %m%n" /> </layout> </appender> <appender name="Database" type="log4net.Appender.RollingFileAppender"> <file value="C:\Log\" /> <appendToFile value="true" /> <datePattern value="yyyy-MM-dd'_Database.log'" /> <staticLogFileName value="false" /> <rollingStyle value="Date" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%d [%t] %-5p - %m%n" /> </layout> </appender> <root> <level value="ALL" /> </root> <logger name="Common"> <level value="ALL" /> <appender-ref ref="Common" /> </logger> <logger name="Socket"> <level value="ALL" /> <appender-ref ref="Socket" /> </logger> <logger name="Database"> <level value="ALL" /> <appender-ref ref="Database" /> </logger> </log4net>

여러 파일에 로그 남기기 소스는 첨부파일을 열어보자

LogManager.cs



'Programming > C#' 카테고리의 다른 글

[C#] 네이버 자동 로그인 / 카페 글쓰기 API  (899) 2019.11.01
[C#]ini file 사용법 & 소스 공유  (540) 2017.01.03
[C#]Snappy 사용법  (143) 2016.12.30
[C#]DataTable Sorting 간단 사용법  (971) 2016.12.30

+ Recent posts