2026년 4월 18일 토요일

파이썬 GUI 환경 곁다리로 배포하기

예전에 mfc나 윈폼으로 메시지 드리븐 기반의 GUI 프로그래밍을 해오던 경험을
해온 나는 GUI라는것이 명령기반으로만 생각을 해왔다..

그러다가 안드로이드가 나왔고 xml기반의 뷰드로잉이 희안하다고 생각했고
플러터가 나와서 메서드 체이닝식 랜더링을 보면서 더이상 디자인에 대한 부분이 
명령기반이 아닌 분리되어 랜더링되게 만들어야 한다는 생각으로 바뀌었다.

그래서 아발로니아 같은것을 다루면서 이런 생각이 완전히 바뀌었다.
헌데.. 이런 구조를 처리할때는 디자인 패턴이라는것이 중요하다 MVVM이나 
DI 뷰협상 이런것들..
그런데 이런것들은 당연히 좋은데 빨리 뭔가를 GUI로 만들고 싶어하는 상황에서는
이러한 아키텍쳐가 오히려 에너지를 빼는 일이 되어 버릴 수 있다..

이상황에서는 드레그 드롭방식으로 뷰를 만들고 명령기반 코딩방식이 유의미 
할것이다. 헌데 나한테는 이 윈폼이 좋기는한데 뭔가 인스턴스 적인 느낌이 
없달까?? 이런 느낌이다. 빌드도 해야하고 요즘에는 실행시 런타임도 깔아야한다.

해서 이 중간 어디쯤을 커버칠수 있는부분을 찾다가 PyQt에 머무르게 되었다.
파이썬도 GUI라고 하면 tkinter, wxpython, kivy 이런것들이 많다..
하지만 지금에 와서는 디자이너가 있고 파일로 분리되는 깔끔함은 사실상 PyQt로
대동단결 되는 느낌이다.  tkinter에서 pack() 이런거 나열하고 있으면
디자인도 구린데 이렇게 까지 해야하나.. 이런 생각이 든다.
그래서 결론적으로 pyqt를 이용할 생각인데 이게 배포하는게 귀찮을 수 있다.
중간 어디쯤인 프로그램 답게 그냥 코드는 노출되어 상관이 없을것이고
콘솔은 사용자가 힘들게 접근할테니까.. 이 pyqt를 잘 배포하는것이 문제로 떠올랐다.


[본론으로 들어가서]

파이썬이 깔려있다는 전재하에 윈도우로 배포(이 개념이 리눅스도 먹힐것이다.) 한다고
가정하면 bat파일로 실행시킬때 가상환경인 venv폴더를 만들고 그때 reqlist.txt
파일을 읽어서 그 안의 내용을 pip install 처리하면 될것이다.
그리고 다음 실행시에는 그 vevn폴더가 있고 그 안에 라이브러리가 있을테니 바로 실행하면
되겠다.  그리고 실행파일은 main.py이다.



- bat파일의 내용 -

@echo off
pushd "%~dp0"

if not exist "venv\installed.txt" (
    echo Ready for Launch..
    rmdir /s /q venv 2>nul
    python -m venv venv
    call venv\Scripts\activate

    python -m pip install --upgrade pip
    if exist "reqlist.txt" pip install -r reqlist.txt    
    echo OK > venv\installed.txt
) else (
    call venv\Scripts\activate
)

start pythonw main.py
exit



이렇게 작성해서 전달하면 GUI 기반으로 프로그램을 전달할수 있다.

아래는 main.ui 파일을 사용하는 기본 윈도우 프레임틀  
(QMainWindow의 minimumsize를 설정해주기 잊지마~)
(reqlist.txt 에는 기본적으로 PySide6 가 들어가 있어야겠쥬)


from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtUiTools import QUiLoader
from PySide6.QtCore import QFile
import sys

#
class ColorApp(QMainWindow):
    def __init__(self):
        super().__init__()        
        #        
        loader = QUiLoader()
        file = QFile("main.ui")
        if file.open(QFile.ReadOnly):
            self.ui_content = loader.load(file)            
            self.setCentralWidget(self.ui_content)
            file.close()        
        
    # 종료 이벤트
    def closeEvent(self, event):
        QApplication.quit()
        event.accept()

#
if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = ColorApp()
    window.show()
    sys.exit(app.exec())


2026년 4월 16일 목요일

MSSQL 시간 관련 처리 쿼리

[시간 관련 함수]

현재시간    |    GETDATE() or SYSDATETIME()    |    현재시간을 추출함
형식변환(날짜)    |    CAST(GETDATE() as DATE)    |    '2026-04-17' 형태만 남김
형식변화(문자)    |    CONVERT(VARCHAR, GETDATE(), 120)    |    스타일로변환
시간차이    |    DATEDIFF(보간값, 전시간, 후시간)    |    두시간사이 차이를 보간으로 반환
시간더하기    |    DATEADD(보간값, 9, GETDATE())    |    특정 보간시간의 합산분처리
특정부분추출    |    DATEPART(보간값, GETDATE())    |    시간에서 보간값을 뽑아냄

※ 보간값: HOUR, MINUTE, SECOND ※


[Convert 스타일 번호] - Convert(Varchar(x), TimeColumn, StyleNumber)

없음    |    cast(dt as varchar)    |    Apr 17 2026 2:25PM
1번    |    mm/dd/yy    |    04/17/26
3번    |    dd/mm/yy    |    17/04/26
23번    |    yyyy-mm-dd    |    202-04-17
108번    |    hh:mm:ss    |    14:25:19
101번    |    mm/dd/yyyy    |    04/17/2026
110번    |    mm-dd-yyyy    |    04-17-2026
111번    |    yyyy/mm/dd    |    2026-04-17
112번    |    yyyymmdd    |    20260417
120번    |    yyyy-mm-dd hh:mm:ss    |    2026-04-17 14:25:19
121번    |    yyyy-mm-dd hh:mm:ss.fff    |    2026-04-17 14:25:19.000
126번    |    yyyy-mm-ddThh:mm:ss.fff    |    2026-04-17T14:25:19.000    


2026년 3월 25일 수요일

아발로니아 독립스텍

계층 기술 스택 비고
프레젠테이션 Avalonia UI + Noto Sans KR 어떤 환경에서도 변치 않는 UI 가독성

비즈니스 로직 .NET 10.0 (C#) 강력한 타입 안정성과 최신 성능

데이터베이스 PostgreSQL (Npgsql) 엔터프라이즈급 쿼리 성능과 데이터 신뢰성

파일/미디어 ImageSharp, FFmpeg, EPPlus, QuestPDF 생성부터 편집, 출력까지 기기 내 완결

네트워크 SMBLibrary 윈도우 공유 폴더와의 유연한 연결성

2026년 2월 11일 수요일

닷넷으로 소형 리눅스 IOT기기의 아이피v4 변경 하기

요즘에는 라즈베리파이니 오렌지파이니 해서 소형 리눅스 디바이스가 활약하는 일이 잦아졌다.






sudo systemctl daemon-reload
sudo systemctl enable orange-net
sudo systemctl start orange-net


2025년 10월 29일 수요일

파워쉘 톺아보기

파워쉘이 강력하다는것은 전부터 익히 들어 알고 있었다.

Azure 원격관리도 할수 있고 자동화 스크립트 이런걸로 말이다..

그리고 파워쉘을 사용하기 위해 다음과 같은 배치파일 (myapp.ps1 파일을 실행)

PowerShell.exe -executionpolicy bypass -file "%~dp0myapp.ps1"
pause

를 보통 사용할것이다.   명령프롬프트가 없으려면 다음처럼

PowerShell.exe -windowstyle Hidden -executionpolicy bypass -file "%~dp0myapp.ps1"


그런데 얼마전 일을하다가 파워쉘의 다른 모습을 봤는데..

이 파워쉘이라는것이 닷넷프레임워크 4.x 기반위에서 동작하고
C# 문법과 비슷하게 프로그래밍이 가능하다는 기능이 있다는걸 알게됬다.

물론 완전한 인터렉션용 프로그램은 아닐지라도 자동화처리를 함에 있어
스트립트 라인바이라인 형식의 프로그래밍을 사용할수 있다는걸 알았다.

그래서 그 위상이 한층 높아졌는데..
닷넷프레임워크를 사용할수 있으니  윈폼이라던가 Math, Socket을 사용할수 있다.


결과적으로

복잡한로직은 c# 라이브러리로 만들고

이걸 파워쉘에서 불러서 호출한다던가

Bitmap을 이용한 간단한 이미지를 생성한다던가

예를들어 직원 명단이 있는 csv파일을 불러다가

Bitmap을 루프돌면서 명찰 PNG파일을 만들어서

각 이름 폴더에 저장한다던가 dll파일이 있는데 

호출규약이나 구조만 알아낸뒤 이부분만 떼어서 별도로 호출해서

결과를 바로 본다던가..


이런행위들은 이제 별도의 프로그램을 만들지말고 파워쉘로 하면된다.

물론 파이썬도 이런거 할수 있긴하지만 명령창까지 품은

파워쉘이 훨씬 강력하다고 느낀다.

게다가 AI가 판지는 요즈에는 커맨드릿 이라던가 이런거 외우지 않아도

바로바로 질문해서 답을 얻을수 있다.

바이브 AI코딩 + 파워쉘...   장난없다..


게다가 파워쉘로 snaketail을 다음명령으로 대체가능하다.

Get-Content "텍스트파일경로" -Wait -Tail 20


ㅇㅇ


로그폴더내의 로그파일을 작업스케쥴러로 삭제

로그 폴더내에 있는 *.log파일들은 기록은 남긴다는 의미에서 
하드디스크 내에 남아 있어야한다. 그리고 이 로그파일은 아마도
특정일자 별로 파일로 남기는것이 관례처럼 느껴진다.
(yyyy-MM-dd.log)  이런 형식으로 남겨질것이다.

헌데 프로그램이 실행될때 이 로그파일이 너무 많이 쌓여 있거나
보관기간이 만료됐거나 혹은 로그파일 한개가 너무커서 금방 하드디스크를
채울것 같으면 과거 로그파일을 정리해야한다.

물론 프로그램내에서 기간이 만료된 로그파일을 삭제하는 쓰레드를 
별도로 작성해도 되지만.. 다른 사람이 작성했다던가 해당 프로그램의
내용을 수정할 수 없을때는 다른 방법을 써야한다.

그럴때 사용하는게 bat파일을 통해서 특정폴더의 *.log파일을 마지막 
수정시간을 현재시간과 비교해서 보관일자가 넘어가는것을 처리하는 
파일을 다음과 같이 기술한다.

[배치파일 내용]

@echo off

::  설정값 (LOG_DIR 에는 폴더경로  DAYS_TO_KEEP에는 보관일수)
set LOG_DIR=C:\Temp\로그
set DAYS_TO_KEEP=30

:: 삭제 실행
forfiles /P "%LOG_DIR%" /S /M *.log /D -%DAYS_TO_KEEP% /C "cmd /c del /F /Q @path"
echo --- LogFile Cleaned Up ---


이렇게 처리하고
이걸  cleanup.bat파일로 만들어서  c:\Temp 폴더에 넣어놓고
윈도우 작업스케쥴러로 등록해서 매일 오전 3시경에 실행하는걸로 하면
일정기간만 로그를 저장하는 환경이 구축된다.



[스케쥴러 등록방법]

1. 해당파일을 메모장으로 열어서  LOG_DIR  과  DAYS_TO_KEEP
적정히 수정한다.  (LOG_DIR=로그폴더   DAYS_TO_KEEP=보관일수)

2. taskschd.msc 를 실행하여 작업스케줄러로 진입

3. 우측작업패널에서 "기본작업 만들기" 선택하고
이름은 "로그삭제"로 지정
트리거는 "매일"  매 1일  03시 00분 00초
작업은 "프로그램 시작"
프로그램/스크립트는   "C:\Temp\cleanup.bat" 로 
경로를 설정하고 마침

4. 이렇게 등록한 스케줄러를 작업 스케줄러 라이브러리의 
내용에서 더블클릭하여 속성창으로 진입
일반탭 보안옵션에서 "가장 높은 수준의 권한으로 실행"  
체크하고 확인


이렇게 처리하면 된다.

ㅇㅇ

2025년 10월 9일 목요일

오렌지파이에서 C#으로 푸쉬버튼 이벤트 받기..

라즈베리파리를 이용하여 GPIO를 처리하기 위해서 2핀짜리 푸쉬버튼을
GPIO와 GND에 연결하고 System.Device.GPIO 로 버튼을 입력방향으로 만들고
버튼이 눌려짐을 감지하여 등록되어있던 델리케이트를 실행하는 방향으로 처리될것이다.

하지만 오렌지 파이에서는 이 부분이 먹히지 않는다.
아마도 i2c나 UART-ttl에 비해서 GPIO부분이 뭔가 통일이 안된 느낌이다.

해서 어떻게 처리를 할것인가를 고민해보았는데 
결론적으로 오렌지파이에 OS는 데비안이 주력 일것이고..
그렇다면 GPIOD.so를 직접적으로 P/Invoke 방식으로 포인팅하여서
파일의 변화를 감지하여 변화시점에 역호출하는 방식을 이용하면 될듯하다.

그렇게 하기 위해서 일단은 apt install을 처리할것이 있는데
다음 명령으로 라이브러리를 설치해 놓자.

sudo apt update
sudo apt install libgpiod-dev

이렇게 받아놓으면 아래폴더에
/lib/aarch64-linux-gnu/libgpiod.so  파일이 생성되고

이를 이용하여 다음과 같이 DllImport 처리를 한다.


using System;
using System.Runtime.InteropServices;

namespace GpioTest;

/// <summary>
/// 네이티브 바인딩
/// </summary>
public static class NativeBindings
{
    //라이브러리 파일명
    private const string LibGpiod = "libgpiod.so";

    //gpiod.h 의 풀업플레그값
    public const int GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP = 1 << 8;

    // so 파일 메서드 시그니처
    [DllImport(LibGpiod, EntryPoint = "gpiod_chip_open_by_name")]
    public static extern IntPtr GpiodChipOpenByName(string name);

    [DllImport(LibGpiod, EntryPoint = "gpiod_chip_close")]
    public static extern void GpiodChipClose(IntPtr chip);

    [DllImport(LibGpiod, EntryPoint = "gpiod_chip_get_line")]
    public static extern IntPtr GpiodChipGetLine(IntPtr chip, uint offset);

    [DllImport(LibGpiod, EntryPoint = "gpiod_line_request_input_flags")]
    public static extern int GpiodLineRequestInputFlags(IntPtr line, string consumer, int flags);

    [DllImport(LibGpiod, EntryPoint = "gpiod_line_request_input")]
    public static extern int GpiodLineRequestInput(IntPtr line, string consumer);

    [DllImport(LibGpiod, EntryPoint = "gpiod_line_get_value")]
    public static extern int GpiodLineGetValue(IntPtr line);

    [DllImport(LibGpiod, EntryPoint = "gpiod_line_release")]
    public static extern void GpiodLineRelease(IntPtr line);

}


그런다음 이를 사용할 모니터링 클래스


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace GpioTest;

/// <summary>
/// 버튼정보 구조체
/// </summary>
public class GpioButtonConfig
{
    public IntPtr LinePtr = IntPtr.Zero;
    public uint Lineoffset { get; set; } = 0;
    public string Description { get; set; } = "";
    public int LastValue { get; set; } = 1;         // 1 (HIGH): 떼어짐 (풀업 가정)
    public Action<uint>? d_Actor = null;
}

/// <summary>
/// Gpio 모니터
/// </summary>
public class GpioMonitor: IDisposable
{
    private IntPtr _chipPtr = IntPtr.Zero;
    private string ChipName = "";
    private List<GpioButtonConfig> _buttonConfigs = new List<GpioButtonConfig>();
    
    // 전체 버튼의 디바운스 타임
    private const int DebounceDelayMs = 250;
    private DateTime _lastPushDebounceTime = DateTime.MinValue;
    private DateTime _lastPullDebounceTime = DateTime.MinValue;

    /// <summary>
    /// 초기화처리
    /// </summary>
    public bool Initialize(List<GpioButtonConfig> a_configs, string a_chipName = "gpiochip1")
    {
        //
        _buttonConfigs.AddRange(a_configs);
        ChipName = a_chipName;

        //
        try
        {
            // 1. 칩열기
            _chipPtr = NativeBindings.GpiodChipOpenByName(ChipName);
            if (_chipPtr == IntPtr.Zero)
            {
                throw new Exception($"칩 {ChipName} 열기 실패. 권한 문제확인.");
            }

            // 2. 등록된 모든 라인 초기화 및 요청
            for (int i = 0; i < _buttonConfigs.Count; i++)
            {
                GpioButtonConfig one_config = _buttonConfigs[i];
                IntPtr linePtr = NativeBindings.GpiodChipGetLine(_chipPtr, one_config.Lineoffset);
                if (linePtr == IntPtr.Zero)
                {
                    Console.WriteLine($"경고: 라인 {one_config.Lineoffset} 가져오기 실패. 건너뜀");
                    continue;
                }

                // 3. 라인 요청 (Input + Pull Up)
                int flags = NativeBindings.GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP;
                int ret = NativeBindings.GpiodLineRequestInputFlags(linePtr, one_config.Description, flags);
                if (ret < 0)
                {
                    Console.WriteLine($"경고: 라인 {one_config.Lineoffset} 풀업요청 실패.");
                }

                //라인포인터 추가 및 액션 바인딩
                one_config.LinePtr = linePtr;
                one_config.LastValue = NativeBindings.GpiodLineGetValue(linePtr);
                one_config.d_Actor += (lineno) => System.Console.WriteLine($"눌린 라인번호{lineno}번.");
                Console.WriteLine($"라인 {one_config.Lineoffset} ({one_config.Description}) 설정 완료.");
            }

            //
            if (!_buttonConfigs.Any())
            {
                throw new Exception("모니터링할 유효한 GPIO라인이 없습니다.");
            }

            //
            return true;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"\n 초기화 실패: {ex.Message}");
            Dispose();
            return false;
        }
    }


    /// <summary>
    /// 모니터링 시작
    /// </summary>
    public void StartMonitoring()
    {
        //루핑
        while (true)
        {
            foreach (var item in _buttonConfigs.Where(c => c.LinePtr != IntPtr.Zero))
            {
                int currentValue = NativeBindings.GpiodLineGetValue(item.LinePtr);

                //
                if (currentValue == 0 && item.LastValue == 1)
                {
                    //버튼 눌림 감지
                    DateTime nowtime = DateTime.Now;
                    if ((nowtime - _lastPushDebounceTime).TotalMilliseconds > DebounceDelayMs)
                    {
                        Console.WriteLine($"버튼 눌림 감지: {item.Description}");
                        item.d_Actor?.Invoke(item.Lineoffset);
                        _lastPushDebounceTime = nowtime;
                    }
                }
                else if (currentValue == 1 && item.LastValue == 0)
                {
                    //버튼 떼짐 감지
                    DateTime nowtime = DateTime.Now;
                    if ((nowtime - _lastPullDebounceTime).TotalMilliseconds > DebounceDelayMs)
                    {
                        Console.WriteLine($"버튼 떼임 감지: {item.Description}");
                        _lastPullDebounceTime = nowtime;
                    }
                }

                // 상태 업데이트
                item.LastValue = currentValue;
            }
            //한바퀴 다돌고 5ms Sleep
            Thread.Sleep(5);    
        }
    }

    /// <summary>
    /// 자원 해지
    /// </summary>
    public void Dispose()
    {
        foreach (var config in _buttonConfigs)
        {
            NativeBindings.GpiodLineRelease(config.LinePtr);
        }
        if (_chipPtr != IntPtr.Zero)
        {
            NativeBindings.GpiodChipClose(_chipPtr);
            Console.WriteLine("GPIO 자원 해제 완료.");
        }
    }
}


이렇게 한후 콘솔 메인에서 다음처리 실행해보면


using System;
using System.Collections.Generic;

namespace GpioTest;


/// <summary>
/// 메인 클래스
/// </summary>
public class MainApp
{
    /// <summary>
    /// 주진입점
    /// doent빌드 명령어: dotnet publish -c Release -r linux-arm64
    /// </summary>
    public static void Main(string[] args)
    {
        //모니터링할 버튼 리스트 정의
        //(PC6=70)
        var buttonConfigurations = new List<GpioButtonConfig>
        {
            new GpioButtonConfig { Lineoffset = 70, Description = "Button PC6" },
        };

        // GpioMonitor 인스턴스를 using 구문으로 생성하여 자동 해제되도록 처리
        using var monitor = new GpioMonitor();
        bool bsetup = monitor.Initialize(buttonConfigurations);
        if (!bsetup)
        {
            System.Console.WriteLine("GPIO버튼 초기화 실패!!");
            Environment.Exit(0);
        }

        //콘솔 종료키 감지
        Console.CancelKeyPress += (sender, e) =>
        {
            System.Console.WriteLine(Environment.NewLine + "종료요청 감지..");
            e.Cancel = true;
            Environment.Exit(0);
        };

        //모니터링 시작
        try
        {
            monitor.StartMonitoring();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"런타임 오류: {ex.Message}");
        }
    }
}




실행해보면 정상적으로 콘솔내용이 출력된다.  예시는 PC6를 예로 들었지만
다른 GPIO핀이라던가 복수개의 chip1에 있는 핀이라면
눌림 이벤트를 제어할수 있다.





ㅇㅇ