Skip to content

Commit 1f408ee

Browse files
authored
Refactor: Remove Spectrum Web Components and integrate Fluent UI (#24)
1 parent a8f3745 commit 1f408ee

25 files changed

Lines changed: 842 additions & 1184 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,4 +372,5 @@ appsettings.json
372372
.idea/
373373
.DS_Store
374374
**/*.js
375-
**/*.wasm
375+
**/*.wasm
376+
nul

src/DotNetDevLottery/.npmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
public-hoist-pattern[]=@spectrum-web-components/theme
1+
public-hoist-pattern[]=@spectrum-web-components/*

src/DotNetDevLottery/App.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<ui-theme scale="large" color="light">
1+
<FluentProvider>
22
<Router AppAssembly="@typeof(App).Assembly">
33
<Found Context="routeData">
44
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
@@ -11,4 +11,4 @@
1111
</LayoutView>
1212
</NotFound>
1313
</Router>
14-
</ui-theme>
14+
</FluentProvider>
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
@inject Blazored.LocalStorage.ILocalStorageService LocalStorage
2+
@inject IJSRuntime JSRuntime
3+
@using DotNetDevLottery.Models
4+
@using Microsoft.AspNetCore.Components.Forms
5+
6+
<FluentDialog @bind-Hidden="@isHidden" Modal="true" TrapFocus="true" @ondialogdismiss="OnDialogDismiss">
7+
<DialogHeader>
8+
<FluentStack Orientation="Orientation.Horizontal">
9+
<FluentLabel Typo="Typography.PaneHeader">볼 디자인 설정</FluentLabel>
10+
</FluentStack>
11+
</DialogHeader>
12+
<DialogBody>
13+
<FluentStack Orientation="Orientation.Vertical" Style="gap: 1.5rem;">
14+
<FluentStack Orientation="Orientation.Vertical" Style="gap: 0.5rem;">
15+
<FluentLabel Style="font-weight: 600;">일반 볼 이미지 업로드</FluentLabel>
16+
<InputFile OnChange="@(e => OnBallImageSelected(e, false))" accept="image/*" class="file-input" />
17+
@if (!string.IsNullOrEmpty(settings.BallImageUrl))
18+
{
19+
<FluentStack Orientation="Orientation.Horizontal" Style="align-items: center; gap: 1rem; padding: 0.5rem; background: var(--neutral-layer-2); border-radius: 4px;">
20+
<img src="@settings.BallImageUrl" alt="볼 이미지 미리보기" style="width: 60px; height: 60px; object-fit: contain; border-radius: 4px;" />
21+
<FluentButton Appearance="Appearance.Lightweight" OnClick="@(() => ClearImage(false))">삭제</FluentButton>
22+
</FluentStack>
23+
}
24+
</FluentStack>
25+
26+
<FluentStack Orientation="Orientation.Vertical" Style="gap: 0.5rem;">
27+
<FluentLabel Style="font-weight: 600;">당첨 볼 이미지 업로드</FluentLabel>
28+
<InputFile OnChange="@(e => OnBallImageSelected(e, true))" accept="image/*" class="file-input" />
29+
@if (!string.IsNullOrEmpty(settings.DrawedBallImageUrl))
30+
{
31+
<FluentStack Orientation="Orientation.Horizontal" Style="align-items: center; gap: 1rem; padding: 0.5rem; background: var(--neutral-layer-2); border-radius: 4px;">
32+
<img src="@settings.DrawedBallImageUrl" alt="당첨 볼 이미지 미리보기" style="width: 60px; height: 60px; object-fit: contain; border-radius: 4px;" />
33+
<FluentButton Appearance="Appearance.Lightweight" OnClick="@(() => ClearImage(true))">삭제</FluentButton>
34+
</FluentStack>
35+
}
36+
</FluentStack>
37+
38+
<FluentDivider Style="width: 100%;" />
39+
<FluentLabel Style="text-align: center; color: var(--neutral-foreground-hint);">또는 색상 사용</FluentLabel>
40+
41+
<FluentStack Orientation="Orientation.Vertical" Style="gap: 0.5rem;">
42+
<FluentLabel Style="font-weight: 600;">일반 볼 색상</FluentLabel>
43+
<input type="color" @bind="@settings.BallColor" style="width: 100%; height: 40px; border: 1px solid var(--neutral-stroke-rest); border-radius: 4px; cursor: pointer;" />
44+
</FluentStack>
45+
46+
<FluentStack Orientation="Orientation.Vertical" Style="gap: 0.5rem;">
47+
<FluentLabel Style="font-weight: 600;">일반 볼 테두리 색상</FluentLabel>
48+
<input type="color" @bind="@settings.BallBorderColor" style="width: 100%; height: 40px; border: 1px solid var(--neutral-stroke-rest); border-radius: 4px; cursor: pointer;" />
49+
</FluentStack>
50+
51+
<FluentStack Orientation="Orientation.Vertical" Style="gap: 0.5rem;">
52+
<FluentLabel Style="font-weight: 600;">당첨 볼 색상</FluentLabel>
53+
<input type="color" @bind="@settings.DrawedBallColor" style="width: 100%; height: 40px; border: 1px solid var(--neutral-stroke-rest); border-radius: 4px; cursor: pointer;" />
54+
</FluentStack>
55+
56+
<FluentStack Orientation="Orientation.Vertical" Style="gap: 0.5rem;">
57+
<FluentLabel Style="font-weight: 600;">당첨 볼 텍스트 색상</FluentLabel>
58+
<input type="color" @bind="@settings.DrawedBallTextColor" style="width: 100%; height: 40px; border: 1px solid var(--neutral-stroke-rest); border-radius: 4px; cursor: pointer;" />
59+
</FluentStack>
60+
</FluentStack>
61+
</DialogBody>
62+
<DialogFooter>
63+
<FluentButton Appearance="Appearance.Neutral" OnClick="OnCancel">취소</FluentButton>
64+
<FluentButton Appearance="Appearance.Accent" OnClick="OnSave">저장</FluentButton>
65+
</DialogFooter>
66+
</FluentDialog>
67+
68+
@code {
69+
private const string SETTINGS_KEY = "ball-design-settings";
70+
private const long MAX_FILE_SIZE = 1024 * 1024 * 5; // 5MB
71+
private BallDesignSettings settings = new();
72+
private bool isHidden = true;
73+
74+
[Parameter]
75+
public EventCallback OnSettingsSaved { get; set; }
76+
77+
protected override async Task OnInitializedAsync()
78+
{
79+
await LoadSettings();
80+
}
81+
82+
public void Show()
83+
{
84+
isHidden = false;
85+
StateHasChanged();
86+
}
87+
88+
public void Hide()
89+
{
90+
isHidden = true;
91+
StateHasChanged();
92+
}
93+
94+
private async Task LoadSettings()
95+
{
96+
var savedSettings = await LocalStorage.GetItemAsync<BallDesignSettings>(SETTINGS_KEY);
97+
if (savedSettings != null)
98+
{
99+
settings = savedSettings;
100+
}
101+
}
102+
103+
private async Task OnBallImageSelected(InputFileChangeEventArgs e, bool isDrawedBall)
104+
{
105+
var file = e.File;
106+
if (file == null) return;
107+
108+
if (file.Size > MAX_FILE_SIZE)
109+
{
110+
Console.Error.WriteLine($"File size exceeds {MAX_FILE_SIZE / 1024 / 1024}MB limit");
111+
return;
112+
}
113+
114+
try
115+
{
116+
using var stream = file.OpenReadStream(MAX_FILE_SIZE);
117+
using var memoryStream = new MemoryStream();
118+
await stream.CopyToAsync(memoryStream);
119+
var bytes = memoryStream.ToArray();
120+
var base64 = Convert.ToBase64String(bytes);
121+
var dataUrl = $"data:{file.ContentType};base64,{base64}";
122+
123+
if (isDrawedBall)
124+
{
125+
settings.DrawedBallImageUrl = dataUrl;
126+
}
127+
else
128+
{
129+
settings.BallImageUrl = dataUrl;
130+
}
131+
}
132+
catch (Exception ex)
133+
{
134+
Console.Error.WriteLine($"Error reading file: {ex.Message}");
135+
}
136+
}
137+
138+
private void ClearImage(bool isDrawedBall)
139+
{
140+
if (isDrawedBall)
141+
{
142+
settings.DrawedBallImageUrl = null;
143+
}
144+
else
145+
{
146+
settings.BallImageUrl = null;
147+
}
148+
}
149+
150+
private async Task OnSave()
151+
{
152+
try
153+
{
154+
await LocalStorage.SetItemAsync(SETTINGS_KEY, settings);
155+
Console.WriteLine("Settings saved to localStorage");
156+
isHidden = true;
157+
await OnSettingsSaved.InvokeAsync();
158+
}
159+
catch (Exception ex)
160+
{
161+
Console.Error.WriteLine($"Error saving settings: {ex.Message}");
162+
}
163+
}
164+
165+
private void OnCancel()
166+
{
167+
isHidden = true;
168+
}
169+
170+
private async Task OnDialogDismiss()
171+
{
172+
await LoadSettings(); // Reload settings if dialog was dismissed without saving
173+
}
174+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
::deep sp-overlay {
2+
display: flex !important;
3+
justify-content: center;
4+
align-items: center;
5+
}
6+
7+
::deep sp-popover {
8+
margin: auto;
9+
}
10+
11+
.settings-content {
12+
display: flex;
13+
flex-direction: column;
14+
gap: 1rem;
15+
padding: 1rem 0;
16+
}
17+
18+
.setting-row {
19+
display: flex;
20+
flex-direction: column;
21+
gap: 0.5rem;
22+
}
23+
24+
.setting-row label {
25+
font-size: 0.9rem;
26+
font-weight: 500;
27+
color: #333;
28+
}
29+
30+
.setting-row sp-textfield {
31+
width: 100%;
32+
}
33+
34+
.setting-row input[type="color"] {
35+
width: 100%;
36+
height: 40px;
37+
border: 1px solid #ccc;
38+
border-radius: 4px;
39+
cursor: pointer;
40+
}
41+
42+
.file-input {
43+
padding: 0.5rem;
44+
border: 2px dashed #ccc;
45+
border-radius: 4px;
46+
cursor: pointer;
47+
transition: border-color 0.3s;
48+
}
49+
50+
.file-input:hover {
51+
border-color: #667eea;
52+
}
53+
54+
.image-preview {
55+
display: flex;
56+
align-items: center;
57+
gap: 0.5rem;
58+
padding: 0.5rem;
59+
background-color: #f5f5f5;
60+
border-radius: 4px;
61+
}
62+
63+
.image-preview img {
64+
width: 60px;
65+
height: 60px;
66+
object-fit: cover;
67+
border-radius: 4px;
68+
border: 1px solid #ddd;
69+
}
70+
71+
.divider-section {
72+
display: flex;
73+
align-items: center;
74+
gap: 1rem;
75+
margin: 0.5rem 0;
76+
}
77+
78+
.divider-section sp-divider {
79+
flex: 1;
80+
}
81+
82+
.divider-text {
83+
font-size: 0.85rem;
84+
color: #666;
85+
white-space: nowrap;
86+
}

0 commit comments

Comments
 (0)