2025년 8월 14일 목요일

한스락을 만들고 난 소감? 같은거

한스락 (HansLock) 공개 — 한영키 리매퍼

개인 제작 윈도우용 유틸리티

오늘 개인 제작 윈도우용 유틸리티를 만들어서 블로그에 공개했다.

그렇게 대단한 건 아니고...

Mac OS 스타일 한영키 리매퍼이다. 


컴퓨터를 사용하다가 느낀 불편함을 직접 해결했는데
이제는 내가 기존 능력으로 할 수 없었던 것을 할 수 있게 되었다고 생각한다.


내가 C++로 만든 프로그램을 공개할 수 있다니 신기해.
누군가에게는 겨우 Hello World 수준일지라도 말이지.
스스로에게는 좀 놀라운 날이다.


내가 실제로 써보면서 약간의 버그를 찾고 수정하고,
이제는 그럭저럭 괜찮다싶어서 배포한다.
몇일정도 실제로 내가 써보니까 괜찮았거든... 아마 배포해도 괜찮겠지?

프로그램 정보

이름
한스락 (HansLock)
소개 & 다운로드
페이지로 이동하기

사용해보시고 의견이나 버그 제보를 남겨주시면 다음 업데이트에 가능한 적극 반영할게요.

2025년 3월 15일 토요일

Python 으로 작성한 리버스 프록시 코드 (Flask 사용 내부 테스트용)

파이썬으로 작성한 리버스프록시 코드. 안드로이드 앱이 외부 접속을 할 때 https 가 아니면 안되는 경우가 있어서 제작함.


from flask import Flask, request, Response
import requests
import logging
from logging.handlers import RotatingFileHandler

app = Flask(__name__)
# netsh advfirewall firewall add rule name="Allow [EXTERNAL_PORT]" dir=in action=allow protocol=TCP localport=[EXTERNAL_PORT]
# 포워딩할 대상 URL (내부 HTTP 서버)
INTERNAL_PORT = 54321  # 내부 서버 포트 (예시)
TARGET = f"http://127.0.0.1:{INTERNAL_PORT}"

# 로그 파일 설정: server.log 파일에 최대 10,000바이트까지 저장하며, 1개의 백업 파일 생성
handler = RotatingFileHandler('server.log', maxBytes=10000, backupCount=1, encoding='utf-8')
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
handler.setFormatter(formatter)
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)

# 모든 경로와 HTTP 메서드를 처리하도록 라우팅
@app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
@app.route('/', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
def proxy(path):
    target_url = f"{TARGET}/{path}"
    app.logger.info(f"Request: {request.method} {request.url} -> Forwarding to: {target_url}")
    
    headers = {key: value for key, value in request.headers if key.lower() != 'host'}
    
    # 대상 서버에 요청 전송
    resp = requests.request(
        method=request.method,
        url=target_url,
        headers=headers,
        params=request.args,
        data=request.get_data(),
        cookies=request.cookies,
        allow_redirects=False
    )
    
    app.logger.info(f"Response: Status code {resp.status_code} from {target_url}")
    
    # 대상 서버의 응답을 그대로 클라이언트에 전달
    response = Response(resp.content, resp.status_code)
    for key, value in resp.headers.items():
        response.headers[key] = value
    return response

if __name__ == '__main__':
    # Let's Encrypt 인증서 사용 (인증서 파일 경로는 마스킹 처리됨)
    ssl_context = ("[인증서 경로]/fullchain.pem", "[인증서 경로]/privkey.pem")
    EXTERNAL_PORT = 12345  # 외부에 노출될 포트 (예시)
    app.logger.info(f"Starting HTTPS reverse proxy server on port {EXTERNAL_PORT} with Let's Encrypt certificate")
    app.run(host='0.0.0.0', port=EXTERNAL_PORT, ssl_context=ssl_context)


2025년 3월 5일 수요일

Using OpenAI's ChatGPT with Unity C# via API // Unity로 ChatGPT 챗봇 만들기 (API 이용)

# Unity C# 으로 구동하는 챗봇 코드
내가 쓸려고 간단히 만든 것을 기록용으로 공개한다.
아래 내용을 유니티에 붙여 넣는다.

1. 빈 gameobject 를 하나 생성한다.
2. new component 로 ChatbotManager 라는 스크립트를 생성해서 1에 붙인다.
3. 코드에디터로 아래 내용을 복사해서 2의 파일에 붙이고 저장한다.
4. 유니티 내 StreamingAssets 경로에 auth.json 을 생성한다.
 

    /// StreamingAssets 폴더의 auth.json 파일에서 API 키를 로드합니다.
    /// 예시 파일 형식: { "api_key": "your_api_key_here" }


5. 유니티로 돌아와서 플레이 해본다. log 창에 표시가 된다.

# 사용 법
StartNewConversation() : 새로운 대화 스레드 시작
SendUserMessage() : 내 대사 전달

# 메인 코드


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

[System.Serializable]
public class ChatMessage {
    public string role;
    public string content;
}

[System.Serializable]
public class ChatCompletionRequest {
    public string model;
    public List messages;
    public int max_tokens;
    public float temperature;
}

[System.Serializable]
public class ChatCompletionChoice {
    public ChatMessage message;
    public string finish_reason;
    public int index;
}

[System.Serializable]
public class ChatbotResponse {
    public string id;
    public string @object;
    public int created;
    public string model;
    public List choices;
}

[System.Serializable]
public class AuthData {
    public string api_key;
}

public class ChatbotManager : MonoBehaviour
{
    // Inspector에 노출되지 않도록 private const로 선언하여 잘못된 값이 덮어씌워지지 않게 합니다.
    private const string apiUrl = "https://api.openai.com/v1/chat/completions";
    public string model = "gpt-4o-mini";
    // public string model = "gpt-4o-mini"; // "o3-mini"; //"gpt-3.5-turbo"
    
    // 응답 속도 개선을 위한 추가 파라미터
    [Tooltip("응답 생성 시 최대 토큰 수를 제한합니다. 낮을수록 응답이 빨라집니다.")]
    public int maxTokens = 150;
    
    [Tooltip("응답의 창의성을 조절합니다. 0에 가까울수록 결정적이고 빠른 응답이 생성됩니다.")]
    [Range(0f, 1f)]
    public float temperature = 0.3f;
    
    private string apiKey;
    private List messages = new List();
    
    [TextArea(3, 10)]
    [Tooltip("챗봇에게 전달할 초기 시스템 인스트럭션입니다. 챗봇의 역할과 행동 방식을 정의합니다.")]
    public string initialInstruction = "안녕하세요, 챗봇과 대화를 시작합니다. 아래 instruction을 참고하세요.";

    [Tooltip("대화 기록에 유지할 최대 메시지 수입니다. 너무 많은 메시지는 응답 속도를 늦출 수 있습니다.")]
    public int maxMessageHistory = 10;
    
    [Tooltip("대화 기록을 저장하는 배열입니다. 인스펙터에서 확인할 수 있습니다.")]
    [SerializeField] private ChatMessage[] messageHistory;

    void Start()
    {
        LoadApiKey();
        StartNewConversation();
    }

    /// 
    /// StreamingAssets 폴더의 auth.json 파일에서 API 키를 로드합니다.
    /// 예시 파일 형식: { "api_key": "your_api_key_here" }
    /// 
    private void LoadApiKey()
    {
        string authFilePath = System.IO.Path.Combine(Application.streamingAssetsPath, "auth.json");

        if (System.IO.File.Exists(authFilePath))
        {
            try
            {
                string jsonContent = System.IO.File.ReadAllText(authFilePath);
                Debug.Log("로드된 auth.json 내용: " + jsonContent);

                if (!jsonContent.Trim().StartsWith("{") || !jsonContent.Trim().EndsWith("}"))
                {
                    Debug.LogError("auth.json 파일이 올바른 JSON 형식이 아닙니다. 올바른 형식: {\"api_key\":\"your_api_key_here\"}");
                    return;
                }

                AuthData authData = JsonUtility.FromJson(jsonContent);
                if (authData != null && !string.IsNullOrEmpty(authData.api_key))
                {
                    apiKey = authData.api_key;
                    Debug.Log("API 키가 성공적으로 로드되었습니다.");
                }
                else
                {
                    Debug.LogError("auth.json 파일에서 API 키를 찾을 수 없습니다.");
                }
            }
            catch (System.Exception e)
            {
                Debug.LogError("auth.json 파일 파싱 중 오류 발생: " + e.Message);
            }
        }
        else
        {
            Debug.LogError("auth.json 파일을 찾을 수 없습니다. 경로: " + authFilePath);
        }
    }

    /// 
    /// 새 대화를 시작합니다. 기존 대화 기록을 초기화하고 초기 instruction 메시지를 추가합니다.
    /// 
    public void StartNewConversation()
    {
        messages.Clear();

        ChatMessage systemMessage = new ChatMessage { role = "system", content = initialInstruction };
        messages.Add(systemMessage);
        
        // 메시지 배열 업데이트
        messageHistory = messages.ToArray();

        Debug.Log("새 대화가 시작되었습니다. 초기 instruction이 추가되었습니다.");
    }

    /// 
    /// 사용자 메시지를 추가하고 API로 전송합니다.
    /// 
    /// 사용자 입력 메시지
    public void SendUserMessage(string userMessageContent)
    {
        ChatMessage userMessage = new ChatMessage { role = "user", content = userMessageContent };
        messages.Add(userMessage);
        
        // 메시지 수가 제한을 초과하면 오래된 메시지 제거 (시스템 메시지는 유지)
        TrimMessageHistory();
        
        StartCoroutine(SendRequestCoroutine());
    }

    /// 
    /// 대화 기록이 너무 길어지지 않도록 오래된 메시지를 제거합니다.
    /// 
    private void TrimMessageHistory()
    {
        // 시스템 메시지를 제외한 메시지 수가 maxMessageHistory를 초과하면 오래된 메시지 제거
        if (messages.Count > maxMessageHistory + 1) // +1은 시스템 메시지 때문
        {
            // 시스템 메시지는 항상 인덱스 0에 있다고 가정
            int excessMessages = messages.Count - maxMessageHistory - 1;
            messages.RemoveRange(1, excessMessages); // 시스템 메시지 다음부터 제거
        }
        
        // 메시지 리스트를 배열로 변환하여 인스펙터에서 확인할 수 있게 함
        messageHistory = messages.ToArray();
    }

    /// 
    /// 대화 전체를 JSON으로 직렬화하여 API에 POST 요청을 보내고, 응답을 받아 처리합니다.
    /// 
    IEnumerator SendRequestCoroutine()
    {
        if (string.IsNullOrEmpty(apiKey))
        {
            Debug.LogError("API 키가 설정되지 않았습니다. API 요청을 보낼 수 없습니다.");
            yield break;
        }

        ChatCompletionRequest requestObj = new ChatCompletionRequest
        {
            model = model,
            messages = messages,
            max_tokens = maxTokens,
            temperature = temperature
        };
        string jsonData = JsonUtility.ToJson(requestObj);
        Debug.Log("요청 JSON: " + jsonData);

        // UnityWebRequest 생성 시 POST 메서드를 직접 지정합니다.
        UnityWebRequest request = new UnityWebRequest(apiUrl, "POST");
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonData);
        request.uploadHandler = new UploadHandlerRaw(bodyRaw);
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");

        // API 키에서 불필요한 문자를 제거합니다.
        string sanitizedApiKey = apiKey.Trim().Replace("\n", "").Replace("\r", "").Replace("\t", "").Replace("\"", "");
        request.SetRequestHeader("Authorization", "Bearer " + sanitizedApiKey);

        Debug.Log("API URL: " + apiUrl);
        Debug.Log("요청 메서드: " + request.method);

        yield return request.SendWebRequest();

        Debug.Log("응답 상태 코드: " + request.responseCode);

        if (request.result == UnityWebRequest.Result.Success)
        {
            string responseText = request.downloadHandler.text;
            Debug.Log("응답 JSON: " + responseText);

            ChatbotResponse response = JsonUtility.FromJson(responseText);
            if (response != null && response.choices != null && response.choices.Count > 0)
            {
                ChatMessage assistantMessage = response.choices[0].message;
                messages.Add(assistantMessage);
                // 메시지 배열 업데이트
                messageHistory = messages.ToArray();
                Debug.Log("Assistant: " + assistantMessage.content);
            }
        }
        else
        {
            Debug.LogError("API 요청 오류: " + request.error);
            Debug.LogError("응답 내용: " + request.downloadHandler.text);
        }
    }
}



내가 도움 받은 만큼 다른 누군가에게 도움이 되기를. 끝.