고급 예제

다양한 도구들을 가지고 마음껏 응용해보세요.

시리얼통신을 사용하여 C++로 아두이노 제어하기

2014-10-23 09:49:45

개요

개요

시리얼 통신으로 스케치내에서 시리얼모니터만으로 제어하는 것이 아닌 C++로 아두이노를 제어해보자.

프로세싱으로 시리얼통신으로 아두이노 스케치를 제어하는 방법과 동일하다. 다만 프로세싱은 스케치처럼 간단하게 포트에 데이터를 올리고 받으면 끝이지만  C++언어는 스케치나 프로세싱과 달리 시리얼통신을 사용하는 방법이 까다롭기 때문에 클래스를 생성하고 포트를 열고 통신 초기화를 시키고 연결을 하는 등 여러 과정을 거쳐야 시리얼통신이 가능하다.

사실상 쓰기 편한 프로세싱이나 자바가 존재하기 때문에 아두이노와 C++과의 시리얼통신은 거의 쓰이지 않는다.
하지만 특수한 경우가 생겨 사용해야할 경우도 생기기 때문에 연결하는 방법을 알고 있으면 두고두고 유용하게 쓰일 수 있다. 
물론 알기 위해서는 C언어의 문법이나 알고리즘 등은 기본적으로 깔고 들어가야 하지만 모른다면 그냥 복사-붙여넣기를 해서 사용해도 상관은 없다. 중요한건 서로간의 통신이 되는지 안되는지의 여부이다.
(소스는 http://blog.naver.com/onlywin7788/140161634165 와 http://mnlt.tistory.com/119 를 참조하였다.)

동영상

C++ 커맨드창으로 간단하게 아두이노 서보모터를 제어하는 영상





소프트웨어 Making

1. C++(VS2010기준)

SerialPort.h

#include <Windows.h>
#include <atlstr.h>

#define BUFFER_SIZE 128

class CSerialPort  
{  
public:  
	CSerialPort(void);  
	virtual ~CSerialPort(void);  

private:  
	HANDLE  m_hComm;  
	DCB     m_dcb;  
	COMMTIMEOUTS m_CommTimeouts;  
	bool    m_bPortReady;  
	bool    m_bWriteRC;  
	bool    m_bReadRC;  
	DWORD   m_iBytesWritten;  
	DWORD   m_iBytesRead;  
	DWORD   m_dwBytesRead;  

public:  
	void ClosePort();  
	bool ReadByte(BYTE &resp);  
	bool ReadByte(BYTE* &resp, UINT size);  
	bool WriteByte(char bybyte);  
	bool OpenPort(CString portname);  
	bool SetCommunicationTimeouts(DWORD ReadIntervalTimeout,  
		DWORD ReadTotalTimeoutMultiplier, DWORD ReadTotalTimeoutConstant,  
		DWORD WriteTotalTimeoutMultiplier,DWORD WriteTotalTimeoutConstant);  
	bool ConfigurePort(DWORD BaudRate, BYTE ByteSize, DWORD fParity,   
		BYTE  Parity,BYTE StopBits);  
};
SerialPort.h에서는 클래스를 선언해 준다.

클래스의 함수에는
포트를 닫는 함수인 ClosePort()
포트에서 데이터를 읽어오는 함수인 ReadByte()
포트로 데이터를 전송하는 함수인 WriteByte()
포트를 여는 함수인 OpenPort()
통신포트에서 Timeout을 설정하는 함수인 SetCommunicationTimeouts()
포트를 초기화설정하는 함수인 ConfigurePort()가 있다.

SerialPort.cpp
#include "serialport.h"
#include <iostream>

using namespace std;

CSerialPort::CSerialPort()  
{  
}  

CSerialPort::~CSerialPort()  
{  
}  

bool CSerialPort::OpenPort(CString portname)  
{  
	m_hComm = CreateFile(L"//./" + portname,GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,0,0); //시리얼 포트를 오픈한다. 
	if(m_hComm == INVALID_HANDLE_VALUE)  //정상적으로 포트가 열렸는지 확인
	{  
		return false;  //열리지 않았을 경우 false 반환
	}  
	else  
		return true;   //제대로 열렸을 경우 true 반환
}  

bool CSerialPort::ConfigurePort(DWORD BaudRate, BYTE ByteSize, DWORD fParity,   
	BYTE Parity, BYTE StopBits)  
{  
	if((m_bPortReady = GetCommState(m_hComm, &m_dcb))==0) //포트의 상태를 확인. 정상적으로 열리지 않았을 경우 false 반환
	{  
		printf("\nGetCommState Error\n");
		//"MessageBox(L, L"Error", MB_OK + MB_ICONERROR);  
		CloseHandle(m_hComm);  
		return false;  
	}  
        //포트의 대한 기본값을 설정
	m_dcb.BaudRate          = BaudRate;  
	m_dcb.ByteSize          = ByteSize;  
	m_dcb.Parity            = Parity ;  
	m_dcb.StopBits          = StopBits;  
	m_dcb.fBinary           = true;  
	m_dcb.fDsrSensitivity   = false;  
	m_dcb.fParity           = fParity;  
	m_dcb.fOutX             = false;  
	m_dcb.fInX              = false;  
	m_dcb.fNull             = false;  
	m_dcb.fAbortOnError     = true;  
	m_dcb.fOutxCtsFlow      = false;  
	m_dcb.fOutxDsrFlow      = false;  
	m_dcb.fDtrControl       = DTR_CONTROL_DISABLE;  
	m_dcb.fDsrSensitivity   = false;  
	m_dcb.fRtsControl       = RTS_CONTROL_DISABLE;  
	m_dcb.fOutxCtsFlow      = false;  
	m_dcb.fOutxCtsFlow      = false;  

	m_bPortReady = SetCommState(m_hComm, &m_dcb);  //포트 상태 확인

	if(m_bPortReady == 0)  //포트의 상태를 확인. 정상일 경우 true 반환 아닐 경우 false 반환
	{  
		//MessageBox(L"SetCommState Error");  
		printf("SetCommState Error");
		CloseHandle(m_hComm);  
		return false;  
	}  

	return true;  
}  

bool CSerialPort::SetCommunicationTimeouts(DWORD ReadIntervalTimeout,  
	DWORD ReadTotalTimeoutMultiplier, DWORD ReadTotalTimeoutConstant,  
	DWORD WriteTotalTimeoutMultiplier, DWORD WriteTotalTimeoutConstant) //통신 포트에 관한 Timeout 설정
{  
	if((m_bPortReady = GetCommTimeouts(m_hComm, &m_CommTimeouts)) == 0)  
		return false;  

	m_CommTimeouts.ReadIntervalTimeout          = ReadIntervalTimeout; //통신할때 한바이트가 전송 후 다음 바이트가 전송될때까지의 시간
//통신에서 데이터를 읽을 때 Timeout을 사용할 것인지에 대한 여부 m_CommTimeouts.ReadTotalTimeoutConstant = ReadTotalTimeoutConstant; m_CommTimeouts.ReadTotalTimeoutMultiplier = ReadTotalTimeoutMultiplier;
//통신에서 데이터를 전송할 때 Timeout을 사용할 것인지에 대한 여부 m_CommTimeouts.WriteTotalTimeoutConstant = WriteTotalTimeoutConstant; m_CommTimeouts.WriteTotalTimeoutMultiplier = WriteTotalTimeoutMultiplier; m_bPortReady = SetCommTimeouts(m_hComm, &m_CommTimeouts); //포트 상태 확인 if(m_bPortReady == 0) //포트 상태가 닫혀 있을 경우 false반환. 아닐 경우 true반환 { //MessageBox(L"StCommTimeouts function failed",L"Com Port Error",MB_OK+MB_ICONERROR); printf("\nStCommTimeouts function failed\n"); CloseHandle(m_hComm); return false; } return true; } bool CSerialPort::WriteByte(char bybyte) { //iBytesWritten=0; m_iBytesWritten=0; cout<< bybyte << endl; if(WriteFile(m_hComm, &bybyte, 1, &m_iBytesWritten, NULL) == 0) //입력받은 값을 WriteFile을 통해 포트로 전송한다. return false; else return true; } bool CSerialPort::ReadByte(BYTE &resp) { BYTE rx; resp=0; DWORD dwBytesTransferred=0; if(ReadFile(m_hComm, &rx, 1, &dwBytesTransferred, 0)) //포트에 존재하는 데이터를 ReadFile을 통해 1바이트씩 읽어온다. { if(dwBytesTransferred == 1) //데이터를 읽어오는데 성공했을 경우 { resp=rx; //resp에 데이터를 저장하고 true 반환 return true; } } return false; //실패했을 경우 false 반환 } bool CSerialPort::ReadByte(BYTE* &resp, UINT size) { DWORD dwBytesTransferred=0; if(ReadFile(m_hComm, resp, size, &dwBytesTransferred, 0)) { if(dwBytesTransferred == size) return true; } return false; } void CSerialPort::ClosePort() { CloseHandle(m_hComm); //포트를 닫는다. return; }
헤더파일에서 선언한 클래스의 함수를 정의하는 부분이다

bool CSerialPort::OpenPort(CString portname)  
{  
	m_hComm = CreateFile(L"//./" + portname,GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,0,0);  //시리얼포트를 오픈한다
if(m_hComm == INVALID_HANDLE_VALUE) //정상적으로 포트가 열렸는지 확인 { return false; //열리지 않았을 경우 false 반환 } else return true; //제대로 열렸을 경우 true 반환 }
OpenPort에서는 통신할 시리얼포트 번호를 받아 CreateFile을 통해 시리얼포트를 열게 된다. 
m_hComm에는 통신을 할 포트의 정보가 들어가게 된다.
포트가 정상적으로 열리게 되면 True를 반환하고 포트를 열지 못했을 경우에는 False를 반환하게 된다.

bool CSerialPort::ConfigurePort(DWORD BaudRate, BYTE ByteSize, DWORD fParity,   
	BYTE Parity, BYTE StopBits)  
{  
	if((m_bPortReady = GetCommState(m_hComm, &m_dcb))==0) //포트의 상태를 확인. 정상적으로 열리지 않았을 경우 false 반환
	{  
		printf("\nGetCommState Error\n");
		//"MessageBox(L, L"Error", MB_OK + MB_ICONERROR);  
		CloseHandle(m_hComm);  
		return false;  
	}  
        //포트의 대한 기본값을 설정
	m_dcb.BaudRate          = BaudRate;  
	m_dcb.ByteSize          = ByteSize;  
	m_dcb.Parity            = Parity ;  
	m_dcb.StopBits          = StopBits;  
	m_dcb.fBinary           = true;  
	m_dcb.fDsrSensitivity   = false;  
	m_dcb.fParity           = fParity;  
	m_dcb.fOutX             = false;  
	m_dcb.fInX              = false;  
	m_dcb.fNull             = false;  
	m_dcb.fAbortOnError     = true;  
	m_dcb.fOutxCtsFlow      = false;  
	m_dcb.fOutxDsrFlow      = false;  
	m_dcb.fDtrControl       = DTR_CONTROL_DISABLE;  
	m_dcb.fDsrSensitivity   = false;  
	m_dcb.fRtsControl       = RTS_CONTROL_DISABLE;  
	m_dcb.fOutxCtsFlow      = false;  
	m_dcb.fOutxCtsFlow      = false;  

	m_bPortReady = SetCommState(m_hComm, &m_dcb); //포트 상태 확인

	if(m_bPortReady == 0)  //포트의 상태를 확인. 정상일 경우 true 반환 아닐 경우 false 반환
	{  
		//MessageBox(L"SetCommState Error");  
		printf("SetCommState Error");
		CloseHandle(m_hComm);  
		return false;  
	}  

	return true;  
}  
ConfigurePort()에서는 열린 포트에 대한 기본설정을 하게 된다. 포트의 상태를 확인하여 포트가 정상적으로 열려 있는지 닫혀있는지 확인한 후 정상적으로 열려 있을 경우 포트 기본값에 대한 설정을 받은 인자값을 통해 하게 된다. 

bool CSerialPort::SetCommunicationTimeouts(DWORD ReadIntervalTimeout,  
	DWORD ReadTotalTimeoutMultiplier, DWORD ReadTotalTimeoutConstant,  
	DWORD WriteTotalTimeoutMultiplier, DWORD WriteTotalTimeoutConstant) //통신 포트에 관한 Timeout 설정
{  
	if((m_bPortReady = GetCommTimeouts(m_hComm, &m_CommTimeouts)) == 0)  
		return false;  

	m_CommTimeouts.ReadIntervalTimeout          = ReadIntervalTimeout; //통신할때 한바이트가 전송 후 다음 바이트가 전송될때까지의 시간
//통신에서 데이터를 읽을 때 Timeout을 사용할 것인지에 대한 여부 m_CommTimeouts.ReadTotalTimeoutConstant = ReadTotalTimeoutConstant; m_CommTimeouts.ReadTotalTimeoutMultiplier = ReadTotalTimeoutMultiplier;
//통신에서 데이터를 전송할 때 Timeout을 사용할 것인지에 대한 여부 m_CommTimeouts.WriteTotalTimeoutConstant = WriteTotalTimeoutConstant; m_CommTimeouts.WriteTotalTimeoutMultiplier = WriteTotalTimeoutMultiplier; m_bPortReady = SetCommTimeouts(m_hComm, &m_CommTimeouts); //포트 상태 확인 if(m_bPortReady == 0) //포트 상태가 닫혀 있을 경우 false반환. 아닐 경우 true반환 { //MessageBox(L"StCommTimeouts function failed",L"Com Port Error",MB_OK+MB_ICONERROR); printf("\nStCommTimeouts function failed\n"); CloseHandle(m_hComm); return false; } return true; }
통신포트에 대한 Timeout을 설정하는 함수이다. 입력받은 값으로 Timeout값을 설정하게 되는데 Timeout을 설정하고 싶지 않을 경우 모든 인자값을 0으로 주면 된다.
보통의 경우에는 모두 0으로 입력한다.

bool CSerialPort::WriteByte(char bybyte)  
{  
	//iBytesWritten=0;
	m_iBytesWritten=0;  
	cout<< bybyte << endl;

	if(WriteFile(m_hComm, &bybyte, 1, &m_iBytesWritten, NULL) == 0) //입력받은 값을 WriteFile을 통해 포트로 전송한다.
		return false;  
	else  
		return true;  
}  
데이터를 전송할때 쓰는 WriteByte함수이다. 입력받은 인자값을 WriteFile을 통해 포트로 전송하게 된다. 전송에 성공하면 true, 전송에 실패하면 false를 반환한다.

bool CSerialPort::ReadByte(BYTE &resp)  
{  
	BYTE rx;  
	resp=0;  

	DWORD dwBytesTransferred=0;  

	if(ReadFile(m_hComm, &rx, 1, &dwBytesTransferred, 0)) //포트에 존재하는 데이터를 ReadFile을 통해 1바이트씩 읽어온다.
	{  
		if(dwBytesTransferred == 1) //데이터를 읽어오는데 성공했을 경우
		{  
			resp=rx;  //resp에 데이터를 저장하고 true 반환
			return true;  
		}  
	}  
	return false; //실패했을 경우 false 반환
}  
포트에 있는 데이터를 읽어올때 쓰는 ReadByte함수이다. ReadFile을 통해 포트에서 데이터를 읽어오며 1바이트씩 읽어오게 된다. 데이터를 읽어오는데 성공하면 true를 반환하고 실패했을 경우 false를 반환한다. 
readByte함수가 두개가 존재하는데 다른 하나는 ReadByte(BYTE* &resp, UINT size)로 인자값으로 읽어올 데이터의 크기를 size로 받아서 그 크기만큼 데이터를 읽어온다.

void CSerialPort::ClosePort()  
{  
	CloseHandle(m_hComm); //포트를 닫는다.
	return;  
}  
ClosePort에서는 CloseHandle을 통해 포트를 닫는다.



SerialComm.h
#include "serialport.h"

#define RETURN_SUCCESS 1
#define RETURN_FAIL 0


class CSerialComm
{
public :
	CSerialComm();
	~CSerialComm();

	CSerialPort	serial;
	int		connect(char* _portNum);
	int		sendCommand(char pos);
	void		disconnect();
};
 
CSerialComm는 위의 SerialPort에서 만든 클래스를 가지고 실질적인 통신에 필요한 함수를 가지는 클래스이다.

SerialComm.cpp
#include "serialcomm.h"


CSerialComm::CSerialComm(){}
CSerialComm::~CSerialComm(){}


int CSerialComm::connect(char* portNum)
{
	if(!serial.OpenPort(portNum)) //포트를 오픈하고 오픈에 실패하였으면 fail을 반환한다.
		return RETURN_FAIL;

	serial.ConfigurePort(CBR_9600, 8, FALSE, NOPARITY, ONESTOPBIT); //포트 기본값을 설정한다.
	serial.SetCommunicationTimeouts(0, 0, 0, 0, 0); //Timeout값 설정

	return RETURN_SUCCESS;
}


int CSerialComm::sendCommand(char pos) //데이터를 전송하는 함수
{
	if(serial.WriteByte(pos)) //WriteByte()를 통해 데이터 전송에 성공하면 SUCCESS, 실패하면 FAIL을 반환한다.
		return RETURN_SUCCESS;
	else
		return RETURN_FAIL;
}

void CSerialComm::disconnect() //포트를 다 쓰고 난뒤 닫는 함수
{
	serial.ClosePort();
}
SerialComm.cpp에서는 통신할 포트를 열어 기본값 설정과 Timeout값을 설정하는 connect()와WriteByte함수를 통해 데이터를 전송하는 sendCommand(), 포트를 닫는 disconnect()가 존재한다.
데이터를 읽어오는 ReceiveCommand()가 존재하지 않기 때문에 필요할 경우 작성하여 완성한다.

main
#include <stdio.h>
#include <iostream>
#include <string>
#include "serialcomm.h"

using namespace std;

int main()
{
	char buffer; 
	CSerialComm serialComm; //SerialComm 객체 생성


	if(!serialComm.connect("COM25")) //COM25번의 포트를 오픈한다. 실패할 경우 -1을 반환한다.
	{
		cout << "connect faliled" << endl;
		return -1;
	}
	else
		cout << "connect successed" << endl;


	while(1) { //오픈에 성공한 경우 sendCommand()를 통해 계속적으로 데이터를 전송한다. 전송에 실패 할 경우 failed 메시지를 출력한다.
		cin >> buffer;

		if(!serialComm.sendCommand(buffer))
		{	
			cout << "send command failed"<< endl;
		}
		else
			cout << "send Command success" << endl;
	}


	serialComm.disconnect(); //작업이 끝나면 포트를 닫는다

	cout << "end connect" << endl;
		return 0;

}


2. 아두이노 스케치
#include <Servo.h>   // 서보 라이브러리 사용


//서보모터 2개 객체 생성
Servo servo_1;
Servo servo_2;

int pos= 0;
char buffer[20];               //통신을 할때 buffer배열에 전송받은 데이터 입력
char bufferIndex = 0;                    

void setup() {
//서보모터의 핀번호를 9번 10번에 입력 servo_1.attach(9); servo_2.attach(10); Serial.begin(9600); // 시리얼 통신 초기화 Serial.flush(); Serial.println(">> "); } void loop() { if (Serial.available() > 0) {// 데이터를 입력 받았을 경우 buffer[bufferIndex] = Serial.read(); // 입력 데이터를 받고, buffer에 저장 if (buffer[bufferIndex] == 'h') { //h값을 입력 받았을 경우 가로로 움직이는 모터 제어 Serial.println(""); buffer[bufferIndex] = 0; pos = atoi(buffer); //입력받은 데이터를 int형으로 형변환 시킨다. servo_1.write(pos); bufferIndex = 0; Serial.print(pos); } else if (buffer[bufferIndex] == 'v') { //v값을 입력 받았을 경우 세로로 움직이는 모터 제어 Serial.println(""); buffer[bufferIndex] = 0; pos = atoi(buffer); //입력받은 데이터를 int형으로 형변환 시킨다. servo_2.write(pos); bufferIndex = 0; Serial.print(pos); } else { bufferIndex++; // 배열을 초기화 bufferIndex = bufferIndex%20; //인덱스 번호가 20을 넘어갈 경우 다시 0부터 시작할 수 있게 나머지 값으로 인덱스 설정 } } }
스케치에서는 서보모터를 두개를 제어하기 위한 소스를 작성한다. 서보모터의 핀번호를 9번 10번으로 설정하고 Serial통신을 통해 포트에 있는 데이터를 읽어서 
buffer[]에 저장한다. 데이터를 읽을때 h와 v에 따라 어느 서보모터를 제어할 것인지의 여부가 나뉘게 된다. 
예를들어 150h을 C++에서 입력했을 경우 h에 해당하는 서보모터가 150도로 회전하고 40v를 입력했을 경우에는 v에 해당하는 서보모터가 40도로 회전하게 된다.

시리얼 통신으로 읽어온 데이터는 int형이 아닌 char형이기 때문에 atoi함수를 통해 int형으로 변환한다.

기술문서

  • 부품목록
  • 회로도
  • 브레드보드 레이아웃
  • 스케치

kocoafabeditor

항상 진취적이고, 새로운 것을 추구하는 코코아팹 에디터입니다!

아두이노, C++, 시리얼 통신

아이유사랑해 2014-12-22 12:25:36

헤헷 onlywin7788 네이버 블로그 주인입니다. 좋은 곳에 코드 써주셔서 감사합니다 ^^

수박쨈 2014-12-23 16:40:02

직접 찾아와 주셔서 감사합니다. 좋은 소스 제공해 주셔서 감사합니다! 말을 건네고 가져왔어야 했는데 글 먼저 올리고 출처만 남기게 됐습니다. 그 점은 죄송합니다ㅜ

아이유사랑해 2014-12-26 14:38:27

아닙니다. 좋은 일에 써 주셔서 저야말로 영광이죠 ㅎ

박정훈 2015-05-15 11:26:49

안녕하세요 위에 블로그 주인분이 만드신 serial 통신 프로그램은 atmega와 연결하여 정상 작동을 하였는데 저는 콘솔 창으로 실행 시킬게 필요해 검색 중 이 사이트를 찾게되었습니다. 시리얼 포트 정상 커넥트되었는데 값을 send하여도 atmega에서 변함이 없습니다.. 혹시 왜 그러는지 알고 계신가요?

수박쨈 2015-05-15 14:01:29

@박정훈 정상커넥트가 됐고 값을 send했는데도 작동이 없을 경우에는 값의 전송과정에서 송신쪽과 수신쪽의 데이터의 형태나 받는 값이 달라졌기 때문에 그럴 확률이 높습니다.

예를 들어 위의 소스를 기준으로 설명하면 송신쪽에서는 int값을 보내려고 합니다. 하지만 전송하기 위해서는 char형으로 보내야 하기 때문에 char타입으로 정수를 보내게됩니다.
수신측에서는 int형값을 받아야 하지만 char형으로 데이터가 왔기 때문에 char형을 int로 바꿔주는 atoi()함수를 통해 char형의 데이터를 int형으로 바꿔서 사용합니다.

이렇게 송신측과 수신측의 데이터의 타입을 맞춰주는 과정이 필요하고 또한 송수신에는 데이터가 1바이트씩 이동하게 됩니다. 데이터가 이동하는 크기 또한 고려하여 맞춰주어야 합니다. 예를 들어 4바이트의 데이터가 필요할 경우 1바이트씩 4번을 받아 그 데이터를 합치는 과정또한 필요합니다.

연결은 무난하지만 아마 이 과정에서 많이들 헤매시는데 송수신의 데이터를 맞춰주는 과정이 간단하면서도 쉽지 않기 때문에 그 부분을 잘 보셔야 할 듯 합니다.(저 또한 이 과정에서 많이 해맸습니다. 연결은 됐는데 서보모터가 움직이질 않아서 이런저런 시행착오를 겪은후에야 알 수 있었습니다.)

박정훈 2015-05-29 17:27:20

안녕하세요 거의 보름만에 방문합니다.. 위에 문제 해결 하여 아트메가와 PC 통신 작동되는것을 확인 하였습니다. Receive도 만들어 볼까 하는데 sendCommand하고 비슷하게 만들면 되나요?...

수박쨈 2015-06-01 08:31:45

SerialPort.cpp를 보면 ReadByte()라는 함수를 볼 수 있습니다. WriteByte함수와 큰 차이가 없기 때문에 비슷하게 만드시면 될 듯 합니다.
구체적으로 어떤식으로 만들어 보라고는 말씀드릴 수는 없을거 같네요...

권동욱 2016-05-30 17:42:45

정말 존경스럽네요 ... 제가 계속 고민하던걸 단박에 이글로 해결됬어요 ... 감사합니다....

권동욱 2016-05-30 17:42:52

정말 존경스럽네요 ... 제가 계속 고민하던걸 단박에 이글로 해결됬어요 ... 감사합니다....

송덕원 2017-03-15 19:56:04

감사합니다. 도움이 많이 되었어요!