diff --git a/app/[locale]/(dashboard)/layout.tsx b/app/[locale]/(dashboard)/layout.tsx index 72f7331..5ee9638 100644 --- a/app/[locale]/(dashboard)/layout.tsx +++ b/app/[locale]/(dashboard)/layout.tsx @@ -1,3 +1,5 @@ +export const dynamic = 'force-dynamic' + import AuthGuard from '@/components/layout/auth-guard' import { Sidebar } from '@/components/layout/sidebar' import { MobileNav } from '@/components/layout/mobile-nav' diff --git a/app/[locale]/(dashboard)/projects/[id]/project-detail-client.tsx b/app/[locale]/(dashboard)/projects/[id]/project-detail-client.tsx index 544ac8e..0c3a269 100644 --- a/app/[locale]/(dashboard)/projects/[id]/project-detail-client.tsx +++ b/app/[locale]/(dashboard)/projects/[id]/project-detail-client.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import { Download, RefreshCw, Loader2, Copy } from 'lucide-react' import { toast } from 'sonner' -import { Link, useRouter } from '@/i18n/navigation' +import { useRouter } from '@/i18n/navigation' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card, CardContent } from '@/components/ui/card' diff --git a/app/[locale]/(dashboard)/projects/actions.ts b/app/[locale]/(dashboard)/projects/actions.ts index a7f5de4..7ea7a3e 100644 --- a/app/[locale]/(dashboard)/projects/actions.ts +++ b/app/[locale]/(dashboard)/projects/actions.ts @@ -1,6 +1,5 @@ 'use server' -import { redirect } from 'next/navigation' import { createClient } from '@/lib/supabase/server' import type { Project } from '@/types/database' diff --git a/app/[locale]/(dashboard)/projects/page.tsx b/app/[locale]/(dashboard)/projects/page.tsx index 548a085..094f480 100644 --- a/app/[locale]/(dashboard)/projects/page.tsx +++ b/app/[locale]/(dashboard)/projects/page.tsx @@ -3,7 +3,7 @@ import { createClient } from '@/lib/supabase/server' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card, CardContent } from '@/components/ui/card' -import { Plus, FolderOpen, ArrowRight, Sparkles } from 'lucide-react' +import { Plus, ArrowRight, Sparkles } from 'lucide-react' import type { ProjectStatus } from '@/types/database' const STATUS_CONFIG: Record = { diff --git a/app/api/ai/icons/route.ts b/app/api/ai/icons/route.ts index 50cbb33..19495fb 100644 --- a/app/api/ai/icons/route.ts +++ b/app/api/ai/icons/route.ts @@ -37,6 +37,7 @@ export async function POST(request: Request) { const images = await generateIcon({ description: body.description, brandProfile: body.brandProfile, + styleModifier: body.styleModifier, }) await incrementUsage(admin, user.id, 'ai_icons_used_this_month') diff --git a/app/api/email/route.ts b/app/api/email/route.ts index b516054..f956f0c 100644 --- a/app/api/email/route.ts +++ b/app/api/email/route.ts @@ -5,7 +5,7 @@ * This route is protected by an internal API key */ -import { NextRequest, NextResponse } from 'next/server' +import { type NextRequest, NextResponse } from 'next/server' import { sendEmail } from '@/lib/email/client' import { WelcomeEmail, ProjectCompleteEmail } from '@/lib/email/templates' diff --git a/app/api/feedback/route.ts b/app/api/feedback/route.ts index 4d32ae5..43758fd 100644 --- a/app/api/feedback/route.ts +++ b/app/api/feedback/route.ts @@ -3,7 +3,7 @@ * Handles submission of general feedback and NPS responses */ -import { NextRequest, NextResponse } from 'next/server' +import { type NextRequest, NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' interface FeedbackBody { diff --git a/app/api/notifications/route.ts b/app/api/notifications/route.ts index d65347c..4621fd4 100644 --- a/app/api/notifications/route.ts +++ b/app/api/notifications/route.ts @@ -3,7 +3,7 @@ * Handles fetching and updating notifications for authenticated users */ -import { NextRequest, NextResponse } from 'next/server' +import { type NextRequest, NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' import { getNotifications, diff --git a/app/globals.css b/app/globals.css index 77bfb8f..184edf1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -66,8 +66,12 @@ body { /* -- Dark Mode -- */ [data-theme="dark"] { - --color-brand: #fafafa; - --color-brand-foreground: #0a0a0b; + --color-brand: #111114; + --color-brand-foreground: #fafafa; + + --color-accent: #818cf8; + --color-accent-light: #a5b4fc; + --color-accent-dark: #6366f1; --color-surface: #0a0a0b; --color-surface-secondary: #18181b; @@ -84,14 +88,18 @@ body { --shadow-md: 0 2px 4px rgb(0 0 0 / 0.3), 0 4px 12px rgb(0 0 0 / 0.4); --shadow-lg: 0 4px 6px rgb(0 0 0 / 0.3), 0 8px 24px rgb(0 0 0 / 0.4); --shadow-xl: 0 8px 16px rgb(0 0 0 / 0.4), 0 16px 48px rgb(0 0 0 / 0.5); - --shadow-glow: 0 0 20px rgb(99 102 241 / 0.25), 0 0 60px rgb(99 102 241 / 0.15); + --shadow-glow: 0 0 20px rgb(129 140 248 / 0.3), 0 0 60px rgb(129 140 248 / 0.15); } /* 시스템 설정 기반 자동 다크모드 (선택적) */ @media (prefers-color-scheme: dark) { html:not([data-theme="light"]) { - --color-brand: #fafafa; - --color-brand-foreground: #0a0a0b; + --color-brand: #111114; + --color-brand-foreground: #fafafa; + + --color-accent: #818cf8; + --color-accent-light: #a5b4fc; + --color-accent-dark: #6366f1; --color-surface: #0a0a0b; --color-surface-secondary: #18181b; @@ -205,7 +213,7 @@ body { /* -- Gradient Text -- */ .text-gradient { - background: linear-gradient(135deg, var(--color-accent) 0%, #a78bfa 50%, var(--color-accent-light) 100%); + background: linear-gradient(135deg, var(--color-accent) 0%, #c4b5fd 50%, var(--color-accent-light) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; diff --git a/components/feedback/feedback-widget.tsx b/components/feedback/feedback-widget.tsx index 0ab759f..e242fad 100644 --- a/components/feedback/feedback-widget.tsx +++ b/components/feedback/feedback-widget.tsx @@ -49,6 +49,17 @@ export function FeedbackWidget({ const t = content[locale] + const handleClose = useCallback(() => { + setIsAnimating(false) + setTimeout(() => { + setIsOpen(false) + setSentiment(null) + setMessage('') + // Return focus to trigger button + triggerRef.current?.focus() + }, 300) + }, []) + // Focus trap handler const handleKeyDown = useCallback( (e: KeyboardEvent) => { @@ -82,7 +93,7 @@ export function FeedbackWidget({ } } }, - [isOpen] + [isOpen, handleClose] ) // Add/remove keyboard listener @@ -104,17 +115,6 @@ export function FeedbackWidget({ }) } - const handleClose = useCallback(() => { - setIsAnimating(false) - setTimeout(() => { - setIsOpen(false) - setSentiment(null) - setMessage('') - // Return focus to trigger button - triggerRef.current?.focus() - }, 300) - }, []) - const handleSubmit = () => { if (!sentiment || !message.trim()) return diff --git a/components/feedback/nps-modal.tsx b/components/feedback/nps-modal.tsx index fb63828..a0173e8 100644 --- a/components/feedback/nps-modal.tsx +++ b/components/feedback/nps-modal.tsx @@ -58,6 +58,16 @@ export function NPSModal({ const t = content[locale] + const handleDismiss = useCallback(() => { + setIsAnimating(false) + setTimeout(() => { + setIsOpen(false) + // Restore focus to trigger element + triggerRef.current?.focus() + }, 300) + onDismiss?.() + }, [onDismiss]) + // Focus trap handler const handleKeyDown = useCallback( (e: KeyboardEvent) => { @@ -91,7 +101,7 @@ export function NPSModal({ } } }, - [isOpen] + [isOpen, handleDismiss] ) // Add/remove keyboard listener @@ -147,16 +157,6 @@ export function NPSModal({ }, 2000) } - const handleDismiss = useCallback(() => { - setIsAnimating(false) - setTimeout(() => { - setIsOpen(false) - // Restore focus to trigger element - triggerRef.current?.focus() - }, 300) - onDismiss?.() - }, [onDismiss]) - const getScoreColor = (score: number, isSelected: boolean) => { if (!isSelected) return 'bg-white/5 border-white/10 text-white/40 hover:bg-white/10 hover:text-white/60' diff --git a/components/ui/cookie-consent.tsx b/components/ui/cookie-consent.tsx index a51a4bf..e16620e 100644 --- a/components/ui/cookie-consent.tsx +++ b/components/ui/cookie-consent.tsx @@ -1,7 +1,7 @@ 'use client' import { useState, useEffect } from 'react' -import { Cookie, X, Shield, ChevronRight } from 'lucide-react' +import { Cookie, Shield, ChevronRight } from 'lucide-react' import { Link } from '@/i18n/navigation' interface CookieConsentProps { diff --git a/components/wizard/icon-ai-tab.tsx b/components/wizard/icon-ai-tab.tsx index d3b4c8d..c8943ce 100644 --- a/components/wizard/icon-ai-tab.tsx +++ b/components/wizard/icon-ai-tab.tsx @@ -1,7 +1,7 @@ 'use client' -import { useState } from 'react' -import { Sparkles } from 'lucide-react' +import { useMemo, useState } from 'react' +import { Sparkles, Wand2 } from 'lucide-react' import Image from 'next/image' import { useWizardStore } from '@/store/wizard-store' import { Input } from '@/components/ui/input' @@ -10,12 +10,13 @@ import { Button } from '@/components/ui/button' import { PlanGate } from '@/components/shared/plan-gate' import { UsageMeter } from '@/components/shared/usage-meter' import { cn } from '@/lib/utils/cn' -import type { BrandProfile, Plan, User } from '@/types/database' +import type { BrandProfile, Plan, StylePreset, User } from '@/types/database' interface IconAiTabProps { plan: Plan user: User brandProfile: BrandProfile | null + stylePreset: StylePreset | null } interface GeneratedImage { @@ -23,14 +24,62 @@ interface GeneratedImage { seed: number } -export function IconAiTab({ plan, user, brandProfile }: IconAiTabProps) { - const { icon, setIcon } = useWizardStore() +function buildSuggestions( + projectName: string, + projectDescription: string, + platform: string, + brandProfile: BrandProfile | null, + stylePreset: StylePreset | null, +): string[] { + const suggestions: string[] = [] + + if (stylePreset && projectName) { + suggestions.push(`${stylePreset.name}-style icon for ${projectName}`) + } + + if (brandProfile?.keywords?.length) { + const keyword = brandProfile.keywords[0] + const iconStyle = stylePreset?.icon_style || brandProfile.icon_style + suggestions.push(`${keyword} themed ${iconStyle ? iconStyle + ' ' : ''}icon`) + } + + if (projectName && projectDescription) { + suggestions.push( + `modern ${platform} app icon for ${projectName}, ${projectDescription}`, + ) + } + + if (projectName && !stylePreset) { + suggestions.push(`minimalist icon for ${projectName}`) + } + + if (suggestions.length === 0) { + suggestions.push('minimalist app icon, clean and modern') + } + + return suggestions +} + +export function IconAiTab({ plan, user, brandProfile, stylePreset }: IconAiTabProps) { + const { icon, setIcon, project, platform } = useWizardStore() const [description, setDescription] = useState('') const [isGenerating, setIsGenerating] = useState(false) const [images, setImages] = useState([]) const [error, setError] = useState(null) const [iconsUsed, setIconsUsed] = useState(user.ai_icons_used_this_month) + const suggestions = useMemo( + () => + buildSuggestions( + project.name, + project.description, + platform.platform, + brandProfile, + stylePreset, + ), + [project.name, project.description, platform.platform, brandProfile, stylePreset], + ) + const iconsLimit = plan === 'pro' ? Infinity : 5 const isExhausted = iconsUsed >= iconsLimit @@ -54,6 +103,7 @@ export function IconAiTab({ plan, user, brandProfile }: IconAiTabProps) { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description, + styleModifier: stylePreset?.ai_style_modifier ?? undefined, brandProfile: brandProfile ? { styleDirection: brandProfile.style_direction, primaryColor: brandProfile.primary_color, @@ -107,6 +157,29 @@ export function IconAiTab({ plan, user, brandProfile }: IconAiTabProps) { onChange={(e) => setDescription(e.target.value)} placeholder="e.g., minimalist checkmark icon for task app" /> + {suggestions.length > 0 && ( +
+ + + Suggested: + + {suggestions.map((s, i) => ( + + ))} +
+ )}