개발팁2022. 7. 27. 15:56

타자게임이란 키보드 타자 연습을 위한 게임으로,

한라인의 문장을 제시해주고, 똑같이 입력하면 성공, 그렇지 못하면 실패로 체크하고,

성공하면, 분당 타수를 알려주는, 간단한 게임을 말한다.

 

개인적으로 혼자서 연습도 가능하지만, 이를 채팅방에 적용하면, 다수의 사람과 경쟁을 통해 성취감을 얻을 수도 있다.

 

타자게임을 만들때 필요한 것은,

 

1. 한글 한줄 문장 데이터이다.

2. 한글을 초/중/종 성으로 쪼개는 작업이 필요하다.

3. 분당 타수를 계산하는 로직을 적용하면 된다.

 

 

 

 

 

프로그램은 시작 버튼을 누르면, 제시어를 보여주고, 똑같이 입력을 하면, 결과를 보여준다.

 

 

 

1. 한글 한줄 문장 데이터이다.

 

문장 데이터는 많을수록 좋다.

 

 

한컴 타자연습 사이트 - 짧은글 연습에서 문장을 가져왔다.

 


2692개의 문장이 제공되고 있다.

 

해당 문장은 DB 에 입력해서 꺼내와도 되고, 구글시트에 넣어놓고 꺼내와도 상관없다.

 

예제는 구글시트에 한줄 문장을 관리하기 때문에, 구글API 가 필요하다.

 

nuget 패키지 관리자에서, Google.Apis.Sheets.v4 를 설치한다.

 

 

 

public static List<string> GetTypingSentenceList()
{
List<string> result;

string _apikey = "***********************";
string _sheetid = "***********************";

result = new List<string>();

try
{
// API 서비스 생성
var service = new SheetsService(new BaseClientService.Initializer()
{
ApplicationName = "GoogleDocs .net API",
ApiKey = _apikey
});

var request = service.Spreadsheets.Values.Get(_sheetid, "타자!A1:A");

ValueRange response = request.Execute();
IList<IList<Object>> values = response.Values;
if (values != null && values.Count > 0)
{
foreach (var row in values)
{
if (row[0] != null && row[0].ToString() != "")
result.Add(row[0].ToString());
}
}

}
catch (Exception e1)
{
MessageBox.Show(e1.Message);
}

return result;
}

 

전체 한줄문장 목록은 이렇게 google sheet 에서 읽어오면 된다.

 

이렇게 읽어온 데이터는 캐시에 저장해두었다가, 요청시마다 꺼내서, 랜덤하게 한 라인을 select 하면 된다.

 

 

시작버튼 클릭시 처리 로직

 

Random rand = new Random();

// 문장 뽑기
List<string> sentence_list = GoogleDocsRepository.GetTypingSentenceList();
int rndNum = rand.Next(0, sentence_list.Count);
string sentence = sentence_list[rndNum].Trim();

this.txtKeyword.Text = sentence;

// 타자수 계산 : 1. 글자를 초/중/종 성으로 쪼갠다.
string[] split_words = HangulWorker.GetDisassemble(sentence, true);
// 타자수 계산 : 2. 글자수를 계산한다.
this._typingCount = HangulWorker.GetTypingSize(split_words);

this._isStart = true;
this._isKeyKirstPress = false;
this.txtInput.Focus();

 

이제 TextBox 에 첫 글자가 입력되는 순간부터 시간을 재고, 단어 입력이 끝나면, 결과를 발표하면 된다.

 

 

2. 한글을 초/중/종 성으로 쪼개는 작업이 필요하다.

 

제시어에서 글자를 모두 초/중/종 성으로 쪼개서, 타자수를 계산해야한다.

한글이라고 모두 2번의 타자가 아니기 때문이다.

 

안녕하세요 를 초/중/종 성으로 쪼개면

ㅇ ㅏ ㄴ ㄴ ㅕ ㅇ ㅎ ㅏ ㅅ ㅔ ㅇ ㅇ

12글자가 되는 것이다.

 

이중에 된발음 ㄲ, ㄸ 이런 글자는 shift 키를 누르고 글짜를 누르기 때문에, 두번의 타자로 계산할지 정의가 필요하다.

예제에서는 한번의 타자로 계산했다.

 

또한 종성에서는

갉 갏 값  등으로 ㄺ ㅀ ㅄ 등의 글자들이 있는데, 이 종성 글자들은 타자수 두번으로 계산해야 한다.

 

시작버튼 클릭시 처리 로직에서 사용한 HangulWorker 의 기본 코드

 

// 유니코드표에서의 기본 위치
private const int hangulStartingPoint = 0xAC00;  // hangulStartingPoint : 유니코드에서 '가'의 위치

// 자소 목록
private static List<string> chosungHangulWords = (
new string[] { "ㄱ", "ㄲ", "ㄴ", "ㄷ", "ㄸ", "ㄹ", "ㅁ", "ㅂ", "ㅃ", "ㅅ"
 , "ㅆ", "ㅇ", "ㅈ", "ㅉ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ" }
).ToList<string>();    // 초성 19개 요소
private static List<string> jungsungHangulWords = (
   new string[] { "ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ"
 , "ㅙ", "ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ"
 , "ㅣ" }
   ).ToList<string>();    // 중성 21개 요소
private static List<string> jongsungHangulWords = (
   new string[] { string.Empty, "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ", "ㄶ", "ㄷ", "ㄹ", "ㄺ"
 , "ㄻ", "ㄼ", "ㄽ", "ㄾ", "ㄿ", "ㅀ", "ㅁ", "ㅂ", "ㅄ", "ㅅ"
 , "ㅆ", "ㅇ", "ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ" }
   ).ToList<string>();    // 종성 28개 요소

 

아래는, 한글문장을 초/중/종 성의 글자별로 쪼개는 GetDisassemble 함수 본문이다.

 

public static string[] GetDisassemble(string str, bool bSkipBase)
{
if (str == "") return null;

List<string> list = new List<string>();
int uniValue;   // 유니코드 상의 글자 위치
int iTemp;
string s;

for (int n = 0; n < str.Length; n++)
{
uniValue = 0;
s = str.Substring(n, 1);
byte[] bytes = Encoding.Unicode.GetBytes(s);
// 빅앤디안 방식이지만 아래 계산 편이를 위해 리틀앤디안으로 변환


// 유니코드 상의 글자 위치 구하기
for (int b = 0; b < bytes.Length; b++)
{
uniValue += (int)(bytes[b] * Math.Pow(256.0, b));
}

if (uniValue < hangulStartingPoint) { list.Add(s); continue; }

// 초성
list.Add(chosungHangulWords[(uniValue - hangulStartingPoint) / jongsungMultiply]);


// 중성
list.Add(jungsungHangulWords[(uniValue - hangulStartingPoint) % jongsungMultiply / jongsungHangulWords.Count]);

// 종성
iTemp = (uniValue - hangulStartingPoint) % jongsungHangulWords.Count;
if (bSkipBase && iTemp == 0) continue;
list.Add(jongsungHangulWords[iTemp]);
}

return list.ToArray();
}

 

GetDisassemble  에서 쪼개진 글자 배열에서 타자수를 계산하는 GetTypingSize 함수 본문이다.

 

public static int GetTypingSize(string[] str_list)
{
int addCount = str_list.Select(d => d == "ㅘ" || d == "ㅙ" || d == "ㅚ" || d == "ㅝ" || d == "ㅞ" || d == "ㅟ" || d == "ㅢ" || d == "ㄳ" || d == "ㄵ" || d == "ㄶ" || d == "ㄺ" || d == "ㄻ" || d == "ㄼ" || d == "ㄽ" || d == "ㄾ" || d == "ㄾ" || d == "ㅀ" || d == "ㅄ").Count(d => d);

return str_list.Length + addCount;
}

 

초/중/종 성을 모두 1글자로 취급하며,

ㅟ ㅚ ㄻ ㄼ  등의 글자는 2글자로 취급하도록 계산하였다.

 

 

3. 분당 타수를 계산하는 로직을 적용하면 된다.

 

이제, 입력글자가 모두 일치 할경우, 분당 타수를 계산해서 알려주면 끝이다.

 

입력 TextBox 의 KeyDown 이벤트를 아래와 같이 정의한다.

 

private void txtInput_KeyDown(object sender, KeyEventArgs e)
{
if (this._isStart == true)
{
if (this._isKeyKirstPress == false)
{
this._isKeyKirstPress = true;
this._startDateTime = System.DateTime.Now;
}

if (this.txtKeyword.Text == this.txtInput.Text)
{
this._isStart = false;
this.checkDone();
}
}
}

 

이제 분당 타수를 표시해주면 끝이난다.

 

private void checkDone()
{
TimeSpan elapsed = DateTime.Now - this._startDateTime;

double typingMM = Math.Round(_typingCount * (60000.0 / elapsed.TotalMilliseconds));

this.txtResult.Text = $"{typingMM:N0} 타/분";
}

 

 

코드를 모두 적용하고, 프로그램을 실행하면, 아래와 같은 결과가 나오게 된다.

 

 

 

 

이것은 혼자하는 타자게임이지만, 채팅방에 이 로직을 적용하면, 여러명이서 경쟁하면서 게임을 즐길 수 있다.

 

아래는, 실제 네이버웍스메신저 단톡방에 메세지Bot 으로 구현한 타자 게임이다.

 

 

 

 

직접 만든 채팅방에 적용할 수 있다면, 좀더 편리한 UI 로 구현할 수 있을 것이다.

 

 

Posted by 헝개
개발팁2022. 4. 7. 11:53

PC에 이더넷 어댑터가 여러개 설치되어 있거나, Virtual Box 와 같은 Virtual Marchine 이 설치되어 있다면,

IP주소 / 맥어드레스를 구하는 다른 소스로는 엉뚱한 IP/Mac 주소를 반환하는 경우가 있다.

정확한 사용 주소를 가져오는 C# 코드는 아래와 같다.

 

 

IP주소

 

            string ipAddress = "";

            using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0))
            {
                socket.Connect("8.8.8.8", 65530);
                IPEndPoint endPoint = socket.LocalEndPoint as IPEndPoint;
                ipAddress = endPoint.Address.ToString();
            }

 

 

맥어드레스

 

            string macAddress = "";

            ManagementClass mc = new ManagementClass("Win32_NetworkAdapterConfiguration");
            using (ManagementObjectCollection moc = mc.GetInstances())
            {
                foreach (ManagementObject mo in moc)
                {
                    if (mo["MacAddress"] != null)
                    {
                        if ((bool)mo["IPEnabled"] == true)
                        {
                            macAddress = mo["MacAddress"].ToString();
                            break;
                        }

                        mo.Dispose();
                    }
                }

            }
Posted by 헝개
개발팁2022. 3. 29. 14:04

mysql 또는 mariadb 에는 slow query 를 탐지해서 log 파일에 저장해주는 기능이 있다.

DB 튜닝을 위해, 속도가 느린 쿼리를 탐지해서 알려주는 기능은 참 유용한 기능이다.

 

현재, slow query 탐지 기능이 동작중인지 확인하려면, 쿼리창에 아래와 같이 실행해주면 알 수 있다.

 

show variables like 'slow_query_%';
show variables like 'long_query_%';

 

 

slow_query_log 가 OFF 상태로 동작중이지 않다.

 

탐지 기능을 켜려면,

 

mysql 이 설치된 폴더의 data 폴더아래 my.ini 설정 파일을 수정해야 한다.

 

MariaDB 10.5 버전이 설치되어 있다면, 아마도 아래와 같은 경로일 것이다.

 

C:\Program Files\MariaDB 10.5\data\my.ini

 

파일을 편집기로 열어서 아래와 같은 내용을 추가해준다.

 

slow_query_log = 1
slow_query_log_file = C:/slowlog/mysql-slow.log
long_query_time = 5

 

slow_query_log = 1

SLOW QUERY 탐지 기능을 사용한다는 의미이다.

 

slow_query_log_file = C:/slowlog/mysql-slow.log

슬로우 쿼리가 저장되는 경로와 파일명인데, 경로 규칙에 주의 해야 한다.

\ 문자가 아니라 / 문자로 폴더가 구분된다.

 

long_query_time = 5

초단위로 입력하며, 5초 이상의 쿼리를 탐지하겠다는 내용이다.

 

 

mysql 서비스를 재시작해야, 변경된 내용이 적용된다.

 

# mysql
net stop MySQL & net start MySQL
# mariadb
net stop mariadb & net start mariadb

 

윈도우의 서비스 항목에서 재시작하거나, command 창에서 재시작 해도 된다.

 

 

 

이제 변경된 설정을 확인해보면 된다.

 

show variables like 'slow_query_%';
show variables like 'long_query_%';

 

 

설정한 경로에 log 파일도 생성되었다.

 

 

슬로우쿼리 탐지가 잘 되는지 테스트 쿼리를 날려서 확인해 볼 수 있다.

 

SELECT SLEEP(5), 'slow query test.';

 

 

슬로우쿼리가 탐지될때마다, 로그 파일의 뒤에 계속 추가가 된다.

 

여기까지만 해도, 주기적으로 로그파일을 열어보고, 확인하면 되지만, 실시간으로 알림을 받을 수 있는 방법이 있다.

 

C# 의 FileSystemWatcher 를 이용하면, 파일의 내용이 변경되는걸 감지해서, slack 이나 naverworks 등의 메신저로 단체 채팅방에 알림메세지를 발송 할 수 있다.

 

 

FileSystemWatcher watcher = new FileSystemWatcher();

watcher.Filter = "mysql-slow.log";
watcher.Path = "C:\\slowlog\\";
watcher.IncludeSubdirectories = false;


watcher.NotifyFilter = NotifyFilters.DirectoryName |
   NotifyFilters.LastWrite |
   NotifyFilters.FileName |
   NotifyFilters.Size;

watcher.Changed += Watcher_Changed;
watcher.EnableRaisingEvents = true;

 

그리고, 파일에 추가되는 내용만 알림을 해야 하기 때문데, 파일 스트림의 position 을 전역변수로 기록해두자.

 

FileInfo fileinfo = new FileInfo(this.logFullPath);

if (this.streamPosition > fileinfo.Length)
{
       this.streamPosition = fileinfo.Length;
}            

 

Watcher_Changed 이벤트에서는 파일이 변경되었으니, append 된 내용만 알림을 보내면 된다.

 

using (FileStream stream = fileinfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
  stream.Position = this.streamPosition;

  using (StreamReader reader = new StreamReader(stream))
  {
    string mesageContent = reader.ReadToEnd()

    // MessageSend(messageContent);
  }
}

 

이렇게 해서 테스트를 해보면, 실시간 알림이 안오고, 한참 있다가 알림이 오게 되는걸 볼 수 있다.

 

이유는, 윈도우 시스템 (NTFS) 이 파일에 쓰기 버퍼를 사용하기 때문이다.

즉, 일정한 시간이 지나야 버퍼를 실제 물리적인 파일에 저장하기 때문이다.

 

윈도우 시스템이 실시간으로 파일을 변경하도록 하기 위해서는

로그파일을 Open / Close 를 주기적으로 해주면 된다.

 

 

try
{
  // NTFS 의 파일 캐싱으로 인해 로그파일이 변화를 watcher 가 즉시 탐지하지 못한다.
  // 파일 변화를 즉시 감지하기 위해 open 모드로 읽고 닫기를 한다.
  File.Open(this.logFullPath, FileMode.Open).Close();
}
catch (Exception)
{
}

 

try catch 를 쓰는 이유는, 로그 파일을 mysql 이 잡고 있기 때문에, Exception 이 발생하기 때문이다.

하지만, 이렇게 주기적으로 해당 파일을 건들어 주면, 윈도우 시스템이 버퍼를 파일로 저장하는 효과를 얻을 수 있어,

실시간 파일 감시가 가능해지기 때문이다.

 

이렇게 해서, 프로그램을 돌려놓고, slow query 를 발생해보면, 알림이 오는것을 확인할 수 있다.

 

 

네이버웍스 메신저르 알림을 보낸 샘플이다.

 

주의할점은 슬로우 쿼리는 트랜잭션 단위의 쿼리가 아니라, 쿼리문장 단위로 탐지를 한다는 점이다.

 

 

Posted by 헝개