본문 바로가기
C & C++/C & C++

[API] API 메시지 처리

by izen8 2011. 4. 25.
반응형

GetSystemMetrics(알고자 하는 정보)를 이용하면 현재 시스템의 설정내용을 알아낼 수 있습니다.

모두 정수 값으로 리턴합니다.

SM_CLEANBOOT: 부팅 모드

SM_CMOUSEBUTTONS: 마우스 버튼 개수

SM_CXFULLSCREEN: 풀 스크린일 때 x축 크기

SM_CXMIN: 윈도우의 x축 최소 크기

SM_CXSCREEN: 가로 해상도

SM_CXSIZE: X축 크기

SM_CYFULLSCREEN: 풀 스크린일 때 y축 크기

SM_CYMIN: 윈도우의 y축 최소 크기

SM_CYSCREEN: 세로 해상도

SM_CYSIZE: Y축 크기

SM_MOUSEPRESENT: 마우스 존재 여부

 

데이터를 포맷에 맞추어 유니코드 문자열로 저장하기

wsprintf(TCHAR 배열명, TEXT(“서식”), 출력대상);

출력대상을 서식에 맞추어 문자열로 배열에 출력해 줍니다.

뒤의 문자열 서식은 일반적인 printf와 같습니다.

ex)

int x = 10;

int y = 20;

2개의 데이터를 하나의 문자열로 만들기

TCHAR str[256] ;

wsprintf(str, TEXT("%d %d"), x, y) ;


 

예제) 현재 윈도우의 해상도와 마우스 존재여부 및 버튼 개수를 출력

 

마우스 메시지

1. 입력 방법

- 마우스 또는 키보드로부터 발생되는 이벤트는 디바이스 드라이버를 거치게 됩니다.

- 윈도우 프로그램에서 인식할 수 있는 메시지로 변환되어 전달됩니다.

- 윈도우 프로그램에서는 마우스나 키보드에 대한 이벤트 처리를 별도로 처리하는 것이 아니고 일반적인 메시지를 처리하는 방법과 동일하게 처리합니다.

 

2. 마우스로부터 메시지

마우스 입력 방법

버튼 클릭

 

SHIFT 또는 CTRL 과 함께 누르는 경우

더블 클릭

드래그

 

마우스 메시지(클라이언트 영역)

 

누름

놓음

더블클릭

드래그

왼쪽

WM_LBUTTONDOWN

WM_LBUTTONUP

WM_LBUTTONDBLCLK

WM_MOUSEMOVE

오른쪽

WM_RBUTTONDOWN

WM_RBUTTONUP

WM_RBUTTONDBLCLK

 

가운데

WM_MBUTTONDOWN

WM_MBUTTONUP

WM_MBUTTONDBLCLK

 

 

마우스 메시지의 부가 정보

- lParam

HIWORD(lParam): 마우스의 y좌표

LOWORD(lParam): 마우스의 x좌표

 

- wParam

설명

매크로 정수 값

MK_LBUTTON

마우스 왼쪽 버튼이 눌러져 있습니다.

1

MK_RBUTTON

마우스 오른쪽 버튼이 눌러져 있습니다.

2

MK_CONTROL

Ctrl 키가 눌러져 있습니다.

8

MK_SHIFT

Shift 키가 눌러져 있습니다.

4

MK_MBUTTON

마우스 중간 버튼이 눌러져 있습니다.

16

이 값들을 조합해서 여러 가지 마우스 메시지 처리를 할 수 있습니다.


 

 예제) 마우스 왼쪽 버튼과 다른 키를 같이 눌렀을 때의 처리

마우스 메시지가 발생할 때 lParam에는 좌표가 넘어갑니다.

좌표를 받는 방법은 2개의 정수 변수를 선언해서 받을 수 있고 하나의 POINTS 구조체를 이용해서 받을 수도 있습니다.

1) 2개의 정수를 변수를 이용한 경우

변수 = LOWORD(lParam); // x좌표

변수 = HIWORD(lParam); // y좌표

 

2) 1개의 POINT 구조체를 이용한 경우

POINTS 변수명 = MAKEPOINTS(lParam);

변수명.x x좌표 변수명.y y좌표

 

예제) 마우스로 왼쪽 버튼을 클릭한 자리의 좌표를 메시지 박스로 출력하기

더블 클릭 처리

기본적으로 더블 클릭 처리는 되지 않습니다.

WinMainwndclass.style CS_DBLCLKS를 추가해주어야만 사용이 가능해 집니다.

이 때 주의 할 점이 있습니다.

더블 클릭 메시지는 클릭 메시지가 발생한 후 발생하게 됩니다.

따라서 더블 클릭 메시지를 사용할 때는 마우스 클릭 메시지와 연관성 있게 작성해 주는 것이 좋습니다.

예제) 더블 클릭 처리

화면 무효화 하기
InvalidateRect(윈도우핸들, 사각영역, 무효화 옵션)
윈도우 핸들은 화면을 무효화 할 윈도우 핸들입니다.
사각영역은 RECT 구조체 주소 변수를 입력해주면 그 영역만을 대상으로 합니다.
NULL이면 윈도우 전체가 대상이 됩니다.
무효화 옵션은 TRUE이면 영역을 무효화하고 WM_PAINT메시지를 호출하고 FALSE이면 WM_PAINT 메시지만 호출합니다.
 
화면 크기 얻어오기
RECT 사각영역변수;
GetClientRect(윈도우핸들, &사각영역변수)
또는 WM_SIZE메시지에서 가로크기는 LOWORD(lParam) 세로크기는 HIWORD(lParam)

 

왼쪽 버튼이 눌러졌을 때 시작해서 드래그하면 그려지고 버튼을 뗄 때 그려지는 프로그램

3. 비 클라이언트 영역 마우스 메시지

1) 메시지

 

누름

놓음

더블클릭

왼쪽

WM_NCLBUTTONDOWN

WM_NCLBUTTONUP

WM_NCLBUTTONDBLCLK

오른쪽

WM_NCRBUTTONDOWN

WM_NCRBUTTONUP

WM_NCRBUTTONDBLCLK

가운데

WM_NCMBUTTONDOWN

WM_NCMBUTTONUP

WM_NCMBUTTONDBLCLK

마우스 이동: WM_NCMOUSEMOVE 발생

 

2) 부가 정보

wParam: 메시지가 발생한 위치

HTBORDER       HTBOTTOM       HTBOTTOMLEFT   HTBOTTOMRIGHT  HTCAPTION

HTCLIENT       HTCLOSE        HTERROR        HTGROWBOX      HTHELP

HTHSCROLL      HTLEFT         HTMENU         HTMAXBUTTON    HTMINBUTTON

HTNOWHERE      HTREDUCE       HTRIGHT        HTSIZE         HTSYSMENU

HTTOP          HTTOPLEFT      HTTOPRIGHT     HTTRANSPARENT  HTVSCROLL

HTZOOM

위의 값들이 전달됩니다.

이 에 따라 다른 처리를 할 수 있습니다.

 

lParam: 메시지가 발생한 스크린 좌표

 

3)WM_NCHITTEST 메시지

윈도우 전체 어디에서든지 발생하는 메시지

lParam에 스크린 좌표가 전달되며 wParam은 사용되지 않음

4. 마우스 휠 메시지 처리

휠 마우스 처리는 WM_MOUSEWHEEL 메시지가 처리합니다.

부가 정보

HIWORD(wParam): 휠 속도(120의 배수)

LOWORD(wParam): 눌러진 버튼

MK_CONTROL = 8: 컨트롤 키

MK_LBUTTON = 1: 왼쪽 버튼

MK_MBUTTON = 16: 가운데 버튼

MK_RBUTTON = 2: 오른쪽 버튼

MK_SHIFT = 4: SHIFT

HIWORD(lParam): Y좌표

LOWORD(lParam): X좌표

 

예제)

5. 마우스 캡쳐

마우스 메시지를 자신의 윈도우 영역 바깥에서 받고자 하는 경우 사용하는 방법입니다.

SetCapture(HWND hWnd): hWnd 윈도우가 마우스 동작을 계속해서 캡쳐

ReleaseCapture(): 마우스 동작 캡쳐를 해제

 

예제 작성)

베이지어 곡선을 만들고 왼쪽 버튼을 클릭한 상태에서 마우스를 움직이면 그 방향으로 곡선이 변경되는 예제

키보드 메시지

1. 키보드 메시지 종류

WM_CHAR: 문자 키를 누를 때

WM_KEYDOWN: 키보드 키를 누를 때

WM_KEYUP: 키보드 키를 놓을 때

WM_SYSCHAR: ALT와 문자 키를 누를 때

WM_SYSKEYDOWN: ALT와 키보드 키를 누를 때

WM_SYSKEYUP: ALT와 키보드 키를 놓을 때

 

키보드로부터 키 입력이 오면 키 값을 wParam에 저장 합니다.

lParam

하위 16비트(0-15) – 키 누름 반복 횟수

16-23비트: OEM 스캔코드 각 제조회사별 코드

24비트: 확장 키 값

25-28비트: 사용되지 않음

아래 3개는 메시지를 구분하기 위해 사용

29비트: ALT 키 누름 여부

30비트: 키 누름 상태

31비트: 동작 상태

2. WM_CHAR

키보드로부터 일반 문자 키를 누를 때 발생하는 메시지

눌러진 키를 메시지 박스를 이용해서 출력하는 예제 작성하기

3. WM_KEYDOWN

문자를 포함해서 키를 입력 받는 메시지

영문의 경우 대소문자 구별을 하지 못합니다.

받은 키 값을 wParam에 저장함

메시지

발생지점

VK_CANCEL

VK_BACK

VK_TAB

VK_RETURN

VK_SHIFT

VK_CONTROL

VK_ALT

VK_ESCAPE

VK_SPACE

VK_LEFT

VK_UP

VK_DOWN

VK_RIGHT

CtrlBreak

Backspace

Tab

Enter

Shift

Ctrl

Alt

Esc

SpaceBar

왼쪽 방향키

위쪽 방향키

아래쪽 방향키

오른쪽 방향키

방향키를 누를 때 이동하는 사각형

키보드의 어떤 키를 눌렀는지 판별해주는 예제

키보드 키 메시지의 발생 순서는 WM_KEYDOWN, WM_CHAR, WM_KEYUP의 순서입니다.

실제로 WinMain()에 있는 TraslateMessage 함수는 WM_KEYDOWN 메시지를 받아서 문자키이면 WM_CHAR 메시지를 발생시키는 함수입니다.

예제)메시지 발생 순서

WM_CREATE

윈도우가 생성될 때 한 번만 수행되는 메시지

윈도우 클래스 구조체를 메모리에 실제 할당할 때, CreatWindow() 호출시에 발생하는 메시지입니다.

lParam: CREATESTRUCT구조체에 윈도우 모양을 전달

 

WM_SIZE

윈도우의 크기가 변경될 때 호출되는 메시지

lParam: LOWORD값이 가로 크기가 되고 HIWORD값이 세로 크기가 됩니다.

wParam: 크기 변경이 발생한 이유가 전달됩니다.

플래그

SIZE_MZXHIDE

다른 윈도우가 최대화 되어 가려진 경우

SIZE_MAXIMIZED

최대화 된 경우

SIZE_MAXSHOW

다른 윈도우가 원래 크기로 복원되어 드러난 경우

SIZE_MINIMIZED

최소화 된 경우

SIZE_RESTORD

크기가 변경된 경우

 

WM_MOVE

윈도우가 이동할 때 발생하는 메시지입니다.

wParam은 사용하지 않으며 LOWORD lParam에는 윈도우의 x좌표가 HIWORD lParam에는 윈도우의 y좌표가 전달되게 됩니다.

int x,y;

TCHAR str[256];

        case WM_MOVE:

                       x = LOWORD(lParam);   

                       y = HIWORD(lParam);

                       wsprintf(str,TEXT("x좌표:%4d y좌표:%4d"),x,y);

                       MessageBox(hwnd,str,TEXT("화면이동"),MB_OK);

                       return 0;
WM_SETFOCUS

윈도우가 활성화 될 때 수행되는 메시지

wParam: 포커스를 잃어버린 윈도우 핸들로 널 일 수도 있음

lParam: 사용하지 않음

WM_KILLFOCUS

윈도우가 비 활성화 될 때 수행되는 메시지로 기존의 모든 메시지는 데드락 상태가 됩니다.

wParam: 포커스를 받은 윈도우 핸들로 널 일 수도 있음

lParam: 사용하지 않음

 

예제)

TIMER 메시지

키보드나 마우스는 프로그램 실행 중에 사용자로부터 입력되는 메시지입니다.

메시지는 이렇게 사용자의 동작으로부터 유발되는 것이 보통이지만 사용자의 동작과는 상관없이 발생하는 메시지도 있습니다.

타이머 메시지인 WM_TIMER를 들 수 있으며 이 메시지는 한번 지정해 놓기만 하면 일정한 시간간격을 두고 연속적으로 계속 발생합니다.

주기적으로 같은 동작을 반복해야 한다거나 여러 번 나누어 해야 할 일이 있을 때 이 메시지를 이용합니다.

타이머는 생성하면 주기적으로 WM_TIMER 메시지를 호출합니다.

 

1. WM_TIMER 메시지의 부가정보

wParam은 타이머의 번호를 전달합니다.

 

2. 타이머의 사용

1) 타이머의 생성

일반적으로 WM_CREATE 메시지에서 생성합니다.

static HANDLE 타이머변수;

타이머변수 = SetTimer(hWnd, 타이머 번호, 타이머 주기, 타이머가 호출할 함수)

타이머가 호출할 함수가 NULL이면 WM_TIMER를 발생시키고 호출할 메시지 처리 함수를 만들어 주면 그 함수를 호출하게 됩니다.

 

2) 타이머 해제

타이머도 자원이므로 사용이 종료되면 해제해야 합니다.

KillTimer(hWnd, 타이머 번호)

 

3. 메시지 발생

SendMessage(hWnd, 메시지, wParam, lParam);

PostMessage(hWnd, 메시지, wParam, lParam);

두 개의 함수는 메시지를 발생시키는 역할을 합니다.

SendMessage는 바로 실행하고 PostMessage는 시스템 큐에 메시지를 전달 한 후 시스템이 한가해 질 때 수행하게 됩니다.

 


 

예제) 타이머를 이용한 사각형 움직이기(WM_TIMER에서 처리)

 

예제) 별도의 메시지 처리 함수를 작성해서 처리하기

현재 시간 받아오기

SYSTEMTIME 변수;

GetLocalTime(&SYSTEMTIME 변수);

 

typedef struct _SYSTEMTIME

{

   WORD wYear;

   WORD wMonth;

   WORD wDayOfWeek;(일요일은 0)

   WORD wDay;

   WORD wHour;

   WORD wMinute;

   WORD wSecond;

   WORD wMilliseconds;

} SYSTEMTIME;

 

현재 화면 크기를 받아오는 2가지 방법

1) RECT구조체 변수를 생성합니다.

GetClientRect(윈도우핸들, RECT * 변수)

 

2) WM_SIZE 메시지에서 수형 변수를 2개 설정합니다.

lParam LOWORD값이 가로크기

wParam HIWORD값이 세로크기이므로 이 값을 변수에 저장하면 됩니다.

 

예제) 시계 만들기

예제) 오전오후에 요일까지 표시하는 시계

2개 이상의 타이머를 사용할 수 있습니다.

이 때는 타이머번호를 기준으로 분기를 하면 됩니다.

WM_TIMER 메시지에서는 wParam이 타이머 번호를 저장하고 있으므로 타이머 번호를 비교해서 호출하는 함수를 달리하거나 아니면 작업을 달리 작성하면 됩니다.

 

예제) 2개의 타이머 사용하기

WM_CLOSE

윈도우가 닫힐 때 발생하는 메시지

Alt + x를 누르면 종료되는 예제

int result;

        TCHAR code;

        switch (message)

        {

        case WM_SYSCHAR:

               code = (TCHAR)wParam;

               if (code == 'x' || code == 'X')

               {

                       result = MessageBox(hwnd, TEXT("응용프로그램을끝내시겠습니까?"),

                       TEXT("Close"),MB_ICONQUESTION | MB_YESNO);

                       if (result == IDYES)

                       {

                              SendMessage(hwnd,WM_CLOSE,0,0);

                       }

               }

               return 0;

        case WM_DESTROY:

               PostQuitMessage(0);

               break;

        }

프로그램이 종료될 때 발생하는 메시지로는 WM_CLOSE -> WM_DESTROY -> WM_QUIT 메시지 순으로 수행됩니다.

메시지 처리

1. 윈도우 메시지 처리 함수

윈도우 메시지 처리함수는 윈도우 클래스당 일반적으로 하나씩 배정됩니다.

동일한 윈도우 클래스로부터 생성된 윈도우들은 일반적으로 모두 같은 윈도우 메시지 처리함수를 공유해서 동작을 하게 됩니다.

윈도우 메시지 처리함수의 기본형은

LRESULT CALLBACK 함수명(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam)

첫 번째 인수는 이 메시지가 처리해야 하는 윈도우 핸들입니다.

어떤 윈도우의 메시지 인지를 구분하기 위하여 hwnd 인수가 필요합니다.

두 번째 인수는 처리해야 할 메시지입니다.

세 번째 인수는 메시지가 발생할 때 전달되는 wParam 값이며 네 번째 인수는 lParam 값입니다.

그래서 일반적인 메시지 처리함수의 형태는 아래와 같습니다.

 

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

{

        switch(message)

{

        case 처리할 메시지 명:

               메시지가 발생했을 때 처리 내용;

               return 0;

        }

        return(DefWindowProc(hwnd, message, wParam, lParam));

}

case 별로 메시지를 처리하고 그 외의 메시지는 DefWindowProc 함수에게 전달하여 디폴트 처리를 하게 됩니다.

2. 메시지의 종류

1) 큐 메시지

메시지 큐로 들어가는 메시지로 일반적으로 사용자의 입력으로부터 발생하는 메시지입니다.

큐 메시지는 발생한 순서대로 메시지 큐로 전달되어 윈도우 메시지 처리함수에 의해 처리됩니다.

 

2) 비 큐 메시지

윈도우에게 특정 사실을 알리거나 명령을 보내기 위한 메시지로 큐를 통하지 않고 바로 윈도우 메시지 처리 함수에게 전달되는 메시지로 대부분의 메시지는 비 큐 메시지입니다.

 

3. 메시지 루프

메시지 루프는 아래와 같은 원형을 일반적으로 가지며 WM_QUIT 메시지가 전달될 때까지 루프를 반복하게 됩니다.

while(GetMessage(&Message,0,0,0))

{

        TranslateMessage(&Message);

        DispatchMessage(&Message);

}

 

GetMessage는 메시지 큐에 대기중인 메시지를 꺼내 첫 번째 인수로 전달된 MSG 구조체에 복사한 후 메시지 큐에서 제거하고 TRUE를 리턴하며 WM_QUIT 메시지 경우 FALSE를 리턴하여 메시지 루프를 탈출하게 됩니다.

이 때 두 번째 인수에 윈도우 핸들을 주면 그 윈도우 핸들에 해당하는 메시지만 처리하며 NULL을 주면 현재 스레드에 속한 모든 윈도우 메시지를 처리하게 됩니다.

세 번째와 네 번째는 메시지의 범위 입니다.

GetMessage 함수에 의해 큐로부터 가져온 메시지는 WM_QUIT 메시지가 아닌 이상 TranslateMessage 함수로 전달되어 가상키 입력을 문자 입력(WM_CHAR)로 바꾸어 주며 가상키 입력이 아니라면 아무 처리도 하지 않게 됩니다.

DispatchMessage 함수는 메시지를 윈도우 메시지 처리 함수에 전달하여 처리하도록 해줍니다.

이 메시지 루프 안에서 직접 메시지를 처리할 수 도 있고 메시지를 제거할 수도 있습니다.

만일 특정 메시지를 처리하지 않기를 원한다면 아래와 같이 작성하면 됩니다.

 

while(GetMessage(&msg,0,0,0))

{

        TranslateMessage(&msg);

        If(msg.message != 메시지명)   DispatchMessage(&Message);

}

 

MFC에서 다이얼로그의 ENTER 문제나 ESC의 문제를 위와 같은 방법으로 해결하는 경우가 많습니다.

 

다이얼로그에서 ENTER ESC를 무시하고 할 때 PreTranslateMessage 함수를 호출한 후 아래와 같이 처리하면 2개의 키에 발생하는 메시지는 무시되어 버립니다.

PreTranslateMessage(MSG* pMsg)
{    if(pMsg->message==WM_KEYDOWN && pMsg->wParam==VK_ESCAPE)
        return TRUE;
    if(pMsg->message==WM_KEYDOWN && pMsg->wParam==VK_RETURN)
        return TRUE;

 

특정 메시지를 처리하고 싶다면 아래와 같이 작성합니다.

while(GetMessage(&msg,0,0,0))

{

        TranslateMessage(&msg);

        if(msg.message == 메시지명)

{

처리 내용;

}

else

{

        DispatchMessage(&Message);

}

}

예제) 마우스로 클릭한 자리에 사각형을 그리다가 엔터를 누르면 화면을 다시 그리는 메시지 처리

예제) ENTER 키 무시하기 (메시지 루프 수정)

4. PeekMessage

GetMessage 함수는 메시지를 메시지 큐에서 가져와서 메시지를 큐에서 제거하고 WM_QUIT이면 FALSE 나머지는 TRUE를 리턴하여 메시지를 처리하게 되는데 메시지 입력이 없으면 아무런 반응없이 기다리게 됩니다.

이에 대치 할 수 있는 함수로 PeekMessage 함수가 있습니다.

BOOL PeekMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax,

UINT wRemoveMsg);

첫 번째 인수는 메시지의 주소이고 두 번째 인수는 윈도우 핸들이며 세 번째와 네 번째는 역시 메시지의 범위이며 마지막은 PM_REMOVE를 주면 메시지를 제거하고 PM_NOREMOVE이면 제거하지 않습니다.

이 함수는 메시지가 유무에 상관없이 리턴하게 됩니다.

메시지가 있다면 TRUE를 리턴하고 없으면 FALSE를 리턴합니다.

이 함수를 이용하면 메시지가 없어서 쉬는 시간에 특정한 처리를 수행할 수 있습니다.

또한 특정한 작업 중에 다른 메시지가 발생했을 때 작업을 수행하게 할 수도 있습니다.

이 함수를 사용할 때는 주의해야 할 점이 있습니다.

일반적으로 메시지 루프를 무한 루프로 묶는데 묶은 후 반드시 break를 수행할 수 있도록 해주어야 한다는 점입니다.

예제1)

메시지가 없는 동안 원을 계속 그리는 예제(위의 예제에서 메시지 루프만 변경)

예제2)

왼쪽 버튼을 누를 때 원을 100개 그리고 오른쪽 버튼을 누를 때 사각형을 1개 그리는 예제를 작성해보면 아래와 같습니다.

이 예제에서는 왼쪽 버튼을 한 번 누르면 원이 100개가 그려지는 동안은 다른 메시지는 발생할 수 없습니다.

5. 2개의 키를 누른 경우 처리

WM_KEYDOWN 메시지는 하나의 키보드를 누를 때 발생하는 메시지입니다.

만일 두 개의 키보드를 눌렀다면 나중에 누른 키보드는 무시됩니다.

하나의 키를 누르고 있는 상태에서 다른 키를 눌렀다면 이전에 누른 키를 조사해서 메시지 처리를 해야 합니다,

이를 처리해주는 함수는 2가지가 있습니다.

SHORT GetKeyState(int nVirKey)

SHORT GetAsyncState(int nVirKey)

2개의 함수 모두 조사하고 싶은 가상 키 값을 인자로 받습니다.

일반 키가 눌러져 있다면 최상위 비트가 1로 설정되고 그렇지 않으면 0으로 설정됩니다.

토글 키의 경우에는 켜져 있다면 최하위 비트가 1이 되고 그렇지 않으면 0으로 설정됩니다.

어떤 키가 눌러져 있는지 알고 싶다면 이 함수의 리턴 값을 0x8000 &연산을 수행하여 결과가 0인가 아닌가를 확인해보면 알 수 있습니다.

GetKeyState는 메시지가 발생한 시점을 조사하고 GetAsyncState는 메시지가 처리될 시점을 조사하게 됩니다.

반응형

'C & C++ > C & C++' 카테고리의 다른 글

API 대화상자  (0) 2011.04.25
[API] 리소스 사용  (0) 2011.04.25
[API] 화면 출력 API  (0) 2011.04.25
API 란 무엇인가?  (0) 2011.04.25
메시지 훅(Message Hook)  (0) 2011.04.25

댓글