NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 팁 앤 테크
  • 매뉴얼

    팁과 테크니컬 노하우를 확인하세요.

    팁 앤 테크

    팁과 테크니컬 노하우를 확인하세요.

    본 사이트의 컨텐츠는 저작권법의 보호를 받으므로 무단 복사, 게재, 배포 등을 금합니다.

    에디터 2부 - 엔지엠 키보드 매크로 만들기. (프레임워크와 에디터)

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 요즘은 파이썬으로 매크로를 만들고 있는데요. 확실히 GUI를 구성하고, 상호 작용하는 UX를 만드는건 C#이 가장 편한거 같습니다. 아마 다른 GUI 제작 툴과 비교해보면 확실하게 체감할 수 있을겁니다. 아무래도 윈도우 계열이다보니 호환성이나 작업 환경이 뛰어날 수 밖에 없죠^^; 엔지엠도 디자이너를 이용하면 Visual Studio의 WinForms의 강력한 기능들을 활용할 수 있습니다. 자~ 이 예제를 학습하려면 [ 엔지엠 프레임워크 개발 환경 구성하기 ]를 먼저 읽어보고 오셔야 합니다. Visual Studio를 실행하고, 아래와 같이 코드를 작성하세요. 그리고~ 실행(Ctrl+Alt+F8) 해보세요.

    #region 참조
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Mouse = NGM.Utility.MouseKeyboardManager.MouseSimulator;
    using Keyboard = NGM.Utility.MouseKeyboardManager.KeyboardSimulator;
    using System.Windows.Forms;
    using System.Drawing;
    #endregion
    
    namespace ConsoleApp1
    {
        class Program
        {
            static void Main(string[] args)
            {
                // 매인뷰를 생성합니다.
                var mv = new Designer.ComponentDefault.MainView();
    
                // 모든 로그를 출력하도록 변경해줍니다.
                DisplayAllLog(mv);
    
                // 로그가 기록되면 이벤트를 발생 시킵니다.
                mv.LogReceived += Mv_LogReceived;
    
                // 매크로 엔진에서 가상으로 실행할 스크립트 뷰를 생성합니다. 
                // isBackground 파라메터를 false로 명시하지 않으면 뷰를 생성하지 않고 백그라운드로 실행됩니다.
                var sv = new Designer.Component.ScriptView(mv, "/Dummy.ngs");
    
                // 스크립트 뷰는 액션을 순차적으로 실행하기 위한 TreeView를 제공합니다.
                TreeNode tn = sv.TreeView.Nodes.Add("M", "Mouse");
    
                // 내문서를 클릭합니다.
                tn.Tag = new NGM.Models.Mouse.ClickModel() {
                    BeforeDelay = 500,
                    FunctionKey = NGM.Definition.FunctionKey.MOUSE_CLICK,
                    MousePoint = new Point(40, 30) };
    
                tn = sv.TreeView.Nodes.Add("M", "Mouse");
    
                // 내 PC를 클릭합니다.
                tn.Tag = new NGM.Models.Mouse.ClickModel() {
                    BeforeDelay = 500,
                    FunctionKey = NGM.Definition.FunctionKey.MOUSE_CLICK,
                    MousePoint = new Point(40, 110) };
    
                tn = sv.TreeView.Nodes.Add("M", "Mouse");
    
                // 네트워크를 클릭합니다.
                tn.Tag = new NGM.Models.Mouse.ClickModel() {
                    BeforeDelay = 500,
                    FunctionKey = NGM.Definition.FunctionKey.MOUSE_CLICK,
                    MousePoint = new Point(40, 200) };
    
                tn = sv.TreeView.Nodes.Add("M", "Mouse");
    
                // 휴지통을 클릭합니다.
                tn.Tag = new NGM.Models.Mouse.ClickModel() {
                    BeforeDelay = 500,
                    FunctionKey = NGM.Definition.FunctionKey.MOUSE_CLICK,
                    MousePoint = new Point(40, 280) };
    
                // 스크립트를 실행합니다.
                sv.Play();
    
                // 콘솔 출력을 확인하기 위해 대기합니다. 아무키나 누르면 프로그램이 종료됩니다.
                Console.ReadLine();
            }
    
            static void DisplayAllLog(NGM.Interface.IMainView mv)
            {
                mv.IsOutput = true;
                mv.ShowApplicationOutput = true;
                mv.ShowConditionOutput = true;
                mv.ShowDebugOutput = true;
                mv.ShowDefaultOutput = true;
                mv.ShowFunctionOutput = true;
                mv.ShowHardwareOutput = true;
                mv.ShowKeyboardOutput = true;
                mv.ShowMemoryOutput = true;
                mv.ShowMouseOutput = true;
                mv.ShowTimeOutput = true;
            }
    
            private static void Mv_LogReceived(object sender, NGM.Events.LogWriteArgs e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }

    toqpJej.gif

     

     

    이제 키보드 관련된 내용을 알아보도록 하겠습니다. 게임 매크로나 업무용 자동화 매크로 또는 주식을 자동으로 매매하려는 분들은 이미지 서치와 마우스 클릭만으로도 충분히 반자동 매크로를 만들 수 있을겁니다. 완전 자동화를 하려면 이미지 서치뿐만 아니라 키보드와 OCR(광학 문자 인식, Optical Character Recognition)이 필요합니다. 이외에도 다크넷의 욜로나 예외 상황에 대처할 수 있는 추가적인 응용 능력도 갖춰야 합니다. 대부분은 엔지엠 에디터를 사용하면 프로그래밍을 몰라도 완전한 무인 자동화를 만들 수 있습니다. 자~ 아래 코드를 붙여넣기 하고 실행 해볼까요?

    #region 참조
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Mouse = NGM.Utility.MouseKeyboardManager.MouseSimulator;
    using Keyboard = NGM.Utility.MouseKeyboardManager.KeyboardSimulator;
    using System.Windows.Forms;
    using System.Drawing;
    #endregion
    
    namespace ConsoleApp1
    {
        class Program
        {
            static void Main(string[] args)
            {
                System.Threading.Thread.Sleep(1000);
    
                // 메모장을 클릭합니다.
                Mouse.Position = new Point(150, 100);
                Mouse.Click(Mouse.MouseButton.Left);
    
                // 메모장에 텍스트를 입력합니다.
                Keyboard.Write("NGMsoftware");
    
                // 콘솔 출력을 확인하기 위해 대기합니다. 아무키나 누르면 프로그램이 종료됩니다.
                Console.ReadLine();
            }
        }
    }

    BYWoTTg.gif

     

     

    비활성 모드로 텍스트를 입력하는건 아니라서 메모장을 한번 클릭해줘야 합니다. 해당 창이 활성화되어야 키보드 입력이 들어가니까요~ 좀 더 천천히 텍스트를 입력하려면 어떻게 해야 할까요? 아래와 같이 인터벌(Interval, 간격: 시간적인)을 설정하면 됩니다. 엔지엠 에디터는 랜덤하게 타이핑하듯이 작성할 수 있지만 코딩 방식으로 하려면 추가적인 조치를 해줘야 합니다. 키보드 down, up으로 char를 넘겨서 처리하면 가능합니다^^; 아래와 같이 250을 입력하면, 0.25초마다 한글자씩 입력합니다.

    // 메모장에 텍스트를 입력합니다.
    Keyboard.Write("NGMsoftware", 250);

    VCQF0vl.gif

     

     

    좀 더 원할한 테스트를 위해 콘솔창이 표시되지 않도록 코드를 수정해주세요. 콘솔창이 표시되기 때문에 마우스가 메모장을 클릭하지 못하는 경우가 발생합니다.

    #region 참조
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Mouse = NGM.Utility.MouseKeyboardManager.MouseSimulator;
    using Keyboard = NGM.Utility.MouseKeyboardManager.KeyboardSimulator;
    using System.Windows.Forms;
    using System.Drawing;
    using System.Runtime.InteropServices;
    #endregion
    
    namespace ConsoleApp1
    {
        class Program
        {
            #region Windows API
            [DllImport("kernel32.dll")]
            static extern IntPtr GetConsoleWindow();
    
            [DllImport("user32.dll")]
            static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
    
            const int SW_HIDE = 0;
            const int SW_SHOW = 1;
            #endregion
    
            static void Main(string[] args)
            {
                // 콘솔창이 표시되지 않도록 합니다.
                ShowWindow(GetConsoleWindow(), SW_HIDE);

     

     

    조합키를 입력하려면 어떻게 해야 할까요? Ctrl+A, Ctrl+C, Ctrl+V와 같은 키들을 말합니다. 이외에도 조합키는 많지만, 예제로 사용하기에는 가장 좋은 단축키들입니다. 메모장에 있는 내용들을 전체 선택하고, 복사한 후 다른 메모장에 붙여넣기 하는 방법입니다. 아래 코드를 붙여넣기하고 실행(F5) 해보세요.

    #region 참조
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Mouse = NGM.Utility.MouseKeyboardManager.MouseSimulator;
    using Keyboard = NGM.Utility.MouseKeyboardManager.KeyboardSimulator;
    using System.Windows.Forms;
    using System.Drawing;
    using System.Runtime.InteropServices;
    #endregion
    
    namespace ConsoleApp1
    {
        class Program
        {
            #region Windows API
            [DllImport("kernel32.dll")]
            static extern IntPtr GetConsoleWindow();
    
            [DllImport("user32.dll")]
            static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
    
            const int SW_HIDE = 0;
            const int SW_SHOW = 1;
            #endregion
    
            static void Main(string[] args)
            {
                // 콘솔창이 표시되지 않도록 합니다.
                ShowWindow(GetConsoleWindow(), SW_HIDE);
    
                // 메모장을 클릭합니다.
                Mouse.Position = new Point(300, 100);
                Mouse.Click(Mouse.MouseButton.Left);
    
                // 메모장에서 전체 선택합니다.
                Keyboard.KeyDown(Keys.Control);
                Keyboard.KeyDown(Keys.A);
                Keyboard.KeyUp(Keys.A);
                Keyboard.KeyUp(Keys.Control);
    
                // 메모장 내용을 클립보드로 복사 합니다.
                Keyboard.KeyDown(Keys.Control);
                Keyboard.KeyDown(Keys.C);
                Keyboard.KeyUp(Keys.C);
                Keyboard.KeyUp(Keys.Control);
    
                // 메모장을 클릭합니다.
                Mouse.Position = new Point(300, 600);
                Mouse.Click(Mouse.MouseButton.Left);
    
                // 클립보드 내용을 메모장에 붙여넣기 합니다.
                Keyboard.KeyDown(Keys.Control);
                Keyboard.KeyDown(Keys.V);
                Keyboard.KeyUp(Keys.V);
                Keyboard.KeyUp(Keys.Control);
    
                // 콘솔 출력을 확인하기 위해 대기합니다. 아무키나 누르면 프로그램이 종료됩니다.
                Console.ReadLine();
            }
        }
    }

    j7HIs55.gif

     

     

    너무 빠르게 동작해서 확인이 어려울 수 있습니다. 파이썬과 비교해보면... 속도 차이를 느낄 수 있을겁니다. 아무튼, 위쪽 메모장에서 전체 선택(Ctrl+A) 후 복사(Ctrl+C)해서 클립보드에 내용을 저장 했습니다. 그리고, 아래 메모장을 클릭한 후 붙여넣기(Ctrl+V)를 수행 했습니다. 위 코드를 보면 아시겠지만, 너무 정직하게 작성한 코드라서 약간 불편합니다-_-; 아래와 같이 좀 더 편리하게 사용할 수 있도록 래퍼를 제공합니다.

    #region 참조
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Mouse = NGM.Utility.MouseKeyboardManager.MouseSimulator;
    using Keyboard = NGM.Utility.MouseKeyboardManager.KeyboardSimulator;
    using System.Windows.Forms;
    using System.Drawing;
    using System.Runtime.InteropServices;
    #endregion
    
    namespace ConsoleApp1
    {
        class Program
        {
            #region Windows API
            [DllImport("kernel32.dll")]
            static extern IntPtr GetConsoleWindow();
    
            [DllImport("user32.dll")]
            static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
    
            const int SW_HIDE = 0;
            const int SW_SHOW = 1;
            #endregion
    
            static void Main(string[] args)
            {
                // 콘솔창이 표시되지 않도록 합니다.
                ShowWindow(GetConsoleWindow(), SW_HIDE);
    
                // 메모장을 클릭합니다.
                Mouse.Position = new Point(300, 100);
                Mouse.Click(Mouse.MouseButton.Left);
    
                // 메모장에서 전체 선택합니다.
                Keyboard.SimulateStandardShortcut(Keyboard.StandardShortcut.SelectAll);
    
                // 메모장 내용을 클립보드로 복사 합니다.
                Keyboard.SimulateStandardShortcut(Keyboard.StandardShortcut.Copy);
    
                // 메모장을 클릭합니다.
                Mouse.Position = new Point(300, 600);
                Mouse.Click(Mouse.MouseButton.Left);
    
                // 클립보드 내용을 메모장에 붙여넣기 합니다.
                Keyboard.SimulateStandardShortcut(Keyboard.StandardShortcut.Paste);
    
                // 콘솔창을 표시합니다.
                ShowWindow(GetConsoleWindow(), SW_SHOW);
    
                // 콘솔 출력을 확인하기 위해 대기합니다. 아무키나 누르면 프로그램이 종료됩니다.
                Console.ReadLine();
            }
        }
    }

    Z1ucbJB.gif

     

     

    코드량을 많이 줄었는데요. 동작은 동일합니다^^; 마지막으로 알아볼 함수는 press입니다. spy++을 이용해서 윈도우 메시지를 확인 해보면 키보드는 keyDown, keyPress, keyUp으로 동작하는걸 알 수 있습니다. 위에서 조합키를 만들 때 keyDown(Ctrl), keyDown(A), KeyUp(A), KeyUp(Ctrl)과 같이 만들었습니다. 대부분은 정상 동작할겁니다. 하지만, 윈도우 메시지를 인터셉트해서 분석하는 소프트웨어라면 명령이 수행되지 않을수도 있습니다. 대부분의 매크로 프로그램들이 단순하게 작성되기 때문이죠. 그래서 처음 코드를 아래와 같이 작성하는게 더 좋습니다.

    #region 참조
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Mouse = NGM.Utility.MouseKeyboardManager.MouseSimulator;
    using Keyboard = NGM.Utility.MouseKeyboardManager.KeyboardSimulator;
    using System.Windows.Forms;
    using System.Drawing;
    using System.Runtime.InteropServices;
    #endregion
    
    namespace ConsoleApp1
    {
        class Program
        {
            #region Windows API
            [DllImport("kernel32.dll")]
            static extern IntPtr GetConsoleWindow();
    
            [DllImport("user32.dll")]
            static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
    
            const int SW_HIDE = 0;
            const int SW_SHOW = 1;
            #endregion
    
            static void Main(string[] args)
            {
                // 콘솔창이 표시되지 않도록 합니다.
                ShowWindow(GetConsoleWindow(), SW_HIDE);
    
                // 메모장을 클릭합니다.
                Mouse.Position = new Point(300, 100);
                Mouse.Click(Mouse.MouseButton.Left);
    
                // 메모장에서 전체 선택합니다.
                Keyboard.KeyDown(Keys.Control);
                Keyboard.KeyPress(Keys.A);
                Keyboard.KeyUp(Keys.Control);
    
                // 메모장 내용을 클립보드로 복사 합니다.
                Keyboard.KeyDown(Keys.Control);
                Keyboard.KeyPress(Keys.C);
                Keyboard.KeyUp(Keys.Control);
    
                // 메모장을 클릭합니다.
                Mouse.Position = new Point(300, 600);
                Mouse.Click(Mouse.MouseButton.Left);
    
                // 클립보드 내용을 메모장에 붙여넣기 합니다.
                Keyboard.KeyDown(Keys.Control);
                Keyboard.KeyPress(Keys.V);
                Keyboard.KeyUp(Keys.Control);
    
                // 콘솔창을 표시합니다.
                ShowWindow(GetConsoleWindow(), SW_SHOW);
    
                // 콘솔 출력을 확인하기 위해 대기합니다. 아무키나 누르면 프로그램이 종료됩니다.
                Console.ReadLine();
            }
        }
    }

     

     

    이보다 더~ 정직하게 소프트웨어 자동화가 아닌것처럼 하려면 전부 keyDown, keyPress, keyUp으로 만들어주는게 좋습니다. 실제 하드웨어 키보드의 5초간 키를 누르고 있는 윈도우 메시지를 보면 아래와 같이 입력되는걸 알 수 있습니다.

    down press press press ... up

     

    소프트웨어로 키보드를 누르고 있으면 보통은 down과 up만 존재합니다. 이걸로 소프트웨어 신호인지 금방 알 수 있게됩니다. 키를 누르고 있는 효과를 내려면 아래와 같이 코드를 변경할 수 있습니다. 개발자라면 얼마든지 마음대로 원하는 동작을 만들어서 사용할 수 있습니다.

    #region 참조
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Mouse = NGM.Utility.MouseKeyboardManager.MouseSimulator;
    using Keyboard = NGM.Utility.MouseKeyboardManager.KeyboardSimulator;
    using System.Windows.Forms;
    using System.Drawing;
    using System.Runtime.InteropServices;
    #endregion
    
    namespace ConsoleApp1
    {
        class Program
        {
            #region Windows API
            [DllImport("kernel32.dll")]
            static extern IntPtr GetConsoleWindow();
    
            [DllImport("user32.dll")]
            static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
    
            const int SW_HIDE = 0;
            const int SW_SHOW = 1;
            #endregion
    
            static void Main(string[] args)
            {
                // 콘솔창이 표시되지 않도록 합니다.
                ShowWindow(GetConsoleWindow(), SW_HIDE);
    
                // 메모장을 클릭합니다.
                Mouse.Position = new Point(300, 100);
                Mouse.Click(Mouse.MouseButton.Left);
    
                // 메모장에서 키보드를 누르고 있는 상태입니다.
                for (int i = 0; i < 10; i++)
                {
                    Keyboard.KeyPress(Keys.A);
                }
    
                // 콘솔창을 표시합니다.
                ShowWindow(GetConsoleWindow(), SW_SHOW);
    
                // 콘솔 출력을 확인하기 위해 대기합니다. 아무키나 누르면 프로그램이 종료됩니다.
                Console.ReadLine();
            }
        }
    }

    o9dhq3G.gif

     

     

    좀 더 응용하면 이렇게도 작성할 수 있습니다. foreach를 이용해서 텍스트를 입력합니다. 더 간단해졌죠^^;

    #region 참조
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Mouse = NGM.Utility.MouseKeyboardManager.MouseSimulator;
    using Keyboard = NGM.Utility.MouseKeyboardManager.KeyboardSimulator;
    using System.Windows.Forms;
    using System.Drawing;
    using System.Runtime.InteropServices;
    #endregion
    
    namespace ConsoleApp1
    {
        class Program
        {
            #region Windows API
            [DllImport("kernel32.dll")]
            static extern IntPtr GetConsoleWindow();
    
            [DllImport("user32.dll")]
            static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
    
            const int SW_HIDE = 0;
            const int SW_SHOW = 1;
            #endregion
    
            static void Main(string[] args)
            {
                // 콘솔창이 표시되지 않도록 합니다.
                ShowWindow(GetConsoleWindow(), SW_HIDE);
    
                // 메모장을 클릭합니다.
                Mouse.Position = new Point(300, 100);
                Mouse.Click(Mouse.MouseButton.Left);
    
                // 메모장에서 키보드를 누르고 있는 상태입니다.
                foreach (char c in "NGMsoftware")
                {
                    Keyboard.Write(c.ToString());
                }
    
                // 콘솔창을 표시합니다.
                ShowWindow(GetConsoleWindow(), SW_SHOW);
    
                // 콘솔 출력을 확인하기 위해 대기합니다. 아무키나 누르면 프로그램이 종료됩니다.
                Console.ReadLine();
            }
        }
    }

    15dUJW3.gif

     

     

    마지막으로 파이썬에서는 할 수 없었던 기능! 한글 입력입니다. 엔지엠 에디터나 디자이너, 프레임워크는 한글 입력도 쉽게 처리가 가능합니다. 파이썬의 경우 한글 입력이 불가능하기 때문에 클립보드에 텍스트를 저장하고 붙여넣기를 사용해야 했습니다. 하지만, 엔지엠 제품들은 한글과 영어를 자동으로 인식해서 변환해주고, 입력되는 프로그램의 IME모드를 감지하여 스마트하게 입력시킬 수 있습니다.

    #region 참조
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Mouse = NGM.Utility.MouseKeyboardManager.MouseSimulator;
    using Keyboard = NGM.Utility.MouseKeyboardManager.KeyboardSimulator;
    using System.Windows.Forms;
    using System.Drawing;
    using System.Runtime.InteropServices;
    #endregion
    
    namespace ConsoleApp1
    {
        class Program
        {
            #region Windows API
            [DllImport("kernel32.dll")]
            static extern IntPtr GetConsoleWindow();
    
            [DllImport("user32.dll")]
            static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
    
            const int SW_HIDE = 0;
            const int SW_SHOW = 1;
            #endregion
    
            static void Main(string[] args)
            {
                // 콘솔창이 표시되지 않도록 합니다.
                ShowWindow(GetConsoleWindow(), SW_HIDE);
    
                // 메모장을 클릭합니다.
                Mouse.Position = new Point(300, 100);
                Mouse.Click(Mouse.MouseButton.Left);
    
                // 메모장에서 키보드를 누르고 있는 상태입니다.
                foreach (char c in "안녕하세요. NGMsoftware입니다.")
                {
                    Keyboard.Write(c.ToString());
                }
    
                // 콘솔창을 표시합니다.
                ShowWindow(GetConsoleWindow(), SW_SHOW);
    
                // 콘솔 출력을 확인하기 위해 대기합니다. 아무키나 누르면 프로그램이 종료됩니다.
                Console.ReadLine();
            }
        }
    }

    O57N4xl.gif

     

     

    엔지엠 디자이너 함수를 사용하면 좀 더 다양하고 풍부한 옵션을 사용할 수 있습니다. 또한, 동작에 대해 아웃풋을 받아서 별도로 처리하거나 각각의 액션마다 실행전, 실행후 이벤트를 처리할수도 있습니다. 다음에는 여러분들이 기다리던~ 이미지 서치와 이미지 매치애 대해 알아볼께요. 마우스, 키보드, 이미지 서치 매크로를 만들 수 있으면, 누구나 쉽게 자동 사냥이나 자동 매매 또는 거래를 할 수 있을겁니다. 이외에도 단순 반복적인 업무를 자동화하고 결과를 편하게 받아볼 수 있죠. 엔지엠 프레임워크를 이용하면 마우스와 키보드를 제어하는 매크로는 한두시간이면 뚝딱 만들겁니다. 몸값이 비싼 개발자에게 의뢰하기 보다는 직접 만들어보는건 어떨까요^^?

     

    개발자에게 후원하기

    MGtdv7r.png

     

    추천, 구독, 홍보 꼭~ 부탁드립니다.

    여러분의 후원이 빠른 귀농을 가능하게 해줍니다~ 답답한 도시를 벗어나 귀농하고 싶은 개발자~

    감사합니다~

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천0 비추천0

    댓글목록

    등록된 댓글이 없습니다.