#nullable enable using System; using System.Collections.Generic; using System.Globalization; using System.Text; using System.Text.RegularExpressions; using UnityEngine; using UVC.Json; using UVC.Log; namespace UVC.Extention { /// /// 문자열 확장 메소드를 제공하는 클래스입니다. /// public static class StringEx { /// /// 문자열을 지정된 열거형 타입으로 변환합니다. /// /// 변환할 열거형 타입 /// 변환할 문자열 /// 변환된 열거형 값 /// 문자열이 열거형 값으로 변환될 수 없을 때 발생 /// /// /// // 사용 예시 /// public enum Direction { North, South, East, West } /// /// string dirText = "North"; /// Direction dir = dirText.ToEnum(); // Direction.North가 반환됨 /// /// public static T ToEnum(this string txt) { return (T)Enum.Parse(typeof(T), txt); } /// /// 문자열이 null이거나 빈 문자열인지 확인합니다. /// /// 검사할 문자열 /// 문자열이 null이거나 빈 문자열이면 true, 그렇지 않으면 false /// /// /// // 사용 예시 /// string text1 = null; /// string text2 = ""; /// string text3 = "Hello"; /// /// bool result1 = text1.IsNullOrEmpty(); // true 반환 /// bool result2 = text2.IsNullOrEmpty(); // true 반환 /// bool result3 = text3.IsNullOrEmpty(); // false 반환 /// /// public static bool IsNullOrEmpty(this string value) { return value == null || value.Length == 0; } /// /// 문자열이 null, 빈 문자열 또는 공백 문자로만 구성되어 있는지 확인합니다. /// /// 검사할 문자열 /// 문자열이 null, 빈 문자열 또는 공백 문자로만 구성되어 있으면 true, 그렇지 않으면 false /// /// /// string text1 = null; /// string text2 = " "; /// string text3 = "Hello"; /// /// bool result1 = text1.IsNullOrWhiteSpace(); // true 반환 /// bool result2 = text2.IsNullOrWhiteSpace(); // true 반환 /// bool result3 = text3.IsNullOrWhiteSpace(); // false 반환 /// /// public static bool IsNullOrWhiteSpace(this string value) { if (value == null) return true; for (int i = 0; i < value.Length; i++) { if (!char.IsWhiteSpace(value[i])) { return false; } } return true; } /// /// 문자열을 지정된 타입으로 변환합니다. /// /// 변환할 타입 /// 변환할 문자열 /// 변환 실패시 반환할 기본값 /// 변환된 값 또는 기본값 /// /// /// string numStr = "123"; /// string invalidStr = "abc"; /// /// int num1 = numStr.To(); // 123 반환 /// int num2 = invalidStr.To(0); // 0 반환 (기본값) /// /// public static T To(this string value, T defaultValue = default) { try { return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); } catch { return defaultValue; } } /// /// 문자열의 첫 글자만 대문자로 변환합니다. /// /// 변환할 문자열 /// 첫 글자가 대문자로 변환된 문자열 /// /// /// string text = "hello world"; /// string result = text.Capitalize(); // "Hello world" 반환 /// /// public static string Capitalize(this string value) { if (value.IsNullOrEmpty()) return value; return char.ToUpper(value[0]) + value.Substring(1); } /// /// 주어진 패턴을 이용하여 문자열이 정규식 패턴과 일치하는지 확인합니다. /// /// 검사할 문자열 /// 정규식 패턴 /// 문자열이 패턴과 일치하면 true, 그렇지 않으면 false /// /// /// string email = "user@example.com"; /// string pattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$"; /// /// bool isValid = email.IsMatch(pattern); // true 반환 /// /// public static bool IsMatch(this string value, string pattern) { if (value == null) return false; return Regex.IsMatch(value, pattern); } /// /// JSON 문자열을 Dictionary로 변환합니다. /// /// Dictionary의 키 타입 /// Dictionary의 값 타입 /// 변환할 JSON 문자열 /// 변환된 Dictionary 객체 /// JSON 문자열 형식이 올바르지 않은 경우 발생 /// /// /// string json = "{\"name\":\"홍길동\",\"age\":30,\"isStudent\":false}"; /// var dict = json.ToDictionaryFromJson(); /// /// // 결과: /// // dict["name"] == "홍길동" /// // dict["age"] == 30 /// // dict["isStudent"] == false /// /// // 중첩된 객체도 처리 가능 /// string nestedJson = "{\"user\":{\"name\":\"홍길동\",\"address\":{\"city\":\"서울\"}}}"; /// var nestedDict = nestedJson.ToDictionaryFromJson(); /// /// public static Dictionary ToDictionaryFromJson(this string json) { if (json.IsNullOrWhiteSpace()) { return new Dictionary(); } // JSON 객체가 아닌 경우 예외 발생 json = json.Trim(); if (!json.StartsWith("{") || !json.EndsWith("}")) throw new ArgumentException("유효한 JSON 객체 형식이 아닙니다.", nameof(json)); try { // JsonHelper를 사용하여 Dictionary로 변환 return JsonHelper.FromJson>(json); } catch (Exception ex) { // 변환 실패 시 예외 정보 로깅 (선택적) ULog.Error($"JSON을 Dictionary로 변환하는 중 오류 발생: {ex.Message}", ex); return new Dictionary(); } } /// /// JSON 문자열을 Dictionary로 변환합니다. /// /// Dictionary의 키 타입 (일반적으로 string) /// Dictionary의 값 타입 /// 변환할 JSON 문자열 /// JSON 문자열에서 파싱된 Dictionary /// JSON 문자열 형식이 올바르지 않은 경우 발생 /// /// /// string jsonStr = "{\"name\":\"홍길동\",\"age\":30,\"isStudent\":false}"; /// var dict = jsonStr.ToDictionary(); /// /// // 결과: /// // dict["name"] = "홍길동" /// // dict["age"] = 30 /// // dict["isStudent"] = false /// /// public static Dictionary ToDictionary(this string json) { // JSON 문자열이 null이거나 비어있는 경우 if (json.IsNullOrWhiteSpace()) return new Dictionary(); // JSON 객체가 아닌 경우 예외 발생 json = json.Trim(); if (!json.StartsWith("{") || !json.EndsWith("}")) throw new ArgumentException("유효한 JSON 객체 형식이 아닙니다.", nameof(json)); // Dictionary 생성 var result = new Dictionary(); // 중괄호 제거 후 내용만 추출 json = json.Substring(1, json.Length - 2).Trim(); if (string.IsNullOrEmpty(json)) return result; int startIndex = 0; while (startIndex < json.Length) { // 키 추출 int keyStartIndex = json.IndexOf('"', startIndex); if (keyStartIndex == -1) break; int keyEndIndex = FindClosingQuote(json, keyStartIndex + 1); if (keyEndIndex == -1) throw new ArgumentException("JSON 키 형식이 올바르지 않습니다.", nameof(json)); string keyStr = json.Substring(keyStartIndex + 1, keyEndIndex - keyStartIndex - 1); // : 찾기 int colonIndex = json.IndexOf(':', keyEndIndex + 1); if (colonIndex == -1) throw new ArgumentException("JSON 형식이 올바르지 않습니다: 콜론이 없습니다.", nameof(json)); // 값의 시작 위치 int valueStartIndex = FindNextNonWhitespaceIndex(json, colonIndex + 1); if (valueStartIndex >= json.Length) break; // 값 유형에 따른 처리 object value; int nextStartIndex; char firstChar = json[valueStartIndex]; switch (firstChar) { case '{': // 중첩된 객체 int objectEndIndex = FindMatchingClosingBrace(json, valueStartIndex); string objectJson = json.Substring(valueStartIndex, objectEndIndex - valueStartIndex + 1); // 재귀적으로 중첩 객체 처리 (최종 타입에 따라 처리) if (typeof(TValue) == typeof(Dictionary)) { value = objectJson.ToDictionary(); } else if (typeof(TValue) == typeof(Dictionary)) { value = objectJson.ToDictionary(); } else { // 기본적으로 문자열로 저장 value = objectJson; } nextStartIndex = objectEndIndex + 1; break; case '[': // 배열(현재 구현에서는 단순 문자열로 처리) int arrayEndIndex = FindMatchingClosingBracket(json, valueStartIndex); value = json.Substring(valueStartIndex, arrayEndIndex - valueStartIndex + 1); nextStartIndex = arrayEndIndex + 1; break; case '"': // 문자열 int stringEndIndex = FindClosingQuote(json, valueStartIndex + 1); string stringValue = json.Substring(valueStartIndex + 1, stringEndIndex - valueStartIndex - 1); value = UnescapeJsonString(stringValue); nextStartIndex = stringEndIndex + 1; break; default: // 숫자, boolean, null int commaOrEndIndex = json.IndexOfAny(new[] { ',', '}' }, valueStartIndex); if (commaOrEndIndex == -1) commaOrEndIndex = json.Length; string primitiveValueStr = json.Substring(valueStartIndex, commaOrEndIndex - valueStartIndex).Trim(); // null, boolean, 숫자 처리 if (primitiveValueStr == "null") { value = null; } else if (primitiveValueStr == "true") { value = true; } else if (primitiveValueStr == "false") { value = false; } else if (double.TryParse(primitiveValueStr, NumberStyles.Any, CultureInfo.InvariantCulture, out double numValue)) { // 정수인지 실수인지 확인 if (Math.Floor(numValue) == numValue && numValue >= int.MinValue && numValue <= int.MaxValue) { value = (int)numValue; } else { value = numValue; } } else { value = primitiveValueStr; // 숫자로 파싱할 수 없다면 문자열로 처리 } nextStartIndex = commaOrEndIndex; break; } // 키와 값을 딕셔너리에 추가 (형변환 시도) try { var key = (TKey)Convert.ChangeType(keyStr, typeof(TKey), CultureInfo.InvariantCulture); result[key] = value is TValue tValue ? tValue : (TValue)Convert.ChangeType(value, typeof(TValue), CultureInfo.InvariantCulture); } catch { // 형변환 실패 시 건너뜀 } // 다음 키-값 쌍의 시작 위치 찾기 startIndex = json.IndexOf(',', nextStartIndex); if (startIndex == -1) break; startIndex++; // 콤마 다음 위치로 이동 } return result; } /// /// JSON 문자열의 이스케이프된 문자를 원래 문자로 변환합니다. /// private static string UnescapeJsonString(string value) { if (string.IsNullOrEmpty(value)) return value; StringBuilder sb = new StringBuilder(value.Length); int i = 0; while (i < value.Length) { char c = value[i++]; if (c == '\\' && i < value.Length) { char escapeChar = value[i++]; switch (escapeChar) { case '\\': sb.Append('\\'); break; case '\"': sb.Append('\"'); break; case 'n': sb.Append('\n'); break; case 'r': sb.Append('\r'); break; case 't': sb.Append('\t'); break; case 'b': sb.Append('\b'); break; case 'f': sb.Append('\f'); break; case 'u': if (i + 4 <= value.Length) { string unicodeHex = value.Substring(i, 4); if (int.TryParse(unicodeHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int unicodeValue)) { sb.Append((char)unicodeValue); i += 4; } else { sb.Append(escapeChar); } } else { sb.Append(escapeChar); } break; default: sb.Append(escapeChar); break; } } else { sb.Append(c); } } return sb.ToString(); } /// /// JSON 문자열에서 닫는 따옴표의 위치를 찾습니다. /// private static int FindClosingQuote(string json, int startIndex) { for (int i = startIndex; i < json.Length; i++) { if (json[i] == '\"' && (i == 0 || json[i - 1] != '\\')) return i; if (json[i] == '\\') i++; // 이스케이프된 문자 건너뛰기 } return -1; } /// /// 주어진 위치 이후의 첫 번째 공백이 아닌 문자의 위치를 반환합니다. /// private static int FindNextNonWhitespaceIndex(string text, int startIndex) { for (int i = startIndex; i < text.Length; i++) { if (!char.IsWhiteSpace(text[i])) return i; } return text.Length; } /// /// 여는 중괄호에 대응하는 닫는 중괄호의 위치를 찾습니다. /// private static int FindMatchingClosingBrace(string json, int openBraceIndex) { int depth = 1; bool inString = false; for (int i = openBraceIndex + 1; i < json.Length; i++) { if (json[i] == '\"' && (i == 0 || json[i - 1] != '\\')) { inString = !inString; continue; } if (inString) continue; if (json[i] == '{') { depth++; } else if (json[i] == '}') { depth--; if (depth == 0) return i; } } return -1; } /// /// 여는 대괄호에 대응하는 닫는 대괄호의 위치를 찾습니다. /// private static int FindMatchingClosingBracket(string json, int openBracketIndex) { int depth = 1; bool inString = false; for (int i = openBracketIndex + 1; i < json.Length; i++) { if (json[i] == '\"' && (i == 0 || json[i - 1] != '\\')) { inString = !inString; continue; } if (inString) continue; if (json[i] == '[') { depth++; } else if (json[i] == ']') { depth--; if (depth == 0) return i; } } return -1; } /// /// 지정된 리소스 경로에서 지정된 유형의 Unity 리소스를 로드합니다. /// /// 이 메서드는 Unity의 메서드를 사용하여 지정된 경로에서 리소스를 검색합니다. 리소스를 찾을 수 없거나 경로가 유효하지 않으면 /// 오류가 기록되고 이 반환됩니다. /// 로드할 리소스의 유형입니다. 에서 파생되어야 합니다. /// Unity 리소스 폴더 내 리소스 경로입니다. null이거나 비어 있을 수 없습니다. /// 유형의 로드된 리소스가 발견되면 해당 리소스를 반환합니다. 그렇지 않으면 . public static T? LoadResource(this string resourcePath) where T : UnityEngine.Object { if (string.IsNullOrEmpty(resourcePath)) { Debug.LogError("리소스 경로가 비어있습니다."); return null; } T resource = Resources.Load(resourcePath); if (resource == null) { Debug.LogError($"리소스를 찾을 수 없습니다: {resourcePath}"); } return resource; } } }