728x90

원문 : http://www.rastertek.com/dx11tut13.html


Tutorial 13: Direct Input


이번 강좌는 DirectX 11에서 Direct 입력 사용하는 법을 설명합니다. 이번 강좌의 코드는 저번 폰트 강좌 코드에 기초합니다.


Direct 입력은 DirectX API가 제공하는 입력 장치와의 초 엄청 빠른 인터페이싱 방법입니다. DirectX 11에서 Direct 입력 API 부분은 이전 버전 그대로여서 여전히 8버전입니다. 그러나 Direct 입력은 애초에 많이 시행되어서 (direct 사운드와 비슷하게) 업데이트될 필요가 없었습니다. Direct 입력은 일반적인 윈도우 입력 시스템을 뛰어넘는 믿을 수 없는 스피드를 제공합니다. 입력 장치에 빠르게 반응하는 것을 요구하는 왠만한 고성능 어플리케이션들은 Direct 입력을 사용할 것입니다.


이번 강좌는 키보드와 마우스 장치에 Direct 입력을 어떻게 사용하는지에 초점을 맞출 것입니다. 또 마우스 포인터의 현재 위치를 나타내기 위해 TextClass를 사용할 것입니다. 이전 강좌들이 이미 InputClass를 가지고 있기 때문에 이번에는 그냥 이전에 사용된 Windows 메소드 대신에 Direct 입력을 사용하도록 재작성할 것입니다.



Inputclass.h


////////////////////////////////////////////////////////////////////////////////
// Filename: inputclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _INPUTCLASS_H_
#define _INPUTCLASS_H_


헤더에 여러분이 사용하는 Direct 입력 버전을 정의해야 합니다. 안그럼 컴파일러가 8버전을 기본으로 하겠다는 짜증스러운 메시지를 발생할 것입니다.


///////////////////////////////
// PRE-PROCESSING DIRECTIVES //
///////////////////////////////
#define DIRECTINPUT_VERSION 0x0800


다음 두 라입러리는 Direct 입력이 동작하도록 링크해야 합니다.


/////////////
// LINKING //
/////////////
#pragma comment(lib, "dinput8.lib")
#pragma comment(lib, "dxguid.lib")


이게 Direct 입력을 위해 요구되는 헤더입니다.


//////////////
// INCLUDES //
//////////////
#include <dinput.h>


////////////////////////////////////////////////////////////////////////////////
// Class name: InputClass
////////////////////////////////////////////////////////////////////////////////
class InputClass
{
public:
	InputClass();
	InputClass(const InputClass&);
	~InputClass();

	bool Initialize(HINSTANCE, HWND, int, int);
	void Shutdown();
	bool Frame();

	bool IsEscapePressed();
	void GetMouseLocation(int&, int&);

private:
	bool ReadKeyboard();
	bool ReadMouse();
	void ProcessInput();

private:


처음의 private 멤버 변수 3개는 Direct 입력과, 키보드 장치, 마우스 장치에 대한 인터페이스 입니다.


	IDirectInput8* m_directInput;
	IDirectInputDevice8* m_keyboard;
	IDirectInputDevice8* m_mouse;


고 다음 private 멤버 변수 2개는 키보드와 마우스 장치의 현재 상태를 기록하기 위해 사용됩니다.


	unsigned char m_keyboardState[256];
	DIMOUSESTATE m_mouseState;

	int m_screenWidth, m_screenHeight;
	int m_mouseX, m_mouseY;
};

#endif


Inputclass.cpp


////////////////////////////////////////////////////////////////////////////////
// Filename: inputclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "inputclass.h"


클래스 생성자는 Direct 입력 인터페이스 변수를 null로 초기화합니다.


InputClass::InputClass()
{
	m_directInput = 0;
	m_keyboard = 0;
	m_mouse = 0;
}


InputClass::InputClass(const InputClass& other)
{
}


InputClass::~InputClass()
{
}


bool InputClass::Initialize(HINSTANCE hinstance, HWND hwnd, int screenWidth, int screenHeight)
{
	HRESULT result;


	// Store the screen size which will be used for positioning the mouse cursor.
	m_screenWidth = screenWidth;
	m_screenHeight = screenHeight;

	// Initialize the location of the mouse on the screen.
	m_mouseX = 0;
	m_mouseY = 0;


이 함수 호출은 Direct 입력의 인터페이스를 초기화할 것입니다. Direct 입력 객체를 얻기만 하면 다른 입력 장치들을 초기화할 수 있습니다.


	// Initialize the main direct input interface.
	result = DirectInput8Create(hinstance, DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&m_directInput, NULL);
	if(FAILED(result))
	{
		return false;
	}


먼저 초기화할 입력 장치는 키보드가 되겠습니다.


	// Initialize the direct input interface for the keyboard.
	result = m_directInput->CreateDevice(GUID_SysKeyboard, &m_keyboard, NULL);
	if(FAILED(result))
	{
		return false;
	}

	// Set the data format.  In this case since it is a keyboard we can use the predefined data format.
	result = m_keyboard->SetDataFormat(&c_dfDIKeyboard);
	if(FAILED(result))
	{
		return false;
	}


키보드의 협력 레벨을 설정하는 것은 장치가 뭘 하는지 장치를 어떻게 사용할 것인지의 이유로 중요합니다. 여기서는 다름 프로그램과 공유하지 않도록 설정할 것입니다. (DISCL_EXCLUSIVE) 이 방법은 여러분이 키를 누르면 오직 여러분의 어플리케이션만 입력을 볼 수 있고 다른 어플리케이션은 그 입력에 접근할 수 없을 것입니다. 그러나 여러분의 프로그램이 실행중인 동안 키보드 입력에 다른 어플리케이션이 접근할 수 있기를 원한다면 non-exclusive로 설정할 수 있습니다. (DISCL_NONEXCLUSIVE) 다시 print screen key가 동작하고 다른 실행중인 어플리케이션이 키보드에 의해 조작될 수 있는 등등이 가능해집니다. 기억하실게 non-exclusive이고 창 모드로 실행 중이면 장치가 언제 focus(윈도우에서 창 활성화를 말한다.)를 잃었고 언제 다시 얻었는지를 확인해야 하며 그럼 프로그램이 다시 장치를 얻어서 사용할 수 있습니다. 이 focus를 잃는 것은 일반적으로 다른 어플리케이션 창이 여러분의 창위로 메인 focus가 되거나 여러분의 창이 '최소화'될때 발생합니다.


	// Set the cooperative level of the keyboard to not share with other programs.
	result = m_keyboard->SetCooperativeLevel(hwnd, DISCL_FOREGROUND | DISCL_EXCLUSIVE);
	if(FAILED(result))
	{
		return false;
	}


키보드가 설정되면 마지막으로 사용할 수 있도록 키보드에 접근을 얻기 위해 Acquire를 호출합니다.


	// Now acquire the keyboard.
	result = m_keyboard->Acquire();
	if(FAILED(result))
	{
		return false;
	}


다음으로 설정할 입력 장치는 마우스입니다.


	// Initialize the direct input interface for the mouse.
	result = m_directInput->CreateDevice(GUID_SysMouse, &m_mouse, NULL);
	if(FAILED(result))
	{
		return false;
	}

	// Set the data format for the mouse using the pre-defined mouse data format.
	result = m_mouse->SetDataFormat(&c_dfDIMouse);
	if(FAILED(result))
	{
		return false;
	}


우리는 마우스에 대해 비배타적 협력 설정을 사용합니다. focus 안인지 밖인지 확인해야하며 매번 다시 얻어야 합니다.


	// Set the cooperative level of the mouse to share with other programs.
	result = m_mouse->SetCooperativeLevel(hwnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
	if(FAILED(result))
	{
		return false;
	}


마우스가 설정되면 마우스 사용을 시작할 수 있도록 Acquire를 호출합니다.


	// Acquire the mouse.
	result = m_mouse->Acquire();
	if(FAILED(result))
	{
		return false;
	}

	return true;
}


Shutdown 함수는 두 장치와 Direct 입력의 인터페이스를 해제합니다. 주의하실 것은 장치들은 항상 먼저 un-acuired되고 해제됩니다.


void InputClass::Shutdown()
{
	// Release the mouse.
	if(m_mouse)
	{
		m_mouse->Unacquire();
		m_mouse->Release();
		m_mouse = 0;
	}

	// Release the keyboard.
	if(m_keyboard)
	{
		m_keyboard->Unacquire();
		m_keyboard->Release();
		m_keyboard = 0;
	}

	// Release the main interface to direct input.
	if(m_directInput)
	{
		m_directInput->Release();
		m_directInput = 0;
	}

	return;
}


InputClass의 Frame 함수는 우리가 설정하는 상태버퍼로 장치의 상태를 읽어들입니다. 각 장치의 상태가 읽어진 후에 변화를 처리합니다.


bool InputClass::Frame()
{
	bool result;


	// Read the current state of the keyboard.
	result = ReadKeyboard();
	if(!result)
	{
		return false;
	}

	// Read the current state of the mouse.
	result = ReadMouse();
	if(!result)
	{
		return false;
	}

	// Process the changes in the mouse and keyboard.
	ProcessInput();

	return true;
}


ReadKeyboard은 키보드의 상태를 m_keyboardState 변수로 읽어들일 것입니다. 그 상태는 어떤 키가 현재 눌렸는지 안눌렸는지를 보여줄 것입니다. 만약 키보드 읽기를 실패한다면 다섯가지 이유중 하나일 것입니다. 여기서 제가 고칠 2개는 focus를 잃었거나 acquired가 되지 않은 것입니다. 이 경우라면 컨트롤을 다시 되찾아 올때까지 매 프레임마다 Acquire를 호출합니다. 창이 최소화되어 있다면 그 경우 Acquire는 실패할 것이지만 창이 다시 포그라운드로 돌아오면 Acquire은 성공할 것이고 키보드 상태를 읽을 수 있게 될 것입니다. 다른 3개의 에러 타입은 이번 강좌에서는 고치고 싶지 않습니다.


bool InputClass::ReadKeyboard()
{
	HRESULT result;


	// Read the keyboard device.
	result = m_keyboard->GetDeviceState(sizeof(m_keyboardState), (LPVOID)&m_keyboardState);
	if(FAILED(result))
	{
		// If the keyboard lost focus or was not acquired then try to get control back.
		if((result == DIERR_INPUTLOST) || (result == DIERR_NOTACQUIRED))
		{
			m_keyboard->Acquire();
		}
		else
		{
			return false;
		}
	}
		
	return true;
}


ReadMouse은 ReadKeyboard가 키보드 상태를 읽은것 처럼 마우스의 상태를 읽을 것입니다. 그러나 마우스 상태는 단지 마지막 프레임에서 마우스 위치에서의 변화입니다. 그래서 예를 들어 변화는 "마우스가 오른쪽으로 5칸 이동했다" 같을 것인데 마우스의 실제 화면상 위치는 얻지 못할 것입니다. 이 델타 정보는 다른 목적들에 매우 유용하며 마우스의 화면상 위치는 우리가 직접 유지할 수 있습니다.


bool InputClass::ReadMouse()
{
	HRESULT result;


	// Read the mouse device.
	result = m_mouse->GetDeviceState(sizeof(DIMOUSESTATE), (LPVOID)&m_mouseState);
	if(FAILED(result))
	{
		// If the mouse lost focus or was not acquired then try to get control back.
		if((result == DIERR_INPUTLOST) || (result == DIERR_NOTACQUIRED))
		{
			m_mouse->Acquire();
		}
		else
		{
			return false;
		}
	}

	return true;
}


ProcessInput 함수는 마지막 프레임 이후 입력 장치에서 일어난 변화를 처리하는 곳입니다. 이번 강좌에서는 Windows가 마우스 커서 위치를 유지하는 법과 비슷한 그저 간단한 마우스 위치 갱신을 할 것입니다. 이것을 위해 0으로 초기화 되어있는 m_mouseX와 m_mouseY 변수들을 사용하며 간단하게 마우스 위치의 변화를 이 두 변수에 더합니다. 이는 마우스의 위치를 사용자의 마우스 움직임에 따라 유지할 것입니다.


주의할 것은 마우스 위치(우리가 유지하는)가 화면에서 벗어나지 않도록 확인해야 합니다. 만약 사용자가 마우스를 계속해서 왼쪽으로 움직이면 다시 오른쪽으로 움직이기 시작할때까지 계속 커서를 0 위치에 유지해야 합니다.


void InputClass::ProcessInput()
{
	// Update the location of the mouse cursor based on the change of the mouse location during the frame.
	m_mouseX += m_mouseState.lX;
	m_mouseY += m_mouseState.lY;

	// Ensure the mouse location doesn't exceed the screen width or height.
	if(m_mouseX < 0)  { m_mouseX = 0; }
	if(m_mouseY < 0)  { m_mouseY = 0; }
	
	if(m_mouseX > m_screenWidth)  { m_mouseX = m_screenWidth; }
	if(m_mouseY > m_screenHeight) { m_mouseY = m_screenHeight; }
	
	return;
}


InputClass에 IsEscapePressed라는 함수를 추가하였습니다. 이 함수는 현재 특정 키가 눌려져 있는지 확인하기 위해 키보드를 활용하는 방법을 보여줍니다. 여러분의 어플리케이션에 중요한 다른 키들에 대해 체크하는 다른 함수를 작성할 수 있습니다.


bool InputClass::IsEscapePressed()
{
	// Do a bitwise and on the keyboard state to check if the escape key is currently being pressed.
	if(m_keyboardState[DIK_ESCAPE] & 0x80)
	{
		return true;
	}

	return false;
}


GetMouseLocation은 마우스의 위치를 리턴하는 도우미 함수입니다. GraphicsClass는 이 정보를 얻어서 TextClass를 사용하여 마우스의 x, y 위치를 화면에 렌더링할 수 있습니다.


void InputClass::GetMouseLocation(int& mouseX, int& mouseY)
{
	mouseX = m_mouseX;
	mouseY = m_mouseY;
	return;
}


Systemclass.cpp


그저 Windows 입력 시스템의 제거와 DirectX 입력 시스템 추가로 변화된 함수들만 다룰 것입니다.


////////////////////////////////////////////////////////////////////////////////
// Filename: systemclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "systemclass.h"


bool SystemClass::Initialize()
{
	int screenWidth, screenHeight;
	bool result;


	// Initialize the width and height of the screen to zero before sending the variables into the function.
	screenWidth = 0;
	screenHeight = 0;

	// Initialize the windows api.
	InitializeWindows(screenWidth, screenHeight);

	// Create the input object.  This object will be used to handle reading the keyboard input from the user.
	m_Input = new InputClass;
	if(!m_Input)
	{
		return false;
	}


입력 객체의 초기화는 이제 윈도우, 인스턴스의 핸들들과 화면 크기 변수가 필요하기 때문에 다릅니다. 또 Direct 입력 시작에서 성공했는지 여부를 나타내기 위해 boolean형 값을 반환합니다.


	// Initialize the input object.
	result = m_Input->Initialize(m_hinstance, m_hwnd, screenWidth, screenHeight);
	if(!result)
	{
		MessageBox(m_hwnd, L"Could not initialize the input object.", L"Error", MB_OK);
		return false;
	}

	// Create the graphics object.  This object will handle rendering all the graphics for this application.
	m_Graphics = new GraphicsClass;
	if(!m_Graphics)
	{
		return false;
	}

	// Initialize the graphics object.
	result = m_Graphics->Initialize(screenWidth, screenHeight, m_hwnd);
	if(!result)
	{
		return false;
	}
	
	return true;
}


void SystemClass::Shutdown()
{
	// Release the graphics object.
	if(m_Graphics)
	{
		m_Graphics->Shutdown();
		delete m_Graphics;
		m_Graphics = 0;
	}


입력 개체 해제는 객체를 delete하기 전에 Shutdown 호출이 필요합니다.


	// Release the input object.
	if(m_Input)
	{
		m_Input->Shutdown();
		delete m_Input;
		m_Input = 0;
	}

	// Shutdown the window.
	ShutdownWindows();
	
	return;
}


void SystemClass::Run()
{
	MSG msg;
	bool done, result;


	// Initialize the message structure.
	ZeroMemory(&msg, sizeof(MSG));
	
	// Loop until there is a quit message from the window or the user.
	done = false;
	while(!done)
	{
		// Handle the windows messages.
		if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}

		// If windows signals to end the application then exit out.
		if(msg.message == WM_QUIT)
		{
			done = true;
		}
		else
		{
			// Otherwise do the frame processing.  If frame processing fails then exit.
			result = Frame();
			if(!result)
			{
				MessageBox(m_hwnd, L"Frame Processing Failed", L"Error", MB_OK);
				done = true;
			}
		}


Run함수에서 esc키 확인은 약간 다르게 InputClass의 도우미 함수의 반환값을 확인하는 것으로 수행됩니다.


		// Check if the user pressed escape and wants to quit.
		if(m_Input->IsEscapePressed() == true)
		{
			done = true;
		}
	}

	return;
}


bool SystemClass::Frame()
{
	bool result;
	int mouseX, mouseY;


Frame 함수 수행 중 키보드와 마우스의 상태를 갱신하기 위해 입력 객체가 가진 Frame 함수를 호출합니다. 이 호출이 실패할 수 있으니 반환값을 확인해야 합니다.


	// Do the input frame processing.
	result = m_Input->Frame();
	if(!result)
	{
		return false;
	}


입력 장치 업데이트를 읽은 후 마우스의 위치로 GraphicsClass를 갱신하여 화면상 텍스트에 그 위치를 렌더링할 수 있습니다.


	// Get the location of the mouse from the input object,
	m_Input->GetMouseLocation(mouseX, mouseY);

	// Do the frame processing for the graphics object.
	result = m_Graphics->Frame(mouseX, mouseY);
	if(!result)
	{
		return false;
	}

	// Finally render the graphics to the screen.
	result = m_Graphics->Render();
	if(!result)
	{
		return false;
	}

	return true;
}


MessageHandler에서 Windows 키보드 읽는 것은 지웠습니다. Direct 입력가 이제 우리를 위한 이 모든 것을 처리합니다.


LRESULT CALLBACK SystemClass::MessageHandler(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
	return DefWindowProc(hwnd, umsg, wparam, lparam);
}


Graphicsclass.h


////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_


/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = true;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "d3dclass.h"
#include "cameraclass.h"
#include "textclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class name: GraphicsClass
////////////////////////////////////////////////////////////////////////////////
class GraphicsClass
{
public:
	GraphicsClass();
	GraphicsClass(const GraphicsClass&);
	~GraphicsClass();

	bool Initialize(int, int, HWND);
	void Shutdown();


Frame 함수는 이제 매 프레임마다 마우스 위치 갱신을 위해 정수형 두개를 받습니다.


	bool Frame(int, int);
	bool Render();

private:
	D3DClass* m_D3D;
	CameraClass* m_Camera;
	TextClass* m_Text;
};

#endif


Graphicsclass.cpp


이전 강좌에서 바뀐 함수만을 살펴볼 것입니다.


////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "graphicsclass.h"


Frame 함수는 이제 마우스 x, y 위치를 받아서 TextClass 객체가 화면에 위치를 쓸 텍스트 문자열을 갱신하게 합니다.


bool GraphicsClass::Frame(int mouseX, int mouseY)
{
	bool result;


	// Set the location of the mouse.
	result = m_Text->SetMousePosition(mouseX, mouseY, m_D3D->GetDeviceContext());
	if(!result)
	{
		return false;
	}

	// Set the position of the camera.
	m_Camera->SetPosition(0.0f, 0.0f, -10.0f);

	return true;
}


Textclass.h


////////////////////////////////////////////////////////////////////////////////
// Filename: textclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _TEXTCLASS_H_
#define _TEXTCLASS_H_

///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "fontclass.h"
#include "fontshaderclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class name: TextClass
////////////////////////////////////////////////////////////////////////////////
class TextClass
{
private:
	struct SentenceType
	{
		ID3D11Buffer *vertexBuffer, *indexBuffer;
		int vertexCount, indexCount, maxLength;
		float red, green, blue;
	};

	struct VertexType
	{
		D3DXVECTOR3 position;
		D3DXVECTOR2 texture;
	};

public:
	TextClass();
	TextClass(const TextClass&);
	~TextClass();

	bool Initialize(ID3D11Device*, ID3D11DeviceContext*, HWND, int, int, D3DXMATRIX);
	void Shutdown();
	bool Render(ID3D11DeviceContext*, D3DXMATRIX, D3DXMATRIX);


TextClass는 이제 마우스 위치 설정을 위한 새 함수를 가집니다.


	bool SetMousePosition(int, int, ID3D11DeviceContext*);

private:
	bool InitializeSentence(SentenceType**, int, ID3D11Device*);
	bool UpdateSentence(SentenceType*, char*, int, int, float, float, float, ID3D11DeviceContext*);
	void ReleaseSentence(SentenceType**);
	bool RenderSentence(ID3D11DeviceContext*, SentenceType*, D3DXMATRIX, D3DXMATRIX);

private:
	FontClass* m_Font;
	FontShaderClass* m_FontShader;
	int m_screenWidth, m_screenHeight;
	D3DXMATRIX m_baseViewMatrix;
	SentenceType* m_sentence1;
	SentenceType* m_sentence2;
};

#endif


Textclass.cpp


이전 버전 이후 달라진 함수들만을 다룰 것입니다.


///////////////////////////////////////////////////////////////////////////////
// Filename: textclass.cpp
///////////////////////////////////////////////////////////////////////////////
#include "textclass.h"


이제 TextClass에서 새 함수를 가지는데 마우스 x, y위치를 두 문자열로 변환하고 마우스 위치가 화면에 렌더링 될 수 있도록 두 문장을 갱신합니다.


bool TextClass::SetMousePosition(int mouseX, int mouseY, ID3D11DeviceContext* deviceContext)
{
	char tempString[16];
	char mouseString[16];
	bool result;


	// Convert the mouseX integer to string format.
	_itoa_s(mouseX, tempString, 10);

	// Setup the mouseX string.
	strcpy_s(mouseString, "Mouse X: ");
	strcat_s(mouseString, tempString);

	// Update the sentence vertex buffer with the new string information.
	result = UpdateSentence(m_sentence1, mouseString, 20, 20, 1.0f, 1.0f, 1.0f, deviceContext);
	if(!result)
	{
		return false;
	}

	// Convert the mouseY integer to string format.
	_itoa_s(mouseY, tempString, 10);

	// Setup the mouseY string.
	strcpy_s(mouseString, "Mouse Y: ");
	strcat_s(mouseString, tempString);

	// Update the sentence vertex buffer with the new string information.
	result = UpdateSentence(m_sentence2, mouseString, 20, 40, 1.0f, 1.0f, 1.0f, deviceContext);
	if(!result)
	{
		return false;
	}

	return true;
}


Summary


보시다시피 DirectX 11에서 Direct 입력을 설정하는 것은 매우 간단하며 입력 장치에서의 정보를 엄청 빠르게 접근할 수 있게 해줍니다.



연습하기


1. 컴파일하고 프로그램을 실행해 보세요. 화면 주위 마우스를 움직여 보고 위치 텍스트가 바뀌는지 확인하세요.


2. 2D 렌더링 강좌에서의 정보와 이번 강좌의 것과 조합하여 마우스 움직임에 따라 움직이는 여러분만의 마우스 커서를 만드세요.


3. 키보드 버퍼를 읽는 함수를 시행해 보고 여러분이 타이핑한 것을 화면에 나타내 보세요.

728x90

+ Recent posts