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코딩 + 파워쉘...   장난없다..


ㅇㅇ


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

로그 폴더내에 있는 *.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에 있는 핀이라면
눌림 이벤트를 제어할수 있다.





ㅇㅇ





2025년 9월 20일 토요일

다시보는 C# 콘솔프로그램

콘솔프로그램이 C#언어를 배우는 초기 단계에서는 Console.WriteLine() 등으로
어떤 출력내용을 확인하고 나서 그 다음 단원으로 가고 이런걸로 많이 사용한다.

개발자로써 어느정도 경력이 쌓이다 보면 관심사의 분리 같은걸 알게 되면서
어느순간 콘솔프로그램은 테스트유닛으로 활용되거나 백그라운드 테스크용도로
사용된다.    

asp.net도 엄밀히 따지면 콘솔프로그램이라고 할수 있을정도로 무시할수 없는
레벨로 콘솔프로그램의 위상이 올라선다.

그리고 이를 활용하는 눈에 보이지 않는 어플을 만들기 시작하는데
비단 웹뿐만이 아니라 백그라운드 서비스.. (리눅스에서는 데몬)
으로 활용된다.  게다가 아두이노 같은 싱글보드에서 돌아가는 한개의 프로그램을
만드는데에도 적극적으로 활용된다. 물론 아두이노에서 C#은 안돌아가지만..
그런 비슷한 느낌으로 만드는 모듈이 바로 라즈베리파이가 아니라 중국에서 만드는 값싼
오렌지파이 제로나  오렌지파이 3B같은 부류이다.

물론 CPU가격이 메모리 대란같은것으로 들쭉날쭉하게 되면 어쩔수 없고 또한
비싼 비메모리 예를 들어 인텔이나 브로드컴 같은 CPU들은 아두이노랑 비빌수
없다. 하지만 오렌지파이 CPU같은것들은 RockChip같은 브렌드의 부류로
구성되고 알리익스프레스같은 사이트를 이용해서 중국에서 구매하는경우
실제로 한국에서 아두이노를 돈주고 구매하는것과 맞먹는 가격까지
내려간다.  이를 이용해서 C#으로 콘솔어플을 돌리면 GPIO나 I2C같은걸 이용해서
마치 아두이노처럼 프로그램을 할수 있다. C#으로.. 그리고 그렇게 돌아가기 위해
오렌지 파이에는 암비안이나 우분투서버 같은것을 OS로 포팅하고 dotnet 런타임을
설치하고 나서 시스템이 시작할때 데몬으로 등록처리하면 실제로 동작한다.

세상 참 많이 발전했다.. 그리고 이러한 콘솔프로그램을 돌릴수 있는
그 중심에는 이 앱이 하나의 정식적인 앱으로 활용할수 있게되는 

MS의 핵심 확장모듈이 있다. 그 모듈은

dotnet add package Microsoft.Extensions.Hosting;
dotnet add package Microsoft.Extensions.Logging;
dotnet add package Microsoft.Extensions.DependencyInjection;


이라 할수 있다..

이번 포스팅에서는 이 Hosting을 이용하여 지난번 로거와 HelloWorld를
출력하는 서비스 그리고 1초마다 한번씩 현재시간을 로거에 출력하는 서비스와
랜덤한 숫자를 출력하는 프로그램을 만든다.. 물론 이를 활용하여 서비스앱을
만드는데 기초재료로 사용될것을 기대한다.


[주요순서]

1. 콘솔프로그램을 만들고 위의 3개 package를 설치한다.
2. 메인 진입점 작성하고 4개(로거, 단순출력, 랜덤숫자출력, 시간출력)의 서비스를 포함한다.
3. 각서비스를 구현한다.
4. 빌드후 실행


[진입점]

/// <summary>
/// 메인 프로그램
/// </summary>
public class MainApp
{
    /// <summary>
    /// 진입점
    /// </summary>
    public async static Task Main()
    {
        //초기로드
        Console.WriteLine("Application StartUp!!");
        LoadSetup();

        //서비스 로딩
        IHost host = Host.CreateDefaultBuilder()
            .ConfigureServices(services =>
            {
                //로거 바인딩
                services.AddSingleton<ILoggerProvider>(sp =>
                {
                    return new FileLoggerProvider();
                });

                //서비스 호스트 바인딩
                services.AddHostedService<NormalPrint>();
                services.AddHostedService<TimePrinter>();
                services.AddHostedService<RandomPrint>();
            })
            .Build();

        // 호스트 실행
        await host.RunAsync();
    }

    /// <summary>
    /// 앱로딩 설정
    /// </summary>
    public static void LoadSetup()
    {
        EUCKR.Init();
    }

}


[로거 부분 생략 (지난번 포스팅 참조)]
...


[NormalPrint.cs]

/// <summary>
/// 일반 메시지 출력용
/// </summary>
public class NormalPrint : IHostedService
{
    /// <summary>
    /// 생성자와 DI
    /// </summary>
    private readonly ILogger<NormalPrint> _logger;
    public NormalPrint(ILogger<NormalPrint> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// 서비스 시작
    /// </summary>
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("NormalPrint 서비스 시작!!");

        // 한번만 로그 출력하고 끝냄
        _logger.LogWarning("아이엠 Just 프린트 노멀 유노암쎙??");
        
        //
        return Task.CompletedTask;
    }

    /// <summary>
    /// 서비스 종료
    /// </summary>
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("NormalPrint 서비스 종료!!");
        return Task.CompletedTask;
    }

}


[RandomPrint.cs]

/// <summary>
/// 랜덤한 숫자 출력
/// </summary>
public class RandomPrint: IHostedService
{
    /// <summary>
    /// 생성자와 내부변수
    /// </summary>
    private readonly ILogger<RandomPrint> _logger;
    private Task _time_count_Task;
    private readonly CancellationTokenSource _stoppingToken = new CancellationTokenSource();
    private Random rnd;
    public RandomPrint(ILogger<RandomPrint> logger)
    {
        _logger = logger;
        rnd = new Random();
    }

    /// <summary>
    /// 서비스 시작
    /// </summary>
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("RandomPrint 서비스 시작!!");

        // 백그라운드 서비스 시작
        _time_count_Task = Task.Run(async () =>
        {
            await DoWorkAsync(_stoppingToken.Token);
        });
        //
        return Task.CompletedTask;
    }

    /// <summary>
    /// 서비스 종료
    /// </summary>
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("RandomPrint 서비스 중지!!");
        //
        try
        {
            //작업중지 신호
            _stoppingToken.Cancel();

            //백그라운드 Task가 완료되기를 기다림
            return Task.WhenAny(_time_count_Task, Task.Delay(Timeout.Infinite, cancellationToken));

        }
        catch (Exception ex)
        {
            _logger.LogError(ex.Message);
            return Task.CompletedTask;
        }
    }

    /// <summary>
    /// 작업로직
    /// </summary>
    private async Task DoWorkAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                //
                int rnd_count = rnd.Next(100);                
                _logger.LogWarning($"0 ~ 99 사이의 랜덤숫자 -> { rnd_count.ToString() }");

                //0.5초쉼..
                await Task.Delay(500);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}


[TimePrinter.cs]


/// <summary>
/// 시간출력용 클래스
/// </summary>
public class TimePrinter : IHostedService
{
    /// <summary>
    /// 생성자와 내부변수
    /// </summary>
    private readonly ILogger<TimePrinter> _logger;
    private Task _time_count_Task;
    private readonly CancellationTokenSource _stoppingToken = new CancellationTokenSource();
    public TimePrinter(ILogger<TimePrinter> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// 서비스 시작
    /// </summary>
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("TimerPrinter 서비스 시작!!");

        // 백그라운드 서비스 시작
        _time_count_Task = Task.Run(async () =>
        {
            await DoWorkAsync(_stoppingToken.Token);
        });
        //
        return Task.CompletedTask;
    }

    /// <summary>
    /// 서비스 종료
    /// </summary>
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("TimerPrinter 서비스 중지!!");
        //
        try
        {
            //작업중지 신호
            _stoppingToken.Cancel();

            //백그라운드 Task가 완료되기를 기다림
            return Task.WhenAny(_time_count_Task, Task.Delay(Timeout.Infinite, cancellationToken));

        }
        catch (Exception ex)
        {
            _logger.LogError(ex.Message);
            return Task.CompletedTask;
        }
    }

    /// <summary>
    /// 작업로직
    /// </summary>
    private async Task DoWorkAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                //
                DateTime nowtime = DateTime.Now;
                _logger.LogWarning("TimePrinter의 현재시간 기록: " + nowtime.ToString("yyyy-MM-dd HH:mm:ss"));

                //1초쉼..
                await Task.Delay(1000);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }            
        }
    }
    
}


이렇게 완성하고 나서  dotnet run으로 돌려보면 로그를 잘찍는다..
배포를 하려면 

[윈도우 배포 - 64비트]
dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true

[리눅스 배포 - 64비트]
dotnet publish -c Release -r linux-x64 --self-contained true /p:PublishSingleFile=true


요로코롬 하면된다..  물론 윈도우나  리눅스에서 시작프로그램에 등록해야 하지만..

ㅇㅇ


2025년 9월 17일 수요일

C#에서 event나 delegate를 구독할때 처리하는 방법

C#에서 event나 delegate를 구독할때 처리하는 방법은
총 3가지 이다.


mybutton이 있었을때 이버튼의 click이벤트를 바인딩하려 한다.


1. 람다식 (인라인으로 처리)

mybutton.click += (_, _) => OtherMethod()

이는 람다식으로 표현하는 방법인데 원래 파라미터 object, eventargs의
내용을 무시하고 내부처리를 이어하기 위함 (컴파일러가 추론할수 있음)


2. 델리게이트처리 (인라인)

mybutton.click += delegate (object sender, EventArgs e) { ... }

델리게이트를 선언하면서 호출형태를 명시하고 { ... } 안에 내용을 기술
파라미터의 형태를 컴파일러가 추론할수 없음으로 데이터형을 기술해야함


3. 별도 함수 지정 (인라인 아님)

public void MyClick(object sender, EventArgs e) { ... }
---
mybutton.click += MyClick;
---

별도의 함수에서 처리하는 방법임


※ 파라미터를 처리할때 EventArgs 가 아닌 다른 Args를 원하는 경우 해당 클래스로
바인딩 해줘야함 대리자의 시그니처가 정확한지 판단하기 때문임. ※


2025년 9월 15일 월요일

VS Code에서 jupyter 확장프로그램 사용하기

먼저 확장프로그램으로 python과 jupyter를 설치한다.

파일명은 굳이 ipynb 같은걸로 안만들고 py확장자 쓰면 된다..


# %% 

이때 위 구분으로 각 셀을 코드로 구분할수 있음

# %% [markdown]

위 구분으로 h1태그 같은 강조 글자를 사용함 코드셀이 아닌 문자셀
그 다음중에서 ###  <-- 샾의 갯수에 때라 글자가 커짐 가장작은건 # 하나



[단축키 정리]

Shift + Enter    현재 셀을 실행하고, 다음 셀로 이동

Ctrl + Enter    현재 셀만 실행 (다음 셀로 이동하지 않음)

Alt + Enter    현재 셀을 실행하고, 아래에 새로운 셀을 추가

Ctrl + Alt + Enter    현재 셀만 다시 실행

Shift + Alt + Enter    현재 셀부터 모든 셀을 순차적으로 실행



난 판다스로 데이터 정리할때 주피터 노트북보다 
이게 더 깔끔해보임

2025년 9월 9일 화요일

asp.net에서 Newtonsoft의 JObject를 Json값으로 리턴하고 싶을때

return Ok(JObject);  
하면 안됨..  직렬화 과정에서 정상적인 클래스 인스턴스가 아니므로 
제대로 직렬화 하지 못함 이런경우에는 다른방식으로 전달해야함..


return Content(JObject.ToString(), "application/json");

아니면 메서드 이름위에 [Produces("application/json")]  를 선언하고

return Content(JObject.ToString());



이렇게 하면 정상적으로 리턴함
왜냐면 리턴과정에서 직렬화를 수행하지 않고 application/json을 보내면서 문자그대로
보내기 때문에 Json으로 리턴되는것임..