2026년 10월 22일 목요일

콘솔기반 asp.net 케스트렐 어플리케이션 생성하고 사설아이피 인증서 만들기

서론:
C#으로 작업을 하다보면 웹서비스를 만들어야 하는 상황이 온다.
이 상황에서 보통은 2의 루트로 나뉘에서 진행하게된다.
HttpListener클래스를 이용해서 여기에 메서드를 등록하고 서비스 하는방법
정식적으로 ASP.Net을 이용해서 케스트렐용 어플리케이션을 만들고 프록시로 
웹서버를 전면에두거나 IIS에서 배포하는 방법등이 거론된다.
문제는 첫번째 방법은 어떤 프로젝트에나 들어갈 수 있지만 콜백 등록이나 DI처리
나 Authorization처리 같은것의 처리가 난해하고 HttpRequest에 대한 디테일한
파이프라인 설정을 하지 못한다. 두번째 방법은 전부 다 갖춰져 있지만 인스턴스한 
느낌이들지 않아서 특정 프로젝트에 소속되기 힘들다.
예를들어 다른사람과 주고 받을 간단한 restapi서버를 만들어야 되고 url이 20개 
미만인 상황이고 이 프로그램이 윈도우의 서비스나 리눅스에 데몬으로 들어가야 
한다거나 아니면 여타 다른 윈폼이나 wpf의 프로그램에서 Sub프로세스로 
띄워져야 하는 경우가 있을 수 있다. 이런상황에서 https로 암호화 통신까지 해야하고  
게다가 도메인은 없고 192.168.1.10 과 같은 사설IP로 https 통신을 해야하는 
상황이 있을 수 있다. 이경우는 앞서 언급한 2가지 케이스 전부 애매한 상황에 놓여진다. 
게다가 이런 케이스가 동시에 한컴퓨터에서 다른 도메인에서 벌어져야 한다면?
이럴때는 하나의 솔류션에서 여러개의 콘솔기반 케스트렐로 작성한후 레고블럭 
쌓듯이 처리하는것이 이롭다.  그래서 이번에는 Mini RestAPI서버를 케스트렐로
만들고 이후 각각의 어플리케이션에 사설인증서를 달아야한다.
그 방법을 알아보자.



[ 콘솔기반 mini restapi 케스트렐 어플리케이션 ]

먼저 솔루션을 하나 만들고 빈 웹어플리케이션을 하나만든다.
간단한 REST API를 만들것이므로 GET방식과 POST방식만 지원하는
인터페이스를 신설(IAPIAction)하고 각요청에 대응하는 인터페이스를 구현한 
클래스를 만든다. 이렇게 만든 클래스들을 스케닝할수 있는 클래스를 만들고
builder.Service에 리플렉션을 이용하여 모두 등록처리한다.
나중에 헤더등에서 전달되어 오는 Authrization 같은것을 처리하기 위해
HttpContext는 주입을 받는 방향으로 설계한다.
기본적으로 WebApplication은  내부적으로 DI를 받을수 있는 Services를
내장하고 있다. 따라서 생성자에서 로거나 어플리케이션DB를 받을수 있다.
이건 나중에 알아서 하면되고 초기 스케폴딩을 먼저 보자.


/// <summary>
/// 액션 인터페이스
/// </summary>
public interface IAPIAction
{
    string HttpMethod { get; }
    string TargetUrl { get; }
    IResult DoAction(HttpContext context);
}

/// <summary>
/// 헬스체크용 API URL 구현체..  이런것을 여러개 만들면 된다.
/// </summary>
public class HealthAction: IAPIAction
{
    public string HttpMethod => "GET";
    public string TargetUrl => "/api/heath";
    public IResult DoAction(HttpContext context)
    {
        return Results.Ok(new
        { 
            Status = "OK",
            Time = DateTimeOffset.UtcNow
        });
    }
}

/// <summary>
/// API Action 검색
/// </summary>
public static class APIActionScanner
{
    public static List<Type> GetActionTypes()
    {
        return Assembly.GetExecutingAssembly()
            .GetTypes()
            .Where(type =>
                type.IsClass &&
                !type.IsAbstract &&
                typeof(IAPIAction).IsAssignableFrom(type))
            .ToList();
    }
}

/// <summary>
/// 프로그램
/// </summary>
public class Program
{
    public static async Task Main(string[] args)
    {
        //초반 셋업파일
        bool bLoadedConfig = LoadConfigFile();
        bool bLoadedDatabase = DataBaseInit();
        
        //웹어플리케이션 빌더
        WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
        
        //케스트렐 연결옵션
        builder.WebHost.ConfigureKestrel(serverOptions =>
        {
            //http 옵션
            serverOptions.ListenAnyIP(MyServerPort);
        });
        
        //API Action들 리스트
        List<Type> actionTypes = APIActionScanner.GetActionTypes();
        foreach (Type actionType in actionTypes) { builder.Services.AddSingleton(actionType); }
        
        //웹어플리케이션 빌드
        WebApplication app = builder.Build();
        
        //API Action 인스턴스 생성 및 URL 맵핑
        foreach (Type actionType in actionTypes)
        {
            IAPIAction action = (IAPIAction)app.Services.GetRequiredService(actionType);
            switch (action.HttpMethod.ToUpperInvariant())
            {
                case "GET":
                    app.MapGet(action.TargetUrl, (HttpContext context) => action.DoAction(context));
                    break;
                case "POST":
                    app.MapPost(action.TargetUrl, (HttpContext context) => action.DoAction(context));
                    break;
                default:
                    throw new InvalidOperationException($"지원하지 않는 HTTP 메서드: {action.HttpMethod}");
            }
        }
        
        //실행
        await app.RunAsync();
    }
    
    
    /// <summary>
    /// 설정파일로드
    /// </summary>
    public static bool LoadConfigFile() => true;
    
    
    /// <summary>
    /// 데이터베이스 로드
    /// </summary>
    public static bool DataBaseInit() => true;

}




[ 케스트렐 프로그램에 인증서 달기 ]

- 먼저 openssl이 설치되어야 한다 openssql을 설치하고 bin폴더를 path로
잡은후 명령창에서 openssql -version 이라고 타입하여 내용이 나오는지 확인

- 사전 개념 사항은 CSR파일과 CRT 파일이 있는데
CSR파일은 무서명된 인증서 신청파일 이고 이것은 최종 인증서가 아님
CRT파일은 서명이 끝난 최종 인증서 파일임  CA(루트인증서와 개인키)를 CSR을 보고
검토한뒤 CA의 도장을 찍어서 발행한 완성본 파일임

- 인증서를 관리할 폴더를 하나만들고 그 경로에서 다음 명령을 타이핑한다.


1. CA 개인키 생성  (나자신이 인증기관이므로 인증기관용 CA 개인키 생성)
openssl genrsa -out rootCA.key 4096

2. CA 루트 인증서 생성 (유효기간 10년)  1번에서 만든 키로 CA 인증서를 발급한다.
rootCA.crt 파일을 만들어야함
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.crt -subj "/CN=MyGoodUnitRootCA"

3. 서버용 인증서 설정파일 미리 준비
폴더에 server.ext 파일을 하나만들고 내용을 다음과 같이 기록한 후 저장한다.
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
# 서비스할 실제 사설 IP 주소로 변경
IP.1 = 192.168.1.100
IP.2 = 127.0.0.1


4. 서버용 개인키 생성  
openssl genrsa -out server.key 2048


5. 서버용 CSR파일 생성 (서버키로 CA에 서명요청을할 파일) (아이피변경)
openssl req -new -key server.key -out server.csr -subj "/CN=192.168.1.100"


6. 서버용 CRT파일 생성 (CA기관이 요청서인 server.csr보고 개인키로 서명한 서버인증서)
openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 3650 -sha256 -extfile server.ext


7. 이렇게 만들어진 server.key파일과 server.crt 파일을 하나의 server.pfx 파일로 비밀번호와 함께 합쳐서 하나로 만들기 (myverygoodpassword 는 자신만의 비밀번호로 변경)
openssl pkcs12 -export -out server.pfx -inkey server.key -in server.crt -passout pass:myverygoodpassword



8. 이제 최종 server.pfx 파일이 만들어졌으니 케스트렐 서버에서 다음 부분만 변경하면됨
(변경전)
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    //http 옵션
    serverOptions.ListenAnyIP(MyServerPort);
});

(변경후)
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    //https 옵션
    serverOptions.ListenAnyIP(MyServerPort, listenOptions =>
    {
        listenOptions.UseHttps("server.pfx", "myverygoodpassword");
    });
    //http 옵션 +1 로 처리 (https만 필요한경우 주석으로 막기)
    serverOptions.ListenAnyIP((MyServerPort+1));
});



추가 참고 사항:  인증기관 CA의 인증서파일인 rootCA.crt 파일은 각 운영체제가 공증해야함
따라서 이 파일을 클라이언트에 넘겨주고 해당 컴퓨터에서 더블클릭(윈도우)하면
이 기관을 신뢰할것인지 등록할 수 있음 (물론 이것 안해도 인증기관 무시하고 암호화 통신할수 있음)






2026년 9월 12일 토요일

샤나인코더 짝퉁.. 가나인코더.. 만들기.

집컴을 리눅스 민트로 바꾸고 계속 사용중이다..

오늘은 중간걸림돌이 였던 샤나 인코더를 대체할 프로그램을 찾다가.
Shutter Encoder라는 걸 찾았는데.. 뭐랄까.. 좀.. UI가 좋긴한데
내 마음에 들지 않았다..  (중간에 멈추는 문제도 있고..)

인코딩을 하는것은..  가끔하기는 하지만 중요한 문제이기도 해서..
차라리 파이썬으로 ffmpeg를 subprocess에서 출력만 로그창에 보이는걸로
처리하는게 훨씬 낫겠다.. 싶었다.. (역시 ffmpeg는 위대하다..)

어짜피 대부분 사용자는 인코딩 프리셋을 한번 가져가면 많이 안바뀌니까..

그래서 그걸 사용하려 한다.  이름은 가나 인코더..  -_-;;

실행폴더에..  presets.json 파일안에 다음처럼 ffmpeg에 들어가는 옵션을 집어넣었고

{
  "H.264 - CRF 23 (Standard)": [
    "-c:v", "h264_nvenc",
    "-preset", "p4",
    "-crf", "23",
    "-c:a", "aac",
    "-b:a", "128k"
  ],
  "H.264 - CRF 20 (High Quality)": [
    "-c:v", "h264_nvenc",
    "-preset", "p4",
    "-crf", "20",
    "-c:a", "aac",
    "-b:a", "192k"
  ],
  "H.264 - Fast (Draft)": [
    "-c:v", "h264_nvenc",
    "-preset", "p1",
    "-crf", "26",
    "-c:a", "aac",
    "-b:a", "96k"
  ]
}


내용은 스텐다드와 고퀄 하옵.. 이렇게 3개 세킹 갖춰넣고..  음질은 aac 로 128k를
기본 셋업으로 했고 엔비디아 gpu 가속을 받는 옵션으로 했다.

파이썬 코드는 다음과 같다.


import json
import os
import subprocess
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk


class ShanaLikeEncoder:

  def __init__(self, root):
    self.root = root
    self.root.title("Gana MP4 Encoder")
    self.root.geometry("900x560")

    self.presets = self.load_presets()
    
    # 프로세스 및 상태 제어를 위한 변수
    self.current_process = None
    self.is_running = False

    self.create_widgets()

  def load_presets(self):
    try:
      with open("presets.json", "r", encoding="utf-8") as f:
        return json.load(f)
    except FileNotFoundError:
      return {
          "Standard MP4": [
              "-c:v",
              "libx264",
              "-crf",
              "23",
              "-c:a",
              "aac",
          ]
      }

  def create_widgets(self):
    # 상단: 옵션 및 제어 프레임
    top_frame = tk.Frame(self.root, padx=10, pady=10)
    top_frame.pack(fill=tk.X)

    tk.Label(top_frame, text="프리셋:").pack(side=tk.LEFT, padx=(0, 5))
    self.preset_combobox = ttk.Combobox(
        top_frame,
        values=list(self.presets.keys()),
        state="readonly",
        width=20,
    )
    if self.presets:
      self.preset_combobox.current(0)
    self.preset_combobox.pack(side=tk.LEFT, padx=(0, 15))

    tk.Label(top_frame, text="해상도 제한:").pack(side=tk.LEFT, padx=(0, 5))
    self.res_options = {
        "원본 유지 (Keep Original)": None,
        "최대 1080p (FHD)": "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease,pad=ceil(iw/2)*2:ceil(ih/2)*2",
        "최대 720p (HD)": "scale='min(1280,iw)':'min(720,ih)':force_original_aspect_ratio=decrease,pad=ceil(iw/2)*2:ceil(ih/2)*2",
    }
    self.res_combobox = ttk.Combobox(
        top_frame,
        values=list(self.res_options.keys()),
        state="readonly",
        width=18,
    )
    self.res_combobox.current(0)
    self.res_combobox.pack(side=tk.LEFT, padx=(0, 15))

    # 시작 / 중지 버튼
    self.start_btn = tk.Button(
        top_frame,
        text="인코딩 시작",
        command=self.start_encoding_thread,
        bg="#4CAF50",
        fg="white",
        width=10,
    )
    self.start_btn.pack(side=tk.LEFT, padx=(0, 5))

    self.stop_btn = tk.Button(
        top_frame,
        text="중지",
        command=self.stop_encoding,
        bg="#f44336",
        fg="white",
        width=8,
        state=tk.DISABLED,
    )
    self.stop_btn.pack(side=tk.LEFT)

    # 중앙: 좌우 분할 컨테이너 (약 35:65 비율)
    main_pane = tk.PanedWindow(self.root, orient=tk.HORIZONTAL, sashrelief=tk.RAISED)
    main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))

    # 왼쪽: 대상 파일 목록 프레임 (35%)
    left_frame = tk.Frame(main_pane)
    main_pane.add(left_frame, width=315)

    list_ctrl_frame = tk.Frame(left_frame)
    list_ctrl_frame.pack(fill=tk.X, pady=(0, 5))

    self.add_btn = tk.Button(list_ctrl_frame, text="파일 추가", command=self.add_files)
    self.add_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 2))
    
    self.del_btn = tk.Button(list_ctrl_frame, text="선택 삭제", command=self.remove_selected)
    self.del_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(2, 0))

    # 파일 리스트박스와 스크롤바
    list_container = tk.Frame(left_frame)
    list_container.pack(fill=tk.BOTH, expand=True)

    self.file_listbox = tk.Listbox(
        list_container, selectmode=tk.EXTENDED, bg="#2b2b2b", fg="#ffffff"
    )
    list_scroll = tk.Scrollbar(
        list_container, orient=tk.VERTICAL, command=self.file_listbox.yview
    )
    self.file_listbox.configure(yscrollcommand=list_scroll.set)

    self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
    list_scroll.pack(side=tk.RIGHT, fill=tk.Y)

    # 오른쪽: 로그 출력 텍스트박스 (65%)
    right_frame = tk.Frame(main_pane)
    main_pane.add(right_frame, width=585)

    self.log_area = scrolledtext.ScrolledText(
        right_frame, wrap=tk.WORD, bg="#1e1e1e", fg="#00ff00", insertbackground="white"
    )
    self.log_area.pack(fill=tk.BOTH, expand=True)

  def add_files(self):
    file_paths = filedialog.askopenfilenames(
        filetypes=[("Video Files", "*.mp4 *.mkv *.avi *.mov *.webm"), ("All Files", "*.*")]
    )
    for path in file_paths:
      if path not in self.file_listbox.get(0, tk.END):
        self.file_listbox.insert(tk.END, path)

  def remove_selected(self):
    if self.is_running:
      return
    selected_indices = self.file_listbox.curselection()
    for index in reversed(selected_indices):
      self.file_listbox.delete(index)

  def start_encoding_thread(self):
    files = self.file_listbox.get(0, tk.END)
    if not files:
      messagebox.showerror("오류", "변환할 파일을 하나 이상 추가해주세요.")
      return

    # 상태 변경 (실행 중)
    self.is_running = True
    self.start_btn.config(state=tk.DISABLED)
    self.stop_btn.config(state=tk.NORMAL)
    self.add_btn.config(state=tk.DISABLED)
    self.del_btn.config(state=tk.DISABLED)
    self.preset_combobox.config(state=tk.DISABLED)
    self.res_combobox.config(state=tk.DISABLED)

    threading.Thread(
        target=self.run_batch_encoding, args=(files,), daemon=True
    ).start()

  def run_batch_encoding(self, files):
    selected_preset = self.preset_combobox.get()
    preset_args = self.presets.get(selected_preset, [])

    selected_res_name = self.res_combobox.get()
    filter_arg = self.res_options.get(selected_res_name)

    total_files = len(files)
    for idx, input_file in enumerate(files, 1):
      if not self.is_running:
        break

      if not os.path.exists(input_file):
        continue

      base, _ = os.path.splitext(input_file)
      output_file = f"{base}_encoded.mp4"

      cmd = ["ffmpeg", "-y", "-i", input_file] + preset_args
      if filter_arg:
        cmd.extend(["-vf", filter_arg])
      cmd.append(output_file)

      self.log_area.insert(
          tk.END,
          f"[{idx}/{total_files}] 변환 중: {os.path.basename(input_file)}\n"
          f"실행 명령어: {' '.join(cmd)}\n" + "-" * 50 + "\n",
      )
      self.log_area.see(tk.END)

      self.current_process = subprocess.Popen(
          cmd,
          stdout=subprocess.PIPE,
          stderr=subprocess.STDOUT,
          universal_newlines=True,
          encoding="utf-8",
          errors="replace",
      )

      # 프로세스 출력을 실시간으로 읽음 (중지 시그널 대응)
      while True:
        line = self.current_process.stdout.readline()
        if not line and self.current_process.poll() is not None:
          break
        if line:
          self.log_area.insert(tk.END, line)
          self.log_area.see(tk.END)

      returncode = self.current_process.poll()
      self.current_process = None

      if not self.is_running:
        self.log_area.insert(tk.END, "\n[중지됨] 사용자에 의해 작업이 중단되었습니다.\n\n")
        break

      if returncode != 0:
        self.log_area.insert(
            tk.END, f"\n[오류] 파일 변환 실패 (코드: {returncode})\n\n"
        )
      else:
        self.log_area.insert(tk.END, f"\n[완료] 성공: {output_file}\n\n")

    if self.is_running:
      self.log_area.insert(tk.END, "[전체 작업 완료] 모든 파일의 인코딩이 끝났습니다.\n")
    
    self.log_area.see(tk.END)
    self.reset_ui_state()

  def stop_encoding(self):
    if self.is_running:
      self.is_running = False
      if self.current_process:
        try:
          self.current_process.terminate()
          self.current_process.wait(timeout=2)
        except subprocess.TimeoutExpired:
          self.current_process.kill()
        except Exception:
          pass
      self.log_area.insert(tk.END, "\n[알림] 중지 버튼이 눌렸습니다. 현재 파일을 정리하는 중...\n")
      self.log_area.see(tk.END)

  def reset_ui_state(self):
    self.is_running = False
    self.current_process = None
    self.start_btn.config(state=tk.NORMAL)
    self.stop_btn.config(state=tk.DISABLED)
    self.add_btn.config(state=tk.NORMAL)
    self.del_btn.config(state=tk.NORMAL)
    self.preset_combobox.config(state="readonly")
    self.res_combobox.config(state="readonly")


if __name__ == "__main__":
  root = tk.Tk()
  app = ShanaLikeEncoder(root)
  root.mainloop()








요로코롬  하고 실행하면 된다.. 
씨야~

2026년 8월 3일 월요일

리눅스 wine에서 한글깨짐 잡기

와인 설치이후 맑은고딕 ttf로 변경을 해줘야함.
아무래도 이 폰트가 한국을 대표하는 폰트이니까..


[아래 경로에 맑은고딕 폰트를 다운로드 하여 복사한다.]
 ~/.wine/drive_c/windows/Fonts/*.ttf


이후 터미널에서 레지스트리를 편집하여 맑은 고딕을 기본으로 설정함
[아래의 내용을 붙혀넣기]


---------------------------------------------------------------

cat << 'EOF' > font_fix_malgun.reg
REGEDIT4

[HKEY_CURRENT_USER\Software\Wine\FontSubstitutes]
"MS Shell Dlg"="Malgun Gothic"
"MS Shell Dlg 2"="Malgun Gothic"
"System"="Malgun Gothic"
"Tahoma"="Malgun Gothic"
EOF

regedit font_fix_malgun.reg
wineserver -k

---------------------------------------------------------------


이후 winecfg 를 실행시켜서 그래픽 탭으로 들어가보면 잘나오는걸 확인한다.




맑은고딕 다운로드





2026년 7월 23일 목요일

PostgreSQL 모든테이블의 SerialType의 인덱스를 최신으로 맞추기

DB 마이그레이션 하거나 그럴때 ID값이 안따라갈수 있음..
아래의 내용으로 인덱스를 최신으로 바꿔줘야함..
안하면 duplicate key value violates unique constraint  에러를 만나게됨..





DO $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN
        SELECT
            n.nspname AS schema_name,
            c.relname AS table_name,
            a.attname AS column_name,
            pg_get_serial_sequence(
                quote_ident(n.nspname) || '.' || quote_ident(c.relname),
                a.attname
            ) AS seq_name
        FROM pg_class c
        JOIN pg_namespace n ON n.oid = c.relnamespace
        JOIN pg_attribute a ON a.attrelid = c.oid
        WHERE c.relkind = 'r'
          AND pg_get_serial_sequence(
                quote_ident(n.nspname) || '.' || quote_ident(c.relname),
                a.attname
              ) IS NOT NULL
    LOOP
        EXECUTE format(
            'SELECT setval(%L, COALESCE((SELECT MAX(%I) FROM %I.%I), 1), true)',
            r.seq_name,
            r.column_name,
            r.schema_name,
            r.table_name
        );
    END LOOP;
END $$;






ㅇㅇ

민트나 조린OS로 프로그램 바로가기 (desktop 파일) 만들기

예를들어 RDRemote라는 프로그램 실행파일이 있을경우 
다음과 같은 ~/MyLauncher/RDRemote.desktop 파일을 만든다.
내용은 다음처럼

[Desktop Entry]
Type=Application
Name=RD Remote
Comment=Remote Support Launcher
Exec=/home/username/MyApps/RDRemote/RDRemoteLauncher
Path=/home/username/MyApps/RDRemote
Icon=/home/dyflrytlf/MyApps/RDRemote/mainlogo.png
Terminal=false
Categories=Utility;



이거는 실제 실행화일은
/home/username/MyApps/RDRemote/RDRemoteLauncher
이고  실행경로는
/home/username/MyApps/RDRemote

이다..  저장하고  다음명령으로 폴더와 바로가기를 복사한다.


mkdir -p ~/.local/share/applications
cp ~/MyLauncher/RDRemote.desktop ~/.local/share/applications/
chmod +x ~/.local/share/applications/RDRemote.desktop



이렇게 하면 앱런처 화면에 아이콘이 복사되어 있을것이다.

음..  lnk 파일은 편리한거였네..



2026년 7월 10일 금요일

남자 셀프 헤어컷 정리

남자는 이제 셀프 헤어컷을 해야한다.
동네 미용실 예약제와 폭리가격에 개짜증이 나니까 이를 정리하겠다.








ㅇㅇ

2026년 7월 4일 토요일

ini-parser 만들기.. C#

누겟에 있는 ini-parser가 리눅스환경에서 정상동작하지 않는다.

윈도우에 있는 ini파일이 개행이 \r\n이거니와.

리눅스쪽으로 복사가 되면서 더미 데이터가 들어가는것 같다..

그래서 ini-parser를 쓰면 정상적으로 쪼개기가 안된다.

짜증나니까.. 나만의 ini-parser (읽기전용)을 만들었드랬다..





Public class MyIniParser
{
/// <summary>
/// 내부 자료
/// </summary>
private Dictionary<string, List<KeyValuePair<string, string>>> _iniData = new Dictionary<string, List<KeyValuePair<string, string>>>();


/// <summary>
/// 데이터 가져오기
/// </summary>
public T GetIniData<T>(string section, string key)
{
T ret_data = default;
if (!_iniData.ContainsKey(section)) { return ret_data; }
if (!_iniData[section].Exists(x => x.Key == key)) { return ret_data; }
KeyValuePair<string, string> ini_data = _iniData[section].Find(x => x.Key == key);
//
try
{
return (T)Convert.ChangeType(ini_data.Value, typeof(T));
}
catch
{
return ret_data;
}
}


/// <summary>
/// 파일경로로 파싱처리하기
/// </summary>
public bool ParseIniFile(string iniFile, Encoding encoding)
{
try
{
if (!File.Exists(iniFile)) { return false; }
string rawContent = File.ReadAllText(iniFile, encoding);

// ini를 읽기 좋게 변환처리
string result = rawContent.Replace("\r", "");
string[] lines = result.Split('\n');
var validLines = new List<string>();
foreach (string line in lines)
{
string trimmed = line.Trim();

// 빈라인 무시
if (string.IsNullOrWhiteSpace(trimmed)) continue;
// 주석으로 시작하는 라인제거 (';' 또는 '#')
if (trimmed.StartsWith(";") || trimmed.StartsWith("#")) continue;
// 깨진문자열 제외
if (trimmed.Contains("M-lM")) continue;
//
bool isSection = trimmed.StartsWith("[") && trimmed.EndsWith("]");
bool isKeyValue = trimmed.Contains("=");
if (isSection || isKeyValue) { validLines.Add(trimmed); }
}


//필터링된 라인으로 KeyValue 삽입
string lastSection = "";
foreach (string one_line in validLines)
{
bool isSection = one_line.StartsWith("[") && one_line.EndsWith("]");
bool isKeyValue = one_line.Contains("=");
//
if (isSection)
{
string sectionName = one_line.Substring(1, one_line.Length - 2);
lastSection = sectionName;
if (!_iniData.ContainsKey(sectionName))
{
_iniData.Add(sectionName, new List<KeyValuePair<string, string>>());
}
}
else if (isKeyValue)
{
string[] keyValue = one_line.Split('=');
if (keyValue.Length != 2) { continue; }
string key = keyValue[0].Trim();
string value = keyValue[1].Trim();
if (_iniData.ContainsKey(lastSection))
{
//동일한 키가 있으면 제거하고 새로 추가
var list = _iniData[lastSection];
int index = list.FindIndex(x => x.Key == key);
if (index != -1) { list.RemoveAt(index); }
//
_iniData[lastSection].Add(new KeyValuePair<string, string>(key, value));
}
}
}

//
return true;
} catch (Exception ex) {
throw ex;
}
}
}