diff --git a/src/AxCopilot/Services/WeatherWidgetService.cs b/src/AxCopilot/Services/WeatherWidgetService.cs
new file mode 100644
index 0000000..3de0d35
--- /dev/null
+++ b/src/AxCopilot/Services/WeatherWidgetService.cs
@@ -0,0 +1,63 @@
+using System.Net.Http;
+
+namespace AxCopilot.Services;
+
+///
+/// 날씨 위젯용 데이터 서비스.
+/// 사외 모드에서만 외부 API(wttr.in)를 호출하며 30분간 결과를 캐시합니다.
+/// 사내 모드(InternalModeEnabled=true)에서는 "--"를 즉시 반환합니다.
+///
+internal static class WeatherWidgetService
+{
+ private static string? _cached;
+ private static DateTime _cacheTime = DateTime.MinValue;
+ private static bool _fetching;
+ private static readonly TimeSpan _ttl = TimeSpan.FromMinutes(30);
+
+ /// 현재 캐시된 날씨 텍스트 (없으면 "--")
+ public static string CachedText => _cached ?? "--";
+
+ ///
+ /// 날씨 정보를 비동기로 갱신합니다.
+ /// 사내 모드이거나 캐시 유효 기간 내이면 즉시 반환합니다.
+ ///
+ public static async Task RefreshAsync(bool internalMode)
+ {
+ if (internalMode)
+ {
+ _cached = "--";
+ return;
+ }
+
+ // 캐시 유효하면 재호출 불필요
+ if (_cached != null && DateTime.Now - _cacheTime < _ttl) return;
+ if (_fetching) return;
+
+ _fetching = true;
+ try
+ {
+ using var client = new HttpClient();
+ client.Timeout = TimeSpan.FromSeconds(6);
+ // wttr.in: %c = 날씨 조건 이모지, %t = 기온
+ var raw = await client.GetStringAsync("https://wttr.in/?format=%c+%t");
+ _cached = raw.Trim().Replace("+", " ");
+ _cacheTime = DateTime.Now;
+ }
+ catch (Exception ex)
+ {
+ LogService.Warn($"날씨 위젯 갱신 실패: {ex.Message}");
+ _cached ??= "--";
+ }
+ finally
+ {
+ _fetching = false;
+ }
+ }
+
+ /// 캐시를 강제 초기화하여 다음 호출 시 재조회합니다.
+ public static void Invalidate()
+ {
+ _cached = null;
+ _cacheTime = DateTime.MinValue;
+ }
+}
diff --git a/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs b/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs
index 75e610a..befc388 100644
--- a/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs
+++ b/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs
@@ -57,6 +57,51 @@ public partial class LauncherViewModel
public bool Widget_McpOnline => ServerStatusService.Instance.McpOnline;
public string Widget_McpName => ServerStatusService.Instance.McpName;
+ // ─── 날씨 ─────────────────────────────────────────────────────────────────
+
+ private string _widget_WeatherText = "--";
+
+ /// 날씨 위젯 텍스트 예: "⛅ 18°C" (사외 모드) / "--" (사내 모드)
+ public string Widget_WeatherText
+ {
+ get => _widget_WeatherText;
+ internal set { _widget_WeatherText = value; OnPropertyChanged(); }
+ }
+
+ // ─── 날짜/일정 ────────────────────────────────────────────────────────────
+
+ /// 날짜 위젯 텍스트 예: "4/4 (화)"
+ public string Widget_CalText =>
+ DateTime.Now.ToString("M/d (ddd)",
+ System.Globalization.CultureInfo.GetCultureInfo("ko-KR"));
+
+ // ─── 배터리 ───────────────────────────────────────────────────────────────
+
+ private string _widget_BatteryText = "--";
+ private string _widget_BatteryIcon = "\uE83F";
+ private bool _widget_BatteryVisible = false;
+
+ /// 배터리 퍼센트 및 충전 표시. 예: "87%" / "45% ⚡"
+ public string Widget_BatteryText
+ {
+ get => _widget_BatteryText;
+ internal set { _widget_BatteryText = value; OnPropertyChanged(); }
+ }
+
+ /// 배터리 잔량별 Segoe MDL2 아이콘 코드포인트
+ public string Widget_BatteryIcon
+ {
+ get => _widget_BatteryIcon;
+ internal set { _widget_BatteryIcon = value; OnPropertyChanged(); }
+ }
+
+ /// 배터리 위젯 표시 여부 (데스크톱/배터리 없음이면 false)
+ public bool Widget_BatteryVisible
+ {
+ get => _widget_BatteryVisible;
+ internal set { _widget_BatteryVisible = value; OnPropertyChanged(); }
+ }
+
// ─── 갱신 메서드 ──────────────────────────────────────────────────────────
/// 1초마다 LauncherWindow.Widgets.cs에서 호출 — UI 바인딩 갱신.
@@ -75,6 +120,9 @@ public partial class LauncherViewModel
OnPropertyChanged(nameof(Widget_LlmOnline));
OnPropertyChanged(nameof(Widget_McpOnline));
OnPropertyChanged(nameof(Widget_McpName));
+ OnPropertyChanged(nameof(Widget_CalText));
+ // WeatherText / BatteryText / BatteryIcon / BatteryVisible 는
+ // LauncherWindow.Widgets.cs에서 직접 setter 호출로 갱신
}
private int _widgetRefreshTick;
diff --git a/src/AxCopilot/Views/LauncherWindow.Widgets.cs b/src/AxCopilot/Views/LauncherWindow.Widgets.cs
index 9d75e30..52d01c5 100644
--- a/src/AxCopilot/Views/LauncherWindow.Widgets.cs
+++ b/src/AxCopilot/Views/LauncherWindow.Widgets.cs
@@ -45,6 +45,9 @@ public partial class LauncherWindow
// 메모 건수 즉시 갱신 (최초 1회)
_vm.UpdateWidgets();
UpdateServerDots();
+ UpdateBatteryWidget();
+ // 날씨: 사외 모드에서만 비동기 갱신
+ _ = RefreshWeatherAsync();
// 1초 타이머
if (_widgetTimer == null)
@@ -57,6 +60,14 @@ public partial class LauncherWindow
{
_vm.UpdateWidgets();
UpdateServerDots();
+
+ // 배터리: 30초마다 갱신
+ if (_vm.Widget_PerfText.Length > 0 && _widgetBatteryTick++ % 30 == 0)
+ UpdateBatteryWidget();
+
+ // 날씨: 2분마다 서비스 쪽 캐시를 체크 (실제 API 호출은 30분마다)
+ if (_widgetWeatherTick++ % 120 == 0)
+ _ = RefreshWeatherAsync();
};
}
_widgetTimer.Start();
@@ -65,6 +76,9 @@ public partial class LauncherWindow
UpdatePomoWidgetStyle();
}
+ private int _widgetBatteryTick;
+ private int _widgetWeatherTick;
+
/// 런처가 숨겨질 때 타이머 중지 (뽀모도로는 계속 실행).
internal void StopWidgetUpdates()
{
@@ -140,4 +154,112 @@ public partial class LauncherWindow
_vm.InputText = "port";
InputBox?.Focus();
}
+
+ private void WgtWeather_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ var internalMode = CurrentApp?.SettingsService?.Settings.InternalModeEnabled ?? true;
+ if (!internalMode)
+ {
+ // 사외 모드: 브라우저에서 날씨 페이지 열기
+ try
+ {
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(
+ "https://wttr.in") { UseShellExecute = true });
+ }
+ catch (Exception ex)
+ {
+ LogService.Warn($"날씨 링크 열기 실패: {ex.Message}");
+ }
+ }
+ else
+ {
+ // 사내 모드: 날씨 캐시 무효화 후 재시도 (사외 모드 전환 시 즉시 갱신되도록)
+ WeatherWidgetService.Invalidate();
+ _ = RefreshWeatherAsync();
+ }
+ }
+
+ private void WgtCal_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ // Windows 달력 앱 열기
+ try
+ {
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(
+ "ms-clock:") { UseShellExecute = true });
+ }
+ catch
+ {
+ try
+ {
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(
+ "outlookcal:") { UseShellExecute = true });
+ }
+ catch (Exception ex)
+ {
+ LogService.Warn($"달력 앱 열기 실패: {ex.Message}");
+ }
+ }
+ }
+
+ private void WgtBattery_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ // 전원 및 절전 설정 열기
+ try
+ {
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(
+ "ms-settings:powersleep") { UseShellExecute = true });
+ }
+ catch (Exception ex)
+ {
+ LogService.Warn($"전원 설정 열기 실패: {ex.Message}");
+ }
+ }
+
+ // ─── 배터리 상태 갱신 ──────────────────────────────────────────────────────
+
+ private void UpdateBatteryWidget()
+ {
+ try
+ {
+ var ps = System.Windows.Forms.SystemInformation.PowerStatus;
+ var pct = ps.BatteryLifePercent; // 0.0~1.0, unknown = 255f
+
+ if (pct > 1.0f || pct < 0f)
+ {
+ // 배터리 없는 장치 (데스크톱 등) → 위젯 숨김
+ _vm.Widget_BatteryVisible = false;
+ return;
+ }
+
+ _vm.Widget_BatteryVisible = true;
+ var pctInt = (int)(pct * 100);
+ var charging = ps.PowerLineStatus == System.Windows.Forms.PowerLineStatus.Online;
+
+ _vm.Widget_BatteryText = charging ? $"{pctInt}% ⚡" : $"{pctInt}%";
+ _vm.Widget_BatteryIcon = charging ? "\uE83E" // BatteryCharging
+ : pctInt >= 85 ? "\uEBA7" // Battery 8 (Full)
+ : pctInt >= 70 ? "\uEBA5" // Battery 6
+ : pctInt >= 50 ? "\uEBA3" // Battery 4
+ : pctInt >= 25 ? "\uEBA1" // Battery 2
+ : "\uEBA0"; // Battery 1 (Low)
+ }
+ catch (Exception ex)
+ {
+ LogService.Warn($"배터리 위젯 갱신 실패: {ex.Message}");
+ _vm.Widget_BatteryVisible = false;
+ }
+ }
+
+ // ─── 날씨 비동기 갱신 ─────────────────────────────────────────────────────
+
+ private async Task RefreshWeatherAsync()
+ {
+ var internalMode = CurrentApp?.SettingsService?.Settings.InternalModeEnabled ?? true;
+ await WeatherWidgetService.RefreshAsync(internalMode);
+ // UI 스레드에서 바인딩 프로퍼티 갱신
+ await Dispatcher.InvokeAsync(() =>
+ {
+ _vm.Widget_WeatherText = WeatherWidgetService.CachedText;
+ });
+ }
}
diff --git a/src/AxCopilot/Views/LauncherWindow.xaml b/src/AxCopilot/Views/LauncherWindow.xaml
index 1b3ea24..69a5953 100644
--- a/src/AxCopilot/Views/LauncherWindow.xaml
+++ b/src/AxCopilot/Views/LauncherWindow.xaml
@@ -790,116 +790,189 @@
Margin="0,0,0,8"
Opacity="0.7"/>
-
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+