diff --git a/TaxBaik.Application/Services/RevenueTrackingService.cs b/TaxBaik.Application/Services/RevenueTrackingService.cs index be9c66c..528236d 100644 --- a/TaxBaik.Application/Services/RevenueTrackingService.cs +++ b/TaxBaik.Application/Services/RevenueTrackingService.cs @@ -34,6 +34,9 @@ public class RevenueTrackingService(IRevenueTrackingRepository repository) public async Task> GetByClientIdAsync(int clientId, CancellationToken ct = default) => await repository.GetByClientIdAsync(clientId, ct); + public async Task GetByIdAsync(int id, CancellationToken ct = default) => + await repository.GetByIdAsync(id, ct); + public async Task> GetAllAsync(CancellationToken ct = default) => await repository.GetAllAsync(ct); diff --git a/TaxBaik.Domain/Interfaces/IRevenueTrackingRepository.cs b/TaxBaik.Domain/Interfaces/IRevenueTrackingRepository.cs index 498402b..6a1563f 100644 --- a/TaxBaik.Domain/Interfaces/IRevenueTrackingRepository.cs +++ b/TaxBaik.Domain/Interfaces/IRevenueTrackingRepository.cs @@ -5,6 +5,7 @@ using TaxBaik.Domain.Entities; public interface IRevenueTrackingRepository { Task CreateAsync(RevenueTracking revenue, CancellationToken cancellationToken = default); + Task GetByIdAsync(int id, CancellationToken cancellationToken = default); Task> GetAllAsync(CancellationToken cancellationToken = default); Task> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default); Task> GetPendingPaymentsAsync(CancellationToken cancellationToken = default); diff --git a/TaxBaik.Infrastructure/Repositories/RevenueTrackingRepository.cs b/TaxBaik.Infrastructure/Repositories/RevenueTrackingRepository.cs index a6fe61a..806d9c7 100644 --- a/TaxBaik.Infrastructure/Repositories/RevenueTrackingRepository.cs +++ b/TaxBaik.Infrastructure/Repositories/RevenueTrackingRepository.cs @@ -24,6 +24,15 @@ public class RevenueTrackingRepository(IDbConnectionFactory connectionFactory) : FROM revenue_tracking ORDER BY invoice_date DESC"); } + public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) + { + using var conn = Conn(); + return await conn.QueryFirstOrDefaultAsync( + @"SELECT id, client_id, invoice_number, invoice_date, service_type, amount, payment_status, payment_date, due_date, notes, created_at, updated_at + FROM revenue_tracking WHERE id = @Id", + new { Id = id }); + } + public async Task> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default) { using var conn = Conn(); diff --git a/TaxBaik.Web/Components/Admin/App.razor b/TaxBaik.Web/Components/Admin/App.razor index c45f15f..333559a 100644 --- a/TaxBaik.Web/Components/Admin/App.razor +++ b/TaxBaik.Web/Components/Admin/App.razor @@ -9,6 +9,8 @@ + + + diff --git a/TaxBaik.Web/Controllers/RevenueTrackingController.cs b/TaxBaik.Web/Controllers/RevenueTrackingController.cs index d850125..da54eba 100644 --- a/TaxBaik.Web/Controllers/RevenueTrackingController.cs +++ b/TaxBaik.Web/Controllers/RevenueTrackingController.cs @@ -41,11 +41,15 @@ public class RevenueTrackingController(RevenueTrackingService service) : Control [HttpGet("{id:int}")] public async Task GetById(int id) { - return StatusCode(StatusCodes.Status501NotImplemented, new + try { - error = "미구현", - message = "RevenueTrackingService.GetByIdAsync 구현이 필요합니다." - }); + var revenue = await service.GetByIdAsync(id); + return revenue is null ? NotFound(new { error = "조회 실패", message = "해당 청구를 찾을 수 없습니다." }) : Ok(revenue); + } + catch (Exception ex) + { + return StatusCode(500, new { error = "조회 실패", message = ex.Message }); + } } [HttpGet("client/{clientId:int}")] diff --git a/TaxBaik.Web/Program.cs b/TaxBaik.Web/Program.cs index 85844b3..1016372 100644 --- a/TaxBaik.Web/Program.cs +++ b/TaxBaik.Web/Program.cs @@ -342,10 +342,6 @@ app.MapRazorPages(); // AllowAnonymous: JWT 미들웨어가 Blazor 셸 요청을 401로 차단하지 않도록 한다. // 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다. -app.MapRazorComponents() - .AddInteractiveServerRenderMode() - .AllowAnonymous(); - app.MapRazorComponents() .AddInteractiveServerRenderMode() .AllowAnonymous(); diff --git a/TaxBaik.Web/wwwroot/css/admin.css b/TaxBaik.Web/wwwroot/css/admin.css index 1930695..4de1316 100644 --- a/TaxBaik.Web/wwwroot/css/admin.css +++ b/TaxBaik.Web/wwwroot/css/admin.css @@ -578,7 +578,7 @@ textarea:focus-visible { } .admin-content { - padding: 16px; + padding: 12px; max-width: 1400px; margin: 0 auto; width: 100%; @@ -843,11 +843,6 @@ textarea:focus-visible { vertical-align: middle; } -.admin-table .mud-chip { - font-size: 0.68rem; - height: 22px; -} - .admin-table tbody a { color: var(--primary-color); text-decoration: none; @@ -865,16 +860,6 @@ textarea:focus-visible { outline-offset: 2px; } -.admin-table .mud-chip-small { - height: 24px !important; - font-size: var(--font-size-xs) !important; - font-weight: var(--font-weight-medium); - min-width: 60px; - display: inline-flex; - align-items: center; - justify-content: center; -} - /* Loading States */ .admin-skeleton { background: linear-gradient(90deg, var(--bg-overlay) 0%, var(--bg-overlay-strong) 50%, var(--bg-overlay) 100%); @@ -1270,15 +1255,6 @@ textarea:focus-visible { /* Mobile S: <480px */ @media (max-width: 479px) { - .admin-login-page.mud-container-maxwidth-small { - max-width: 100% !important; - padding: var(--space-3); - } - - .admin-login-page .mud-typography--h4 { - font-size: var(--font-size-2xl); - } - .admin-shell { flex-direction: column; height: auto; @@ -1327,11 +1303,6 @@ textarea:focus-visible { overflow-x: auto; } - .admin-nav .mud-nav-link { - min-width: 100px; - font-size: var(--font-size-xs); - } - .admin-drawer-footer { display: none; } @@ -1393,10 +1364,6 @@ textarea:focus-visible { font-size: var(--font-size-base) !important; } - .admin-section-header .mud-button { - width: 100%; - } - .admin-table { font-size: var(--font-size-xs); } @@ -1416,8 +1383,6 @@ textarea:focus-visible { } /* Touch Target Sizing (WCAG 2.5.5) */ - .mud-button, - .mud-icon-button, a, input, select, @@ -1622,10 +1587,6 @@ textarea:focus-visible { color: var(--text-secondary); } -.admin-footer-item .mud-icon { - color: var(--primary-color); -} - /* Responsive Topbar */ @media (max-width: 600px) { .admin-topbar-action { @@ -1636,8 +1597,4 @@ textarea:focus-visible { .admin-topbar-title { min-width: 120px; } - - .mud-toolbar > :last-child { - margin-right: -8px; - } } diff --git a/TaxBaik.Web/wwwroot/css/design-tokens.css b/TaxBaik.Web/wwwroot/css/design-tokens.css new file mode 100644 index 0000000..d7492f0 --- /dev/null +++ b/TaxBaik.Web/wwwroot/css/design-tokens.css @@ -0,0 +1,26 @@ +:root { + --color-primary: #C89D6E; + --color-primary-dark: #A67C52; + --color-secondary: #2E5C4E; + --color-secondary-dark: #1F3A30; + --color-accent: #E8E4D8; + --color-accent-dark: #D9D3C4; + --color-bg: #F9F7F3; + --color-bg-alt: #EFE9DD; + --color-text: #3D2817; + --color-text-light: #6B5D4F; + --color-border: #D9D3C4; + --color-success: #2E7D32; + --color-warning: #F57C00; + --color-danger: #C62828; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --shadow-sm: 0 1px 3px rgba(61, 40, 23, 0.08); + --shadow-md: 0 4px 12px rgba(61, 40, 23, 0.12); + --shadow-lg: 0 8px 24px rgba(61, 40, 23, 0.15); + --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: 300ms cubic-bezier(0.4, 0, 0.2, 1); + --site-font-base: 'Noto Sans KR', 'Apple SD Gothic Neo', sans-serif; +} diff --git a/TaxBaik.Web/wwwroot/css/site.css b/TaxBaik.Web/wwwroot/css/site.css index e2ef124..73601d3 100644 --- a/TaxBaik.Web/wwwroot/css/site.css +++ b/TaxBaik.Web/wwwroot/css/site.css @@ -91,184 +91,6 @@ a:hover { text-decoration: none; } -/* ===== 버튼 ===== */ -.btn { - border-radius: var(--radius-md); - font-weight: 600; - transition: all var(--transition-normal); - cursor: pointer; - border: none; - padding: 0.75rem 2rem; - font-size: 1rem; - letter-spacing: 0.3px; - display: inline-block; - text-align: center; - text-decoration: none; -} - -.btn:active { - transform: scale(0.98); -} - -.btn-primary { - background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%); - color: white; - box-shadow: var(--shadow-md); -} - -.btn-primary:hover { - background: linear-gradient(135deg, var(--color-primary-dark) 0%, #8B5E3C 100%); - box-shadow: var(--shadow-lg); - transform: translateY(-2px); -} - -.btn-warning { - background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%); - color: white; - box-shadow: var(--shadow-md); -} - -.btn-warning:hover { - background: linear-gradient(135deg, var(--color-secondary-dark) 0%, #0D1E1A 100%); - box-shadow: var(--shadow-lg); - transform: translateY(-2px); -} - -.btn-outline-primary { - color: var(--color-primary); - border: 2px solid var(--color-primary); - background: transparent; -} - -.btn-outline-primary:hover { - background-color: var(--color-primary); - color: white; -} - -.btn-lg { - padding: 1rem 2.5rem; - font-size: 1.1rem; -} - -.btn-sm { - padding: 0.5rem 1.25rem; - font-size: 0.95rem; -} - -/* ===== 카드 ===== */ -.card { - border: 1px solid var(--color-border); - border-radius: var(--radius-xl); - transition: all var(--transition-normal); - box-shadow: var(--shadow-sm); - background: white; - overflow: hidden; -} - -.card:hover { - transform: translateY(-6px); - box-shadow: var(--shadow-lg); - border-color: var(--color-primary); -} - -.card-body { - padding: var(--spacing-xl); -} - -.card-title { - font-weight: 700; - color: var(--color-text); - margin-bottom: var(--spacing-md); - font-size: 1.25rem; -} - -.card-text { - color: var(--color-text-light); - line-height: 1.8; -} - -/* ===== 히어로 섹션 ===== */ -.hero-section { - padding: clamp(3rem, 20vh, 6rem) 0; - background: linear-gradient(135deg, var(--color-secondary) 0%, #1F3A30 100%); - color: white; - position: relative; - overflow: hidden; - border-bottom: 4px solid var(--color-primary); -} - -.hero-section::before { - content: ''; - position: absolute; - top: -50%; - right: -10%; - width: 600px; - height: 600px; - background: rgba(200, 157, 110, 0.1); - border-radius: 50%; -} - -.hero-section::after { - content: ''; - position: absolute; - bottom: -30%; - left: -10%; - width: 500px; - height: 500px; - background: rgba(232, 228, 216, 0.05); - border-radius: 50%; -} - -.hero-section h1 { - font-size: clamp(2rem, 8vw, 3.5rem); - font-weight: 800; - margin-bottom: var(--spacing-lg); - position: relative; - z-index: 1; - color: white; -} - -.hero-section p { - font-size: 1.2rem; - margin-bottom: var(--spacing-xl); - position: relative; - z-index: 1; - color: rgba(255, 255, 255, 0.95); - line-height: 1.8; -} - -/* ===== 섹션 ===== */ -.bg-light { - background-color: var(--color-accent) !important; -} - -.bg-primary { - background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%); -} - -.section-title { - font-size: clamp(1.75rem, 5vw, 2.75rem); - font-weight: 800; - color: var(--color-text); - margin-bottom: var(--spacing-xl); - text-align: center; - position: relative; - display: inline-block; - left: 50%; - transform: translateX(-50%); - width: 100%; -} - -.section-title::after { - content: ''; - display: block; - width: 60px; - height: 4px; - background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-secondary) 100%); - margin: var(--spacing-md) auto 0; - border-radius: 2px; -} - /* ===== 휴스 스트립 (신뢰도) ===== */ .trust-strip { background: linear-gradient(135deg, var(--color-bg-alt) 0%, var(--color-accent) 100%); diff --git a/TaxBaik.Web/wwwroot/css/ui-primitives.css b/TaxBaik.Web/wwwroot/css/ui-primitives.css new file mode 100644 index 0000000..52f1bbd --- /dev/null +++ b/TaxBaik.Web/wwwroot/css/ui-primitives.css @@ -0,0 +1,111 @@ +/* Shared UI primitives for site and admin */ + +.site-button, +.admin-icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 44px; + text-decoration: none; + font-weight: 700; + border-radius: 12px; + transition: all var(--transition-normal); +} + +.site-button { + padding: 0.9rem 1.4rem; +} + +.site-button.primary { + background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%); + color: white; +} + +.site-button.secondary { + border: 1px solid var(--color-primary); + color: var(--color-primary-dark); + background: rgba(255,255,255,0.7); +} + +.admin-surface, +.site-post-card, +.taxbaik-skeleton-item { + background: rgba(255,255,255,0.82); + border: 1px solid rgba(0,0,0,0.08); + border-radius: 16px; + box-shadow: 0 12px 30px rgba(61,40,23,0.08); +} + +.admin-page-hero, +.site-hero { + border-bottom: 1px solid rgba(61, 40, 23, 0.08); +} + +.admin-table, +.admin-kv-grid, +.admin-dialog-card, +.admin-pagination, +.admin-tabbar { + width: 100%; +} + +.admin-table { + border-collapse: collapse; +} + +.admin-table th, +.admin-table td { + padding: 0.85rem 0.75rem; + border-bottom: 1px solid rgba(61, 40, 23, 0.08); +} + +.status-pill { + display: inline-flex; + align-items: center; + padding: 0.3rem 0.65rem; + border-radius: 999px; + font-size: 0.82rem; + font-weight: 700; + background: rgba(61, 40, 23, 0.08); +} + +.admin-input { + width: 100%; + min-height: 44px; + padding: 0.75rem 0.9rem; + border-radius: 12px; + border: 1px solid rgba(61, 40, 23, 0.16); + background: #fff; + color: var(--color-text); +} + +.admin-icon-button { + min-width: 40px; + min-height: 40px; + border: 1px solid rgba(61, 40, 23, 0.14); + background: rgba(255,255,255,0.8); + color: var(--color-text); +} + +.admin-icon-button.danger { + color: var(--color-danger); +} + +.admin-page-hero { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 1rem; + margin-bottom: 1rem; + padding-bottom: 1rem; +} + +.admin-page-title { + margin: 0; + font-size: 1.45rem; +} + +.admin-page-subtitle, +.muted { + color: var(--color-text-light); +}