'Programmings'에 해당되는 글 51건

  1. 2010.04.27 warning C4995: 'xxx' : name was marked as #pragma deprecated
  2. 2010.04.27 "응용 프로그램 구성이 올바르지 않기 때문에..." 해결 방안
  3. 2010.02.11 DLL에서 WndProc 메시지 처리하는 방법
  4. 2009.12.18 connect()
  5. 2009.12.16 listen() and accept()
  6. 2009.12.15 bind()
  7. 2009.12.15 socket()
  8. 2009.10.09 아스키코드(ASCII CODE)와 유티코드(UNICODE)
  9. 2009.07.16 GetLastError()
  10. 2009.06.23 WinIO
2010. 4. 27. 14:30

warning C4995: 'xxx' : name was marked as #pragma deprecated



컴파일을 하다보면 "warning C4995: 'xxx': name was marked as #pragma deprecated" 와 같은 warning 메시지를 볼 때가 있다.

MSDN에 이렇게 나와있다.

Error Message

'function': name was marked as #pragma deprecated

The compiler encountered a function that was marked with pragma deprecated. The function may no longer be supported in a future release. You can turn this warning off with the warning pragma (example below).

더 이상  지원되지 않을 수 있는 함수이다.
warning을 보고 싶지 않으면 pargma를 사용해라.

#pragma warning(disable:4995)

위와 같은 처리를 하게 되면, 빌드 시에 warning 메시지는 나오지 않을 것이다.
하지만, 더 이상 지원이 되지 않는 함수라는 것은 뭔가 문제가 있기 때문에 그럴 것이다.

위의 경고를 내는 함수들은 버퍼 오버플로우 혹은 보안적으로 문제를 가질 수 있는 함수들이다. 그렇기 때문에 위의 경고 메시지를 낼 만한 함수의 사용을 자제하고, 개선 된 함수를 사용하는것이 방법이 될 것이다.

아래의 링크가 그 위의 문제를 해결하는데 도움을 줄 것이다.
http://msdn.microsoft.com/en-us/library/8ef0s5kh(VS.80).aspx
http://www.chaos3d.net/zboard/view.php?id=cs_public&no=17


2010. 4. 27. 13:39

"응용 프로그램 구성이 올바르지 않기 때문에..." 해결 방안



예전에 http://todayis.tistory.com/148 에 "응용 프로그램 구성이 올바르지 않기 때문에 이 응용 프로그램을 시작하지 못했습니다. 이 문제를 해결하려면 응용 프로그램을 다시 설치하십시오." 라는 주제에 대해서 이야기 한 적이 있었다.

왠만한 경우 'vcredist_x86.exe' 파일을 설치함으로 해결 되었던 것으로 기억이 되는데.. 왠지 이번에 만든 프로그램의 경우 전혀 먹히질 않는다.. -_-;

http://kldp.org/node/95859 의 대글을 보니 이런 부분이 있다.

좀 더 정확히 얘기하면, Side-by-Side Assembly가 없어서 발생하는 문제이죠.

Visual Studio 2005부터는 공용 DLL을 System32라는 곳에 몰아서 보관하지 않고, WinSxS라는 공용 어셈블리 저장공간에 별도로 저장하게 됩니다. 이렇게 저장하는 이유는 버전 충돌을 막기 위해서이고요.

이러한 공용 어셈블리의 위치 정보는 모듈의 Menifest에 저장되어서 참조되게 됩니다.

그래서 사용하는 DLL을 단순히 복사하셔서는 안되고요. cynicjj님 말씀처럼 재배포 패키지를 이용해서 공용 DLL들을 설치하셔야 합니다. (Menifest는 Windows XP이상에서만 사용할 수 있기 때문에, Windows2000에서는 그냥 System32나 어플리케이션이 있는 폴더에 DLL들을 같이 복사해 주셔야 하고요.)

참고로 그런에 에러가 나왔을때, Dependency Walker 프로그램을 이용하면, 무슨 DLL이 없는지 쉽게 확인하실 수 있습니다.


이런 비슷한 내용을 본적이 있는데..
http://www.serious-code.net/moin.cgi/RedistributingVisualCppRunTimeLibrary

위의 사이트를 보니
실행 파일 자체와 실행 파일에서 액세스하는 DLL들에 대한 manifest 파일들을 private assembly로서 같이 배포하면, 에러를 피할 수 있다. 

라고 한다. 

위의 문제를 해결하기 위해서 "C:\Program Files\Microsoft Visual Studio 8\VC\redist\x86" 를 찾아보면 "Microsoft.VC80.CRT" 와 "Microsoft.VC80.MFC" 폴더가 존재한다.
Win32API의 경우 "Microsoft.VC80.CRT", MFC의 경우 "Microsoft.VC80.MFC" 디렉토리 내의 파일들과 함께 실행파일을 제공하면 될 듯 하다.

실행파일에 대한 manifest 파일도 필요하다고 하는데.. 그 파일 없이도 일단 실행이 되는것은 같은데..
참고로 실행파일에 대한 manifest 파일은 프로젝트 관련 디렉토리 내의 Debug 혹은 Release 디렉토리 내에 존재한다.

2010. 2. 11. 18:04

DLL에서 WndProc 메시지 처리하는 방법



응용 프로그램에서 특정 DLL을 사용할 때, 해당하는 DLL에서 윈도우즈 메시지를 처리해야 할 경우가 있다.
한참을 고민했네.. 메시지를 후킹해서 필요한 녀석만 처리를 하게끔 해야하나?? -_-;

의외로 간단한 방법이 있었다.

그것을 '서브클래싱(SubClassing)' 이라고 부르는 것 같다.

DLL의 초기화 하는 적절한 부분에 다음과 같이 서브클래싱을 해준다.
본인의 경우 다음과 같이...

WNDPROC oldProc;
...

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
...
switch(msg)
{
...
}
return CallWindowProc(oldProc, hwnd, msg, wp, lp);
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
...
switch(fdwReason)
{
case DLL_PROCESS_ATTACH :
...
oldProc = (WNDPROC)SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG)WndProc);
}
}

SetWindowLongPtr() 함수를 통해 메시지를 처리할 윈도우 프로시저를 기존의 응용프로그램의 것에서 DLL의
것으로 옮긴다.
DLL 내부에 만들어 놓은 윈도우 프로시저(WndProc)에서 처리하고자 하는 메시지만 처리하도록 하고 그 외의 것은
CallWindowProc()함수를 통해 기존의 응용프로그램의 윈도우 프로시저가 처리하도록 한다.



2009. 12. 18. 09:30

connect()



TCP 기반의 클라이언트 프로그램의 구현순서는 간단하다
  1. 소켓 생성
  2. 연결 요청
  3. 데이터 송수신
  4. 연결 종료
TCP 기반의 서버 프로그램과 비교해 볼 때 소켓 생성, 데이터 송수신, 연결 종료는 공통되는 부분이고, 다른 부분은 단지 '연결 요청'이다.

서버 프로그램에서 연결 요청 대기를 하고 있을 때 비로소 클라이언트 프로그램에서는 연결 요청을 할 수 있는 것이다. 연결 요청은 'connect()' 함수를 사용한다.

#include <sys/types.h>
#include <sys/socket.h>

int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);

  • sockfd - 통신을 위해 생성한 소켓의 파일 디스크립터
  • serv_addr - 연결 요청을 할 서버의 주소정보를 가진 구조체 변수의 포인터
  • addrlen - 연결 요청을 할 서버의 주소정보를 가진 구조체 변수의 크기

connect() 예제
...
int sock;
struct sockaddr_in serv_addr;
...
// 클라이언트 소켓 생성
sock = socket(PF_INET, SOCK_STREAM, 0);       
...
// connect()로 연결 요청
if(connect(sock, (struct sockaddr* ) &serv_addr, sizeof(serv_addr)) == -1)
...








'Programmings > TCP/IP socket programming' 카테고리의 다른 글

listen() and accept()  (0) 2009.12.16
bind()  (0) 2009.12.15
socket()  (0) 2009.12.15
2009. 12. 16. 09:30

listen() and accept()



socket() 함수를 통해 통신을 위한 소켓을 생성하고, bind() 함수를 통해 소켓에 주소를 할당 한 다음 해야할 일은 어디선가로 부터의 통신 연결 요청에 대해 대기하고 있어야 한다.
이 때 연결 요청 대기상태로 만들어주는 함수가 'listen()' 함수이다.

#include <sys/types.h>

int listen (int s, int backlog);

s - 연결 요청을 받아들일 소켓의 파일 디스크립터(서버 소켓)
backlog - 연결 요청 대기 큐의 크기
리턴값 - 성공 시 0, 실패 시 -1을 리턴

listen() 함수는 SOCK_STREAM과 SOCK_SEQPACKET 에만 사용된다.


실제 네트워크 상에서 소켓을 이용한 통신이 이루어지는 순서를 보자.

'서버측'의 프로그램은 socket() 함수를 통해 통신을 위한 소켓을 하나 생성한다. 이는 클라이언트의 연결을 받아들이기 위한 용도로 사용된다. 주소정보를 sockaddr_in 구조체에 채워 넣은 후 bind() 함수를 사용해 소켓에 서버측 프로그램의 주소정보를 할당한다. 그리고 클라이언트의 연결 요청이 있기 전까지 서버측의 프로그램은 listen() 함수를 통해 응답 요청에 대한 대기상태로 들어간다.

'클라이언트측'의 프로그램은 socket() 함수를 통해 통신을 위한 소켓을 생성하고, connect() 함수를 통해 서버측 프로그램에 연결을 요청한다. 이러한 클라이언트 프로그램의 요청에 대해 서버측 프로그램은 backlog 크기의 연결 요청 대기 큐에 클라이언트측 프로그램의 요청을 밀어넣고, accept() 함수를 사용하여 클라이언트측 프로그램과 연결을 하고 통신을 하게 된다.
backlog는 최대 접속할 수 있는 클라이언트의 수를 의미한다. backlog를 5로 설정했을 때 연결 요청 대기 큐에는 5개 까지의 클라이언트 프로그램의 요청이 들어갈 수 있다.

이때 서버측 프로그램은 새로이 소켓을 하나 생성하게 된다.
처음 만들어진 소켓은 통신을 위해 클라이언트 측의 요청을 대기하기 위한 용도의 소켓이고, accept() 시 생성되는 소켓은 클라이언트와 실제 데이터를 주고 받는 통신을 하기 위해 사용되는 용도의 소켓이다.

다음은 'accept()' 함수의 프로토타입이다.

#include <sys/types.h>
#include <sys/socket.h>

int accept (int s, struct sockaddr *addr, int *addrlen);

s - 연결 요청을 받아들일 소켓의 파일 디스크립터(서버 소켓)
addr - 연결 요청을 수락할 클라이언트측 프로그램의 주소정보를 가진 변수의 포인터
addrlen - 두번째 인자 addr 포인터가 가리키는 구조체의 크기
리턴값 - 성공 시 클라이언트와 데이터를 주고받기 위해 사용될 소켓의 파일 디스크립터, 실패 시 -1

accept() 함수는 연결지향 소켓타입 (SOCK_STREAM, SOCK_SEQPACKET, SOCK_RDM)에 사용된다.

'Programmings > TCP/IP socket programming' 카테고리의 다른 글

connect()  (0) 2009.12.18
bind()  (0) 2009.12.15
socket()  (0) 2009.12.15
2009. 12. 15. 10:30

bind()



통신을 위한 소켓을 생성하고나면, 소켓에 주소를 할당해야 한다. 이때 사용하는 함수가 'bind()' 이다.

#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *myaddr, int addrlen);

  • sockfd - 주소를 할당하고자 하는 소켓의 파일 디스크립터
  • myaddr - 할당하고자 하는 주소정보를 가지고 있는 구조체 변수의 포인터
  • addrlen - 인자로 전달되는 주소정보 구조체의 길이
  • 리턴값 - 성공 시 0, 실패 시 -1 을 리턴

두 번째 인자 myaddr는 struct sockaddr* 형이지만, 실제는 sockaddr_in을 형변환 한 것이다.
sockaddr_in을 살펴보면...

struct sockaddr_in {
    sa_family_t     sin_family;        // 소켓 타입
    uint16_t           sin_port;          // 연결에 사용되는 포트 번호
    struct in_addr  sin_addr;         // 연결을 받아들일 주소
    char               sin_zero[8];    // 사용되지 않음
}

struct in_addr {
    uint32_t    s_addr;                  // IPv4 인터넷 주소
}


소켓 생성을 해서 소켓에 주소를 할당하는 bind() 까지의 예

...   
int serv_sock;
char *serv_ip = "127.0.0.1";
char *serv_port = "9190";
struct sockaddr_in serv_addr;

serv_sock = socket(PF_INET, SOCK_STREAM, 0);        /* 소켓 생성 */
if(serv_sock == -1)
// error 처리..
 
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;                               // 소켓 타입
serv_addr.sin_addr.s_addr = inet_addr(serv_ip);         // IP 주소
serv_addr.sin_port = htons(atoi(serv_port));               // 포트 번호 

if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)     /* 소켓에 주소 할당 */
// error 처리..
...


추가 #1.
소켓에 사용되는 모든 데이터는 Big Endian으로 동작한다. 일반적으로 사용하는 기계가 Little Endian 인경우가 대부분이고, 혹시나 모르기 때문에 소켓에 사용되는 데이터(IP 주소, 포트 번호 등)은 네트워크 바이트 순서(Big Endian)으로 변경해 주어야 한다.
이 때 사용되는 함수가 htons(), ntohs(), htonl(), ntohl() 이다.
h - host byte
n - network byte
l - long (32bit)
s - short (16bir)
htons()라고 한다면 short형의 host byte를 short형의 network byte로 변경해준다는 의미이다.
[참고] Endian에 대해서는 이곳을 참고!!!

추가 #2.
IP 주소를 보면 127.0.0.1 과 같은 형태인 것을 볼 수 있다. 하지만 실제 사용될 때에는 unsigned long 형으로 표현이 된다.
127.0.0.1 과 같은 형태를 'Dotted-Decimal Notation'이라고 부르며, 이는 위의 예제 소스에서 확인 할 수 있듯 문자열이다. 이를 unsigned long 형으로 바꾸어주는 함수가 'inet_addr()' 이다.

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

unsigned long inet_addr (const char* string);


추가 #3.
위의 예제 소스를 보면 IP 주소를 직접 입력했다. 하지만 직접 IP 주소를 입력할 경우 다른 기계에서 동작을 할 수 없게 된다. 각 기계마다 IP 주소가 동일한 것이 아니기에...
이 때 'INADDR_ANY' 라는 상수를 통해서 현재 시스템의 IP를 자동으로 찾아서 할당해 주는 방법을 사용할 수 있다.

...   
char *serv_port = "9190";
struct sockaddr_in serv_addr;
...
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;                                         // 소켓 타입
serv_addr.sin_addr.s_addr = inet_addr(INADDR_ANY);         // IP 주소
serv_addr.sin_port = htons(atoi(serv_port));                          // 포트 번호 
...


 

'Programmings > TCP/IP socket programming' 카테고리의 다른 글

connect()  (0) 2009.12.18
listen() and accept()  (0) 2009.12.16
socket()  (0) 2009.12.15
2009. 12. 15. 10:02

socket()



통신을 하기 위해선 소켓을 생성해야 한다. 소켓은 통신을 위한 엔드포인트 정도로 생각하면 될 듯 하다.
소켓을 생성하기 위해서 socket() 이란 함수를 사용한다. socket()를 통해 시스템 내부적으론 소켓을 생성하고, 생성된 소켓을 조작하기 위한 파일 디스크립터를 리턴한다.

#include <sys/types.h>
#include <sys/socket.h>

int socket (int domain, int type, int protocol);

  • domain - 통신을 하기 위해 사용할 프로토콜 체계(Protocol Family)
  • type - 소켓에서 사용하게 될 데이터 전송 타입
  • protocol - 통신을 하기 위한 프로토콜
  • 리턴값 - 성공 시 int형 파일 디스크립터, 실패 시 -1 리턴

일반적으로 아래와 같이 TCP socket 또는 UDP socket 를 생성하여 사용하게 된다.
...
int tcp_sock, udp_sock;
tcp_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
udp_sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
...

리턴값으로 파일 디스크립터를 받게 되는데, 리눅스에서 현재 사용하고 있는 디스크립터(0, 1, 2 이외의 )가 있다면 그 다음 숫자부터 차례로 디스크립더를 리턴받게 된다.

'Programmings > TCP/IP socket programming' 카테고리의 다른 글

connect()  (0) 2009.12.18
listen() and accept()  (0) 2009.12.16
bind()  (0) 2009.12.15
2009. 10. 9. 11:37

아스키코드(ASCII CODE)와 유티코드(UNICODE)



현재 운영체제가 표현하는 대표적인 문자셋(Character Sets)으로는 아스키코드(ASCII Code)유니코드(UNICode)가 있다.
여기서는 윈도우즈 운영체제를 기준으로 한다.

아스키코드
아스키코드는 미국에서 정의하고 있는 표준이다.
알파벳 26개와 확장 문자를 포함하여 총 256개를 넘지 않는 문자가 존재한다. 이는 1 바이트를 가지고 충분히 표현할 수 있기에 1 바이트의 char형을 사용해서 표현을 할 수 있다.
아스키코드에 해당하는 문자들은 http://todayis.tistory.com/191 에서 확인할 수 있다.

문제는 영어권이 아닌 다른 국가의 언어를 표현하는데 1 바이트로는 무리가 있다는 것이다. 한글의 경우만 보아도 한글의 글자 하나하나에 값을 지정해 주어야 한다. 한글은 그렇다 치더라도 중국어는 어떻할 것인가??
그래서 등장한 것이 유니코드이다.

유니코드
문자를 표현하는데 2 바이트를 사용해 총 65,536개의 문자를 표현할 수 있도록 정의해 놓은 문자셋이다.
전 세계의 모든문자를 컴퓨터에서 일관되게 표현하고 다룰 수 있도록 설계되었다.


문자를 표현하는데 아스키코드는 1 바이트, 유니코드는 2 바이트를 사용하기 때문에, 동시에 이 둘을 모두 사용하기에는 운영체제가 혼란을 겪을 것이다. 컴퓨터(운영체제)는 단순하기 때문에 하나로 통일을 해 주어야 일을 제대로 할수 있다고 하던데.. ^^;

해결 방법으로 문자셋마다 처리하는 표준을 정의했다.
아스키코드를 처리하기 위한 표준으로 SBCS(Single Byte Character Set)라는 녀석을 정의했고, 유니코드를 처리하기 위해서는 WBCS(Wide Byte Character Set)라는 녀석을 정의해 둔 것이다.
중간에 경우에 따라 1 바이트, 2 바이트를 처리하도록 정의된 MBCS(Multi Byte Character Set)이란 녀석도 있다.

아스키코드 기반으로 문자열을 처리하기 위한 프로그램을 작성할 때에는 이전에 C언어를 배울 때 습관적으로 사용하던 문자 처리방식을 습관적으로 사용하던 방식을 그대로 사용하면 된다.
char, "string", strlen()...

유니코드 기반으로 문자열을 처리하기 위해서는 이와는 약간 다른 부분들이 존재한다.
wchar_t, L"string", wcslen()

위와 같이 자료형, 문자열 표현방법, 함수등이 유니코드 처리에 맞게 재설정 되어있다. 이러한 내용은 윈도우즈에서 제공하는 헤더파일(windows.h, windef.h, winnt.h)을 살펴보면 알 수 있다.

일반적으로 유니코드와 아스키코드를 동시에 지원하기 위해 아래와 같은 방법을 tchar.h 라는 헤더파일에 정의해 두었다. tchar.h 는 windows.h에 포함되지 않기 때문에 명시적으로 추가해 주어야 한다.
자료형의 경우..
#ifdef UNICODE
    typedef  WCHAR     TCHAR;
    typedef  LPWSTR    LPTSTR;
    typedef  LPCWSTR  LPCTSTR;
#else
    typedef  CHAR        TCHAR;
    typedef  LPSTR       LPTSTR;
    typedef  LPCSTR     LPCTSTR;
#endif

함수의 경우..
#ifdef _UNICODE
    #define  _tprintf    wprintf
...
#else
    #define  _tprintf    printf
...
#endif

위와 같이 정의가 되어 있기 때문에 UNICODE(_UNICODE) 의 정의 유무에 따라 아스키코드 또는 유니코드 방식으로 컴파일을 할 수가 있다.
tchar.h 헤더파일을 살펴보는 것도 큰 도움이 될 듯 하다.

참고로 본인이 사용하고 있는 Visual Studio 2005는 유니코드로 미리 정의가 되어 있다.
메뉴에서 [project] -> [CommandPrompt Properties...] 를 선택하면 확인할 수 있다. Alt+P 키를 두 번 눌러도 됨

미리 정의된 유니코드로 작업하기 싫다고 하면, 체크박스로 된 Inherit from parent or project defaults의 체크를 해제하면 된다. 이게 싫다면, 소스코드 내에서 #undef 를 사용해 미리 정의된 UNICODE를 무효화 시키면 된다.
#undef _UNICODE
#undef UNICODE
...

2009. 7. 16. 10:00

GetLastError()



Windows 시스템에서 함수를 사용할 때 이녀석이 제대로 실행되지 않고 에러를 발생시킬 경우가 허다하다.
사실.. 프로그램을 짜면서 에러 하나 없이 순탄하게 짜는 사람이 있을까마는..
적어도 난 한번도 없다면서.. -_-;

그럼 에러가 발생했다 라는것 말고 무슨 이유로 에러가 발생했다는 것을 아는 방법이 없을까?
수정을 하려 해도 어떤 이유에서 에러가 발생하는 지 알아야 수정을 할것인데...
그래서 나온 것이 바로 GetLastError() 라는 함수이다. GetLastError()를 에러가 발생하는 함수 바로 뒤에 호출하게 되면 오류의 원인에 대한 에러코드를 얻을 수가 있다.

DWORD GetLastError(void);

MSDN을 보게 되면 수많은 에러코드의 의미가 나와있다.
오호라.. MSDN을 보니 대충 0 ~ 15999가지의 에러에 대한 내용이 있는 모양이다. 열라 많다 -_-;;

간단하게 사용법을 보면은..
...
HANDLE hFile = CreateFile(...);
if(hFile == INVALID_HANDLE_VALUE)
{
    printf("Error Code : %d \n", GetLastError());
}
...

위와 같이 사용을 하게 될 경우, CreateFile()의 결과가 잘못될 경우, if문의 GetLastError()의 반환값을 보구선 어떤 이유로 원치 않는 결과가 나왔는지를 확인 할 수가 있다.


'Programmings > Windows Programming' 카테고리의 다른 글

DLL에서 WndProc 메시지 처리하는 방법  (0) 2010.02.11
아스키코드(ASCII CODE)와 유티코드(UNICODE)  (0) 2009.10.09
WinIO  (0) 2009.06.23
IME 입력모드 설정  (5) 2009.05.29
레지스트리(Registry)  (0) 2009.05.29
2009. 6. 23. 09:45

WinIO



WinIO 는 윈도우즈(9x, NT, 2000, XP 등) 보호모드상에서 포트나 메모리의 직접적인 접근을 가능하도록 Yariv Kaplan이라는 사람이 만들어 놓은 라이브러리로 오픈소스이다. WinIO는 드라이버의 소스까지 공개되어 있어 누구나 분석하거나 사용할 수 있고, http://www.internals.com 에 방문하게 되면 얻을 수 있다.
사용법이라든지 도움말, 그리고 간단한 예제 프로그램이 같이 있어 사용하는데 문제는 없을 듯 하다.


원하는 포트를 사용하여 어플리케이션과 디바이스 드라이버간의 통신을 위해 사용할 수 있다. 특시 60, 64 포트를 이용하여 키보드 제어를 위해서도 사용을 하기도 한다.

기타 WinIO 에 대한 자료는 google을 검색하면 수도 없이 나올 것이다.
필요한 부분은 알아서 자알 찾으면 될 듯.. ㅎ