2025년 8월 16일 토요일

C# 로거 만들기..

관심사의 분리니 뭐니 해서..
옛날에는 그냥 static으로 WriteLog를 처리 했었다..
그런데 요즘 트렌드는 요로코롬 안한다.
유행을 무시해도 되지만 아발로니아를 비롯해 asp.net 에서도 ILogger를 
생성자로 DI해서  사용한다.. 
그래서 그냥 이참에 나만의 파일로거를 만들어야 것다
뭐 언젠가는 해야하것지만..

일단 나의 전략은 LogLevel에서 Trace와 Debug까지는 콘솔에만 출력하고
Infomation이상은 콘솔출력 + 파일출력 (날짜별 폴더)
가 목표이다..

이제 시작~!!

먼저 DI와 Logging 패키지를 받아야한다..
Asp.net은 둘다 설치 되어 있으니까 건너뛰고 안되어 있는 프로젝트만 설치 고고싱

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

이렇게 2개

그다음 2개의 클래스를 만들어야 하는데 하나는 ILogger구현체이고 
나머지는 이.. 구현체클래스를 제공하는 클래스이다..
하나로 합치지.. 에휴 신발..

1.  ILogger 구현체


/// <summary>
/// 파일로거 본체
/// </summary>
public class FileLogger : ILogger
{
    private readonly string _logdir;
    public FileLogger(string target_dir)
    {
        _logdir = target_dir;
    }

    //
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default;
    
    //모든 로그레벨 허용 여부
    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    //로그본체
    private static object log_sync = new object();
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        try
        {
            DateTime nowtime = DateTime.Now;            
            string write_msg = nowtime.ToString("[HH:mm:ss]: (") + $"{logLevel}) {formatter(state, exception)}" + Environment.NewLine;
            Trace.Write(write_msg);
            //
            if (logLevel > LogLevel.Debug)
            {
                lock (log_sync)
                {
                    string date_word = nowtime.ToString("yyyy-MM-dd");
                    string target_dir = Path.Combine(_logdir, "로그", date_word);
                    if (!Directory.Exists(target_dir)) {
                        Directory.CreateDirectory(target_dir);
                    }
                    //
                    string target_file = Path.Combine(target_dir, $"{date_word}.log");
                    using (StreamWriter sw = new StreamWriter(target_file, true, EUCKR.GetEncoding))
                    {
                        sw.Write(write_msg);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Trace.WriteLine(ex.Message);            
        }
    }
}


2. 프로바이더 구현체

/// <summary>
/// 로거 프로바이더
/// </summary>
public class FileLoggerProvider : ILoggerProvider
{
    private readonly string _target_dir;
    public FileLoggerProvider(string target_dir)
    {
        _target_dir = target_dir;        
    }
    //
    public ILogger CreateLogger(string categoryName)
    {
        return new FileLogger(_target_dir);
    }
    //
    public void Dispose() { }
}


요로 코롬 만들어 놓고
App수준의 .cs 파일에서  다음과 같이 처리함

public static IServiceProvider Service { get; private set; } = null;

이후 초기화 함수등에서

var services = new ServiceCollection();
services.AddLogging(builder => 
{
    builder.SetMinimumLevel(LogLevel.Trace);
    builder.AddProvider(new FileLoggerProvider(AppContext.BaseDirectory));
});
Services = services.BuildServiceProvider();

이렇게 등록하고 

사용할때는 생성자에서 (ILogger<T> logger)로 받아서 멤버에 등록해서 쓰거나
아니면 명시적으로 (App수준의 cs파일에서 static 멤버로 등록했으니까)

var mylogger = App.Services.GetService<ILogger<T>>();

이렇게 받아서 사용하면 됨..


음.. 기왕지사 DI 얘기가 나온김에 한술 더 떠보자..
결론적으로 저 Services에 등록된 애들끼리만 생성자에서 DI를 받을 수 있지 않은가??

그래서 원칙적으로는 DI가 필요한 클래스 전부 저걸 일일이 다 등록해야한다.

그러면 짜치자너??

이럴때는 DI로 등록될 클래스를 어트리뷰트로 만들어서 쓰는게 좋다.
예를 들어 MyDevice라는 클래스가 있는데 이 녀셕을 DI로 쓸껀데..
싱글톤으로 하고 싶다면 클래스 선언 앞에
[DIObject(ServiceLifttime.Singleton)]
이런식으로 어트리뷰트를 달아서 쓰고 이 어트리뷰트가 달린 모든 클래스를
확장메서드로 한번에 불러와서 서비스에 담으면 된다..

3. DIObjectAttribute.cs  파일 추가

public class DIObjectAttribute : Attribute
{
    public ServiceLifetime Lifetime { get; }

    public DIObjectAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient)
    {
        Lifetime = lifetime;
    }        
}


4. 그 다음 서비스가 사용할 확장 메서드

public static class MyExtentions
{
    public static IServiceCollection AddConventionBasedServices(this IServiceCollection services)
    {
        var assembly = Assembly.GetExecutingAssembly();
        var typesWithAttribute = assembly.GetTypes()
            .Where(t => t.IsClass && t.GetCustomAttribute<DIObjectAttribute>() != null);

        //
        foreach (var type in typesWithAttribute)
        {
            //클래스에 있는 속성을 읽어옴
            var attribute = type.GetCustomAttribute<DIObjectAttribute>();

            //속성에 지정된 Lifetime에 따라 적설한 등록 메서드를 호출함
            switch (attribute.Lifetime)
            {
                case ServiceLifetime.Singleton:
                    services.AddSingleton(type);
                    break;
                case ServiceLifetime.Scoped:
                    services.AddScoped(type);
                    break;
                case ServiceLifetime.Transient:
                    services.AddTransient(type);
                    break;
                default:
                    break;
            }
        }

        //
        return services;
    }
}


이렇게 확장메서드를 등록햇으니까
아까 앱수준의 Service 초기화 구문에서 다음 구문만 추가하면 된다.


var services = new ServiceCollection();
services.AddLogging(builder => 
{
    builder.SetMinimumLevel(LogLevel.Trace);
    builder.AddProvider(new FileLoggerProvider(AppContext.BaseDirectory));
});

//여기에 DIObject 있는 클래스들 등록

services.AddConventionBasedServices();
Services = services.BuildServiceProvider();

 
요로코롬 사용하면 나중에
[DIObject(ServiceLifetime.Singleton)]
뭐이런 식으로 적혀있는 클래스는 서비스에 자동등록됨..
 
요로코롬 사용하면 지들끼리 생성자에서 북치고 장구치구가 됨..
ㅇㅇ.


2025년 8월 15일 금요일

아발로니아 초기 mvvm 설정하기

초기에 아발로니아 프로젝트를 mvvm모드로 시작할수도 있지만 
vscode에서 기본으로 시작하는 경우 mvvm까지 진입하는 과정을 설명한다.
물론 초기 스케폴딩이 알아서 다 해주지만 초기 진입에 대한 이해가 부족하면
나중에 힘들어진다.

그래서 아발로니아 프로젝트를 
dotnet new avalonia.mvvm -o [프로젝트이름]  가 아닌
dotnet new avalonia.app -o [프로젝트이름]  으로 시작한 경우 스케폴딩을 처리


1. dotnet add package CommunityToolkit.Mvvm 
으로 mvvm 툴킷 설치하기..

2. ViewModels폴더를 생성하고 그아래 ViewModelBase.cs 클래스를 만들어서
ObservableObject를 상속 받기 그리고 MainWindowViewModel.cs만들어서
ViewModelBase를 상속 받기

3.메인 뷰템플릿(MainWindow.axaml)에서  window 최상위 노드에 DataType선언
xmlns:vm="clr-namespace:MyProject.ViewModels"
x:DataType="vm:MainWindowViewModel"
이렇게 정의 하고 해당 Datacontext를 사용할때 
<Window.DataContext>
    <vm:MainWindowViewModel></vm:MainWindowViewModel>
</Window.DataContext>

라고 선언하면 다음 트리에서 뷰모델의 프로퍼티나 메서드 사용가능

4. C# 로거를 프로젝트에 바인딩

5. 2번에서 만든 ViewModelBase의 생성자에서 dbcontext와 logger바인딩
db연결이 없는 프로젝트면 dbcontext는 제외
public class ViewModelBase : ObservableObject
{
    protected readonly ApplicationDBContext _dbcontext;
    protected readonly ILogger _logger;
    //
    public ViewModelBase()
    {
        _logger = App.Service.GetService<ILogger<ViewModelBase>>()!;
        _dbcontext = App.Service.GetService<ApplicationDBContext>()!;
    }
}

6. 프로젝트명.csproj 파일에  아발로니아 리소스 경로추가
  <ItemGroup>
    <AvaloniaResource Include="Assets\**" />
  </ItemGroup>

7.  App.axaml 파일에서 초기 스타일 명시 (테마는 기본쓸꺼면 FluentTheme 를 다른거로 변경)
  <!-- 어플리케이션 스타일 --> 
  <Application.Styles>
    <FluentTheme />
    <StyleInclude Source="Style/AppDefaultStyle.axaml"></StyleInclude>
  </Application.Styles>


8. 초기 폰트를 바인딩 처리 (여기서는 NotoSansKR로 예시)
Assets/Fonts 폴더에  NotoSansKR-Bold.ttf, NotoSansKR-Regular.ttf 복사

App.axaml 파일에서  사용할 폰트추가

<!-- 어플리케이션 리소스들 -->
<Application.Resources>

    <!-- 사용할 폰트 -->
    <FontFamily x:Key="NotoSansKR">/Assets/Fonts/NotoSansKR-Regular.ttf#Noto Sans KR</FontFamily>
    <FontFamily x:Key="NotoSansKRBold">/Assets/Fonts/NotoSansKR-Bold.ttf#Noto Sans KR</FontFamily>
        
</Application.Resources>  

9 .위처럼 설정하고 모든 윈도우는 해당 폰트를 따르도록 설정 다시 AppDefaultStyle.axaml 파일에서 윈도우와 유저컨트롤을 정의

  <Style Selector="Window">    
    <Setter Property="FontFamily" Value="{DynamicResource NotoSansKR}"></Setter>
  </Style>
  <Style Selector="UserControl">    
    <Setter Property="FontFamily" Value="{DynamicResource NotoSansKR}"></Setter>
  </Style>
  


2025년 8월 10일 일요일

C# 한글조합기..

한글조합기를 AI를 통해서 만들었다..

가상키보드를 만든다던가.. 뭐.. 이런..

여튼 잘쓰셈.. ㅋㅋ



public class HangulComposer
{
    // 기본 테이블
    static readonly char[] CHO = new[] { 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' };
    static readonly char[] JUNG = new[] { 'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ' };
    static readonly string[] JONG = new[] { "", "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ", "ㄶ", "ㄷ", "ㄹ", "ㄺ", "ㄻ", "ㄼ", "ㄽ", "ㄾ", "ㄿ", "ㅀ", "ㅁ", "ㅂ", "ㅄ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ" };

    // 겹받침 맵
    static readonly Dictionary<string, string> FinalComposite = new()
    {
        {"ㄱㅅ","ㄳ"}, {"ㄴㅈ","ㄵ"}, {"ㄴㅎ","ㄶ"},
        {"ㄹㄱ","ㄺ"}, {"ㄹㅁ","ㄻ"}, {"ㄹㅂ","ㄼ"},
        {"ㄹㅅ","ㄽ"}, {"ㄹㅌ","ㄾ"}, {"ㄹㅍ","ㄿ"},
        {"ㄹㅎ","ㅀ"}, {"ㅂㅅ","ㅄ"}
    };

    // 겹모음 맵
    static readonly Dictionary<string, char> MedialComposite = new()
    {
        {"ㅗㅏ",'ㅘ'}, {"ㅗㅐ",'ㅙ'}, {"ㅗㅣ",'ㅚ'},
        {"ㅜㅓ",'ㅝ'}, {"ㅜㅔ",'ㅞ'}, {"ㅜㅣ",'ㅟ'},
        {"ㅡㅣ",'ㅢ'}
    };

    // 이중초성(선택)
    static readonly Dictionary<string, char> InitialComposite = new()
    {
        {"ㄱㄱ",'ㄲ'}, {"ㄷㄷ",'ㄸ'}, {"ㅂㅂ",'ㅃ'}, {"ㅅㅅ",'ㅆ'}, {"ㅈㅈ",'ㅉ'}
    };

    // 내부 버퍼
    private List<char> jamoBuffer = new List<char>();
    private StringBuilder committed = new StringBuilder();
    private string preedit = "";

    // 입력
    public void Input(char c)
    {
        if (IsJamo(c))
        {
            jamoBuffer.Add(c);
            RecomposeAndMaybeCommit();
        }
        else
        {
            CommitAll();
            committed.Append(c);
            preedit = "";
        }
    }

    // 백스페이스
    public void Backspace()
    {
        if (jamoBuffer.Count > 0)
        {
            jamoBuffer.RemoveAt(jamoBuffer.Count - 1);
            RecomposeAndMaybeCommit();
            return;
        }

        if (committed.Length > 0)
            committed.Remove(committed.Length - 1, 1);
    }

    // 현재까지 확정된 문자열
    public string Committed => committed.ToString();

    // 현재 미완성(조합 중) 문자열
    public string Preedit => preedit;

    // 확정 문자열 + 조합중 문자열
    public String GuessWord()
    {
        return Committed + Preedit;
    }

    //클리어
    public void Clear()
    {
        jamoBuffer.Clear();
        committed.Clear();
        preedit = "";
    }

    // 강제 커밋
    public void CommitAll()
    {
        var parsed = ParseJamos(jamoBuffer);
        foreach (var p in parsed)
            committed.Append(p.composed);
        jamoBuffer.Clear();
        preedit = "";
    }

    // --- 내부 로직 ---
    private void RecomposeAndMaybeCommit()
    {
        var parsed = ParseJamos(jamoBuffer);
        if (parsed.Count == 0)
        {
            preedit = "";
            return;
        }

        // 마지막 음절 제외하고 모두 확정
        int stableCount = Math.Max(0, parsed.Count - 1);
        int removeJamoCount = 0;
        for (int i = 0; i < stableCount; i++)
        {
            committed.Append(parsed[i].composed);
            removeJamoCount += parsed[i].consumed;
        }

        if (removeJamoCount > 0)
            jamoBuffer.RemoveRange(0, removeJamoCount);

        // 마지막 항목은 미완성 preedit 으로 유지
        preedit = parsed.Last().composed;
    }

    private List<(string composed, int consumed)> ParseJamos(List<char> arr)
    {
        var res = new List<(string composed, int consumed)>();
        int i = 0;
        while (i < arr.Count)
        {
            char cur = arr[i];

            // 모음 시작 처리 (수정: 모음 단독일 경우 초성 없이 모음만 표시)
            if (IsVowel(cur))
            {
                int consumed = 1;
                char medial = cur;

                // 겹모음 체크
                if (i + 1 < arr.Count && IsVowel(arr[i + 1]))
                {
                    string mk = $"{cur}{arr[i + 1]}";
                    if (MedialComposite.ContainsKey(mk))
                    {
                        medial = MedialComposite[mk];
                        consumed = 2;
                    }
                }

                // 종성 처리
                string finalStr = "";

                // 모음 단독인 경우 (뒤에 자음 없거나, 모음 다음에 자음이 초성일 가능성 있는 경우)
                // 여기서는 “모음만 단독” 표시 위해 초성 없이 모음 문자 그대로 출력
                // (예: ㅜ, ㅏ 같은 단독 모음)
                if (consumed == arr.Count - i)
                {
                    res.Add((new string(medial, 1), consumed));
                    i += consumed;
                    continue;
                }

                // 기존 종성 판단 유지
                if (i + consumed < arr.Count && IsConsonant(arr[i + consumed]))
                {
                    if (i + consumed + 1 < arr.Count && IsConsonant(arr[i + consumed + 1]))
                    {
                        string pk = $"{arr[i + consumed]}{arr[i + consumed + 1]}";
                        if (FinalComposite.ContainsKey(pk))
                        {
                            if (i + consumed + 2 < arr.Count && IsVowel(arr[i + consumed + 2]))
                            {
                                finalStr = arr[i + consumed].ToString();
                                consumed += 1;
                            }
                            else
                            {
                                finalStr = FinalComposite[pk];
                                consumed += 2;
                            }
                        }
                        else
                        {
                            if (i + consumed + 2 < arr.Count && IsVowel(arr[i + consumed + 2]))
                            {
                                // 다음 초성으로 넘어감
                            }
                            else
                            {
                                finalStr = arr[i + consumed].ToString();
                                consumed += 1;
                            }
                        }
                    }
                    else
                    {
                        if (i + consumed + 1 < arr.Count && IsVowel(arr[i + consumed + 1]))
                        {
                            // 다음 초성으로 넘어감
                        }
                        else
                        {
                            finalStr = arr[i + consumed].ToString();
                            consumed += 1;
                        }
                    }
                }

                // 초성 없이 조합
                if (finalStr == "")
                {
                    res.Add((new string(medial, 1), consumed));
                }
                else
                {
                    res.Add((Combine('ㅇ', medial, finalStr), consumed));
                }
                i += consumed;
                continue;
            }

            // 자음 시작 처리
            if (IsConsonant(cur))
            {
                if (i + 1 < arr.Count && IsVowel(arr[i + 1]))
                {
                    char onset = cur;
                    int usedOnset = 1;

                    if (i + 1 < arr.Count && IsConsonant(arr[i + 1]) && i + 2 < arr.Count && IsVowel(arr[i + 2]))
                    {
                        string ik = $"{arr[i]}{arr[i + 1]}";
                        if (InitialComposite.ContainsKey(ik))
                        {
                            onset = InitialComposite[ik];
                            usedOnset = 2;
                        }
                    }

                    char medial = arr[i + usedOnset];
                    int consumed = usedOnset + 1;

                    if (i + consumed < arr.Count && IsVowel(arr[i + consumed]))
                    {
                        string mk = $"{medial}{arr[i + consumed]}";
                        if (MedialComposite.ContainsKey(mk))
                        {
                            medial = MedialComposite[mk];
                            consumed += 1;
                        }
                    }

                    string finalStr = "";
                    if (i + consumed < arr.Count && IsConsonant(arr[i + consumed]))
                    {
                        if (i + consumed + 1 < arr.Count && IsConsonant(arr[i + consumed + 1]))
                        {
                            string pk = $"{arr[i + consumed]}{arr[i + consumed + 1]}";
                            if (FinalComposite.ContainsKey(pk))
                            {
                                if (i + consumed + 2 < arr.Count && IsVowel(arr[i + consumed + 2]))
                                {
                                    finalStr = arr[i + consumed].ToString();
                                    consumed += 1;
                                }
                                else
                                {
                                    finalStr = FinalComposite[pk];
                                    consumed += 2;
                                }
                            }
                            else
                            {
                                if (i + consumed + 2 < arr.Count && IsVowel(arr[i + consumed + 2]))
                                {
                                    // 다음 초성으로 넘어감
                                }
                                else
                                {
                                    finalStr = arr[i + consumed].ToString();
                                    consumed += 1;
                                }
                            }
                        }
                        else
                        {
                            if (i + consumed + 1 < arr.Count && IsVowel(arr[i + consumed + 1]))
                            {
                                // 다음 초성으로 넘어감
                            }
                            else
                            {
                                finalStr = arr[i + consumed].ToString();
                                consumed += 1;
                            }
                        }
                    }

                    res.Add((Combine(onset, medial, finalStr), consumed));
                    i += consumed;
                    continue;
                }

                // 자음 단독
                res.Add((cur.ToString(), 1));
                i += 1;
                continue;
            }

            // 그 외 문자 (에러 방지용)
            res.Add((cur.ToString(), 1));
            i += 1;
        }

        return res;
    }

    private string Combine(char cho, char jung, string jong)
    {
        int choIndex = Array.IndexOf(CHO, cho);
        if (choIndex < 0) choIndex = Array.IndexOf(CHO, 'ㅇ');
        int jungIndex = Array.IndexOf(JUNG, jung);
        if (jungIndex < 0) jungIndex = 0;
        int jongIndex = Array.IndexOf(JONG, jong);
        if (jongIndex < 0) jongIndex = 0;

        int code = 0xAC00 + (choIndex * 21 + jungIndex) * 28 + jongIndex;
        return ((char)code).ToString();
    }

    private bool IsVowel(char c) => Array.IndexOf(JUNG, c) >= 0 || MedialComposite.Values.Contains(c);
    private bool IsConsonant(char c) => Array.IndexOf(CHO, c) >= 0 || InitialComposite.Values.Contains(c);
    private bool IsJamo(char c) => IsVowel(c) || IsConsonant(c);

}




활용 예시..

HangulComposer hc = new HangulComposer();
hc.Input('ㅈ');
Console.WriteLine(hc.GuessWord());
hc.Input('ㅗ');
Console.WriteLine(hc.GuessWord());
hc.Input('ㅈ');
Console.WriteLine(hc.GuessWord());
hc.Input('ㄲ');
Console.WriteLine(hc.GuessWord());
hc.Input('ㅏ');
Console.WriteLine(hc.GuessWord());
hc.Input(' ');
Console.WriteLine(hc.GuessWord());
hc.Input('ㅅ');
Console.WriteLine(hc.GuessWord());
hc.Input('ㅡ');
Console.WriteLine(hc.GuessWord());
hc.Input('B');
Console.WriteLine(hc.GuessWord());
hc.Input('u');
Console.WriteLine(hc.GuessWord());
hc.Input('l');
Console.WriteLine(hc.GuessWord());

//
string strr = hc.GuessWord();

아발로니아 C# 프로젝트

C#으로 GUI 프로그래밍을 하는데에 있어서 여러가지 프로젝트가 있다.
윈폼, WPF, 마우이, WinUI 등등

이것은 모두 ms산하에서 나오는 프로젝트 들이며 새로운 윈도우와 밀접한 관련이 있다.
그리고 크로스 플랫폼을 지원하는 것은 마우이가 유일하다. (자마린은 언급하지말자)

요즘은 클로스 플랫폼이 대세이며 지금빌드가 되고 있는 나의 프로젝트가 타겟이 윈도우 였다가 어느순간 리눅스나 안드로이드로 바뀔수 있다.
특히 키오스트 레벨에서는 더욱 그러하다..
이러한 상황에서는 웹이 그 대안이겠지만 소켓이라던가 시리얼통신이 들어간다면 얘기는 다르다.

그리고 ms는 윈도우 프로그래밍처리를 할때  주로 다뤄졌던  윈폼과 WPF는 
이제 업데이트대상이 아니다. 그냥 비쥬얼베이직처럼 
레거시로 남아서 쭉 가는.. 그런 포지션이 되어버렸다.

wpf로 앱을 만드는 방식은 이대로라면 사장될것이 분명하다..
그래서 이러한 상황에서 나온게 아발로니아 프로젝트이다.
처음에는 나는 모노프레임워크 처럼 윈폼을 리눅스에서 돌리듯이 wpf가 리눅스에서
돌아가나 보구나.. 이런식으로 생각했다.. 그런데 그 생각은 틀렸다.
아발로니아는 닷넷 파운데이션에서 분리하여 독립적인 아키텍쳐가 되었다.
그리고 플러터처럼 스키아 엔진으로 랜더링을 지원하고 이를 기반으로 
크로스 플렛폼을 지원한다. 그래서 C#진영에서의 플러터 포지션이라고 생각한다.

wpf의 개발지식을 그대로 가져가면서 UI만 살짝 바꾸고 백단에서는 C#으로 처리된다.
C#으로 할수있는거 거의다되고 UI만 손본 느낌이다..
이게 참 매력적이다.  새롭게 언어를 안배워도 되니까..

ms는 자체적으로 DirectX가 있어서 이를 기반으로 랜더링을 지원하도록 만들었는데
이 아발로니아를 자신의 정식 프로젝트로 흡수하기에는 스키아엔진은 구글꺼라서 좀 거시기
할것이다.

그리고 마우이가 포장지는 그럴싸한데.. 이게 네이티브앱을 래핑하는 느낌으로 가기 때문에
결과적으로 실패할것으로 보인다.
하지만 아발로니아는 플러터와 같은 포지션이기 때문에 이게 뒷단에서 쭉 남아있는
자바처럼 될수도 있다.. 망하지는 않을것 같고 업데이트는 계속될 것 같다.
아니 오히려 플러터는 컴퍼넌트 트리기반이라서 랜더링에 직접관여하지 않는 구조이지만
아발로니아는 로우레벨 렌더링을 직접컨트롤 할 수 있다.
그래서 C#을 배우고 GUI프로그래밍을 하려거든 나는 아발로니아를 추천한다.
물론 윈폼에 비해서 학습곡선이 높은편이기는 하지만 그 고비만 넘기면 어느정도는 쓸만하다.

메시지 드리븐과 위지윅방식의 윈폼은 UI의 유려함을 따지지 않는 프로젝트에서는 
개발속도만 놓고 따지면 WPF를 압도한다. 하지만 키오스크처럼 유려한 디자인이 중요한
프로젝트에서는 그와 양상이 반대가 된다.  게다가 OS의존성까지 없에려고 한다면 더욱이
아발로니아를 사용하는것을 추천한다.

WinUI가 새로운 개발트랜드인것 처럼 꾸며지는데 물론 네이티브앱으로써 새
로운 이벤트라던가 메시징이 운영체제에서 생겨나는것에 대해서는 좋다.. 하지만..
내가 봤을때 그런거는 이미 성숙기를 지났기때문에 혁신적인 메시지가 나오기란 힘들지 않을까??

일단 어느정도 종결자가 나온것 같은 느낌이긴 하다..  비슷한 포지션으로 우노플랫폼이 있는데..
이거는 모바일중심으로 보여지고 xaml이 상대적으로 덜성숙해 보이고 제한적인 커스터 마이징에 메서드체이닝으로 ui빌드에 상태관리 까지 있는것으로 보아 진짜로 플러터랑 비벼야할것 같다..
뭐 어쨋든.. 기술은 계속 발전하니까 
또 어떻게 변할지는 아무도 모르고 AI가 완전히 흡수해서 GUI프로그래밍 자체가
필요 없어질 수 도 있겠지만..

2025년 7월 31일 목요일

하기싫은 자바 치고 빠지기..

난 자바를 싫어한다.. 빌드환경이 난잡하고 보일러 플레이팅이 많아서..
그런데 주변환경에서 어쩔수 없이 자바를 해야하는 경우가 생긴다..
에휴 신발..  그리하야 자바 빌드과정을 정리를 한다..  
(메이븐이니 그리들이니 이딴건 집어치우고 본질에 집중)
(그리고 여담이지만 한국 지역정보 같은 자료 참조처리할때 GPKI 모듈 사용 하고
어쩌고 하려면 반드시 Java로 신청해라.. C언어로 된것도 테스트할라면 
Java로 해야한다.. 망할넘들 -_-)


[자바 빌드 정리]



javac 사용 예시

javac -encoding UTF-8 -d . -cp "classes;lib/*" src\Main.java

(src폴더 아래에 있는 Main.java파일을 컴파일하는데 참조클래스 경로는 현경로 아래에 
있는 classes폴더와 lib를 참조하고 Main.java파일을 컴파일 할때는 UTF8로 인코딩으로
하여 빌드하고 최종 출력되는 Main.class 파일은 현재폴더에 출력하라
-cp 옵션일때  근본적으로 모든 파일을 하나하나 묶어야 하지만 * 표현법으로 "lib\*" 라고
표현이 가능함)

javac -d classes -encoding UTF8 src\ownutil\biz\*.java
(src - ownutil - biz 안에 있는 모든 자바파일을 한번에 컴파일하여 루트밑에 있는 classes
폴더에 복사하라 단 인코딩옵션은 utf8이다.)

javac -encoding UTF-8 -d classes -sourcepath src src/Main.java
(src - Main.java를 컴파일하는데 src밑에 있는 하위 패키지들까지 모드 포함하여 classes폴더에 컴파일 하라  src에 Main.java가 있고 그밑에 참조 java파일들이 존재하는 경우)

[cmd 배치파일 명령으로 컴파일]
FOR /R . %f IN (*.java) DO javac -d ..\classes -encoding UTF8 %f
(현재폴더 아래를 재귀적으로 하위폴더까지 돌면서 모든 java파일을 javac로 컴파일하여
한수준 위에 있는 classes폴더에 *.class 파일을 만들어라. 인코딩 옵션은 utf8이다.


jar파일로 묶는 예시


jar -cfv mylib.jar -C classes .
(classes폴더에 있는 모든 class파일을 재귀적으로 돌면서 jar아카이브로 묶꼬 c옵션으로 jar를 새로 생성하고 f옵션으로 생성할 jar파일의 이름을 mylib.jar로 지정하고 v옵션으로 jar파일의 생성과정을 표출하라 단 클래스 아카이브폴더인 classes폴더를 기준으로 하위폴더에 있는 package를 기준으로한다!!  -C옵션 중요!!)

jar -cfev myapp.jar Main classes
(classes폴더에 있는 모든 class파일을 재귀적으로 돌면서 jar아카이브로 묶고 c옵션으로 jar를 새로 생성하고 f옵션으로 생성할 jar파일의 이름을 myapp.jar로 지정하고 e옵션으로 실행될 엔트리 포인트는 Main클래스이며 v옵션으로 jar파일의 생성과정을 표출하라. - 이명령은 진입점을 포함함)




java 실행 옵션


java -cp ".;lib\*" Main
(Main.class 파일을 실행하라 참조라이브러리는 현재 폴더 혹은 lib폴더에 있다.
-cp 인 클래스 패스가 중요함!!  거기에 Main.class파일도 있어야함)

java -cp "classes;lib\*" Main
(Main.class 파일을 실행하라 참조라이브러리는 classes폴더와 lib폴더에 있다. 이 두개의 폴더중에서 Main.class 파일이 있으니까 찾아서 실행해라.. 만약 Main.class파일이 2개이상있다면 cp옵션에서 지정한 순서중 먼저나오는 아이의 Main.class 파일이 우선적이다.)

java -jar MyApp.jar
jar파일로 묶인 아카이브 파일을 java명령을 통해서 실행함.







***  윈도우의 배치 파일 명령 ***

자~!! 다 좋다..  그런데 언제 저거를 일일이 다 치고 있겠는가??
ide를 빌려서 하지.. 하지만 이런식으로 가버리면 javac 명령이건 뭐건 
암것도 모르는 툴키드 멍청이가 되것지??

그래서 내부구조를 들여다 볼수있는 배치파일로 만든다~!!
그러면 그럼 이제 폴더 구조부터 잡겠다.
일단 내프로젝트 폴더를 하나만든다  이폴더를 이제 루트(root)폴더 라고 칭하겠다.
그리고 그 밑에 classes폴더, lib폴더, src폴더를 만든다.
classes폴더 -> javac하여 class파일이 들어갈 폴더
lib폴더 -> 프로그램에서 참조하는 라이브러리(*.jar)들이 들어갈 폴더
src폴더 -> Main.java 파일이 있는(public static void main) 폴더임
src폴더 밑에는 mycom - biz 등등 자기가 만든 패키지가 들어가 있음.


그래서 이런 구조에서 만들고 활용하는 배치파일을 3가지를 준비했다.
buildapp.bat -> 해당구조에서 classes 폴더에 java파일을 빌드해서 class파일로 변경
runapp.bat -> 해당구조에서 Main.class파일을 실행
makejar.bat -> 해당구조에서 Myapp.jar 파일을 생성 (이름바꾸기는 알아서..)
이렇게 3개 이다.


[buildapp.bat]

@echo off
javac -d classes -encoding UTF-8 -cp "lib\*" -sourcepath src src/Main.java



[runapp.bat]

@echo off
java -cp "classes;lib\*" Main



[makejar.bat]

@echo off
setlocal enabledelayedexpansion

rem 설정값
set JAR_NAME=MyApp.jar
set CLASS_PATH=classes
set LIB_PATH=lib

rem 컴파일
javac -d classes -encoding UTF-8 -cp "lib\*" -sourcepath src src/Main.java

rem 매니페스트 파일 생성 / Class-Path는 Jar 파일 내부에서 라이브러리 경로를 찾기 위한 설정
echo Main-Class: Main > manifest.txt
set classpath_string=
for %%f in ("%LIB_PATH%\*.jar") do (
    set "classpath_string=!classpath_string! %LIB_PATH%/%%~nxf"
)
if not "!classpath_string!"=="" (
    echo Class-Path: !classpath_string! >> manifest.txt
) else (
    echo No library files found in %LIB_PATH%
)
echo Class-Path is !classpath_string!

rem 컴파일된 클래스와 매니페스트 파일을 포함하여 Jar 파일 생성
jar -cvfm "%JAR_NAME%" manifest.txt -C classes .

rem 임시 매니페스트 파일 삭제
del manifest.txt

echo %JAR_NAME% File Create Done.



이런식으로 만들었으니 활용하면된다.
java프로그램을 급하게 만들어야 하는 상황이고
난 java를 그닥 좋아하지 않으니까.. 메이븐이나 그리들을 활용하거나
IDE를 활용할 생각은 없다. 

ㅋㅋㅋ



2025년 7월 30일 수요일

안드로이드 웹뷰로된 어플 빨리 만들기 (코틀린 버전)

안드로이드 웹앱 빨리 만들기 자바버전이 이제 안된다..
코틀린으로 바꼈으니까 그리들이랑 해서 새롭게 다시 빌드해야함..
귀찮은거 시러하니 다시 한번 빨리 가보자!!

이런거는 플러터로 하면 이상해진다..
web_view 라던가 권한이라던가 intent처리라던가 쿠키라던가
매우 복잡해진다.  안드로이드 시스템의 근본앱은 코틀린앱 이기때문에
안드로이드 시스템에 대해서 자원활용이 굉장히 용이하다.

반면 플러터로 한다면 이것저것 신경쓸것이 많다.  이런류의 앱은
서비스 중심이라기 보다는 있는 자원을 OS레벨에 최대한 가까운 네이티브앱이
최고다.  코틀린 언어는 몰라도 된다.. (나도 모르고..  하지만 대충 JAVA로
했던 지난 이력이 있으니까 그러려니 하면서 보면 보인다.)

하... 그냥 이런 고민하지말고  블레이저 익혀서  프로그레시브웹 만들면 되는데..


(준비물은 이미지 파일 준비하셈)


1. 안드로이드 스튜디오로 새 안드로이드 프로젝트 하나 만든다. (empty activity로)


2. app -> res 우클릭 New Image Asset으로 Source Asset의 Path에 그림파일 선택하고
Scaling에서 Trim이나 Resize하면서 적절히 배치후 finish하면 아이콘 만들어짐


3.이미지 업로드용 파일제공을 위해 res -> xml -> file_paths.xml 파일을 만들고
내용을 다음과 같이 기제

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="images" path="Pictures" />
</paths>


4. AndroidManifest.xml 파일 세팅

퍼미션:

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />


앱이름변경:

   manifest -> application -> android:lable 을 한글 앱이름으로 변경



2025년 7월 29일 화요일

여름철 보습에 관해 AI에 물어본 결과

한국의 여름철, 특히 고온다습한 환경이 역설적으로 피부를 더 건조하게 만들 수 있다는 점을 아주 정확하게 파악하셨습니다. 많은 분들이 "습하니까 보습은 충분하겠지"라고 생각하지만, 실제로는 그렇지 않은 경우가 많습니다.

말씀하신 과정을 다시 정리해 드릴게요.

여름철 피부 건조의 악순환

  1. 높은 습도와 땀 분비 증가: 한국 여름은 기온과 습도가 모두 높습니다. 우리 몸은 체온 조절을 위해 더 많은 을 분비하게 됩니다.

  2. 땀의 불완전한 증발: 공기 중의 습도가 너무 높으면 땀이 피부 표면에서 효과적으로 증발하기 어렵습니다. 땀이 증발해야 피부 온도가 내려가는데, 증발이 잘 안 되니 땀이 계속 피부에 머물게 됩니다.

  3. 피부 장벽 약화:

    • 과도한 습기: 피부가 장시간 과도하게 습한 상태에 노출되면, 피부를 보호하는 지질 장벽이 약해질 수 있습니다. 마치 물에 불린 종이가 약해지듯이, 피부 장벽의 구조가 느슨해지거나 손상될 수 있죠.

    • 알칼리화: 땀의 일부 성분은 피부의 약산성 상태를 약화시켜 알칼리성으로 만들 수 있습니다. 피부가 알칼리화되면 장벽 기능이 더욱 저하되기 쉽습니다.

    • 염증 반응: 땀과 노폐물이 피부에 오래 머물고, 세균 번식이 활발해지면 미세한 염증 반응이 생길 수 있으며, 이는 피부 장벽을 손상시키는 요인이 됩니다.

  4. 의류와의 마찰 증가: 땀으로 인해 피부가 축축해지면 옷이 피부에 달라붙어 마찰이 심해집니다. 이 마찰은 물리적으로 피부 장벽을 손상시키고, 각질층을 자극하여 피부를 더욱 예민하고 건조하게 만들 수 있습니다.

  5. 내부 수분 손실: 약해진 피부 장벽은 피부 속의 수분을 지켜내는 능력이 떨어집니다. 외부 습도가 높더라도, 손상된 장벽을 통해 피부 내부의 중요한 수분(자연 보습 인자, 세포 간 지질 등)이 오히려 더 쉽게 증발해 버릴 수 있습니다. 그 결과 피부 속은 건조함을 느끼게 됩니다.

결론적으로, 여름철의 고온다습한 환경, 과도한 땀, 그리고 옷과의 마찰이 복합적으로 작용하여 피부 장벽을 약화시키고, 결국 겉은 축축해도 속은 건조한 상태를 만들 수 있습니다. 이것이 피부 질환이 여름철에 더 심해지는 주된 원인 중 하나가 되는 것이죠.