mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2026-06-13 10:19:11 +00:00
FSD 아키텍처로 프로젝트 구조 리팩토링
주요 변경사항: - FSD(Feature-Sliced Design) 아키텍처 도입 - shared 레이어 구조 생성 (ui, lib, api, model, types, config) - 공통 UI 컴포넌트를 shared/ui로 이동 (Header, BottomNavigation, Loading, Toast) - 라이브러리 코드를 shared/lib로 이동 (theme-provider, react-query-provider, theme) - 모든 import 경로 업데이트하여 새로운 구조 반영 - 기존 components, lib 디렉토리 제거 - 빌드 검증 완료 향후 확장: - widgets: 복잡한 UI 블록 - features: 기능 단위 코드 - entities: 비즈니스 엔티티 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactNode, useState } from 'react';
|
||||
|
||||
export function ReactQueryProvider({ children }: { children: ReactNode }) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime)
|
||||
refetchOnWindowFocus: true,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
|
||||
import { theme } from '@/shared/lib/theme';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export function MUIThemeProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<AppRouterCacheProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</AppRouterCacheProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
// KT 브랜드 컬러 시스템
|
||||
const colors = {
|
||||
// Primary: KT Red
|
||||
primary: {
|
||||
main: '#E31E24',
|
||||
light: '#FF4D52',
|
||||
dark: '#C71820',
|
||||
contrastText: '#FFFFFF',
|
||||
},
|
||||
// Secondary: AI Blue
|
||||
secondary: {
|
||||
main: '#0066FF',
|
||||
light: '#4D94FF',
|
||||
dark: '#004DBF',
|
||||
contrastText: '#FFFFFF',
|
||||
},
|
||||
// Grayscale
|
||||
gray: {
|
||||
900: '#1A1A1A', // Black
|
||||
700: '#4A4A4A',
|
||||
500: '#9E9E9E',
|
||||
300: '#D9D9D9',
|
||||
100: '#F5F5F5',
|
||||
50: '#FFFFFF', // White
|
||||
},
|
||||
// Semantic Colors
|
||||
success: {
|
||||
main: '#00C853',
|
||||
},
|
||||
warning: {
|
||||
main: '#FFA000',
|
||||
},
|
||||
error: {
|
||||
main: '#D32F2F',
|
||||
},
|
||||
info: {
|
||||
main: '#0288D1',
|
||||
},
|
||||
};
|
||||
|
||||
// Breakpoints (Mobile First)
|
||||
const breakpoints = {
|
||||
values: {
|
||||
xs: 320,
|
||||
sm: 768,
|
||||
md: 1024,
|
||||
lg: 1280,
|
||||
xl: 1920,
|
||||
},
|
||||
};
|
||||
|
||||
// Typography (Pretendard)
|
||||
const typography = {
|
||||
fontFamily: '"Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", system-ui, sans-serif',
|
||||
// Display
|
||||
h1: {
|
||||
fontSize: '28px',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.3,
|
||||
letterSpacing: '-0.5px',
|
||||
'@media (min-width:768px)': {
|
||||
fontSize: '32px',
|
||||
},
|
||||
'@media (min-width:1024px)': {
|
||||
fontSize: '36px',
|
||||
},
|
||||
},
|
||||
// H1
|
||||
h2: {
|
||||
fontSize: '24px',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.3,
|
||||
letterSpacing: '-0.3px',
|
||||
'@media (min-width:768px)': {
|
||||
fontSize: '28px',
|
||||
},
|
||||
'@media (min-width:1024px)': {
|
||||
fontSize: '32px',
|
||||
},
|
||||
},
|
||||
// H2
|
||||
h3: {
|
||||
fontSize: '20px',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.4,
|
||||
letterSpacing: '-0.2px',
|
||||
'@media (min-width:768px)': {
|
||||
fontSize: '22px',
|
||||
},
|
||||
'@media (min-width:1024px)': {
|
||||
fontSize: '24px',
|
||||
},
|
||||
},
|
||||
// H3
|
||||
h4: {
|
||||
fontSize: '18px',
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.4,
|
||||
letterSpacing: '0px',
|
||||
'@media (min-width:768px)': {
|
||||
fontSize: '20px',
|
||||
},
|
||||
},
|
||||
// Body Large
|
||||
body1: {
|
||||
fontSize: '16px',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: '0px',
|
||||
'@media (min-width:768px)': {
|
||||
fontSize: '18px',
|
||||
},
|
||||
},
|
||||
// Body Medium
|
||||
body2: {
|
||||
fontSize: '14px',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: '0px',
|
||||
'@media (min-width:768px)': {
|
||||
fontSize: '16px',
|
||||
},
|
||||
},
|
||||
// Body Small
|
||||
caption: {
|
||||
fontSize: '12px',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: '0px',
|
||||
'@media (min-width:768px)': {
|
||||
fontSize: '14px',
|
||||
},
|
||||
},
|
||||
// Button
|
||||
button: {
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: '0px',
|
||||
textTransform: 'none' as const,
|
||||
},
|
||||
};
|
||||
|
||||
// Spacing (4px Grid System)
|
||||
const spacing = 4;
|
||||
|
||||
// Component Overrides
|
||||
const components = {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: '8px',
|
||||
padding: '12px 20px',
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
},
|
||||
sizeLarge: {
|
||||
height: '48px',
|
||||
padding: '16px 24px',
|
||||
},
|
||||
sizeMedium: {
|
||||
height: '44px',
|
||||
padding: '12px 20px',
|
||||
},
|
||||
sizeSmall: {
|
||||
height: '36px',
|
||||
padding: '8px 16px',
|
||||
},
|
||||
contained: {
|
||||
boxShadow: '0 2px 4px rgba(227, 30, 36, 0.2)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||
border: '1px solid #E0E0E0',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '8px',
|
||||
'& fieldset': {
|
||||
borderColor: '#D9D9D9',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: '#E31E24',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#0066FF',
|
||||
borderWidth: '2px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCheckbox: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&.Mui-checked': {
|
||||
color: '#E31E24',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiRadio: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&.Mui-checked': {
|
||||
color: '#E31E24',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Create MUI Theme
|
||||
export const theme = createTheme({
|
||||
palette: {
|
||||
primary: colors.primary,
|
||||
secondary: colors.secondary,
|
||||
success: colors.success,
|
||||
warning: colors.warning,
|
||||
error: colors.error,
|
||||
info: colors.info,
|
||||
grey: colors.gray,
|
||||
background: {
|
||||
default: '#FFFFFF',
|
||||
paper: '#FFFFFF',
|
||||
},
|
||||
text: {
|
||||
primary: colors.gray[900],
|
||||
secondary: colors.gray[700],
|
||||
disabled: colors.gray[500],
|
||||
},
|
||||
},
|
||||
breakpoints,
|
||||
typography,
|
||||
spacing,
|
||||
components,
|
||||
shape: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { BottomNavigation as MuiBottomNavigation, BottomNavigationAction, Paper } from '@mui/material';
|
||||
import { Home, Celebration, Analytics, Person } from '@mui/icons-material';
|
||||
|
||||
export default function BottomNavigation() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
// Determine current value based on pathname
|
||||
const getCurrentValue = () => {
|
||||
if (pathname === '/' || pathname === '/dashboard') return 'home';
|
||||
if (pathname.startsWith('/events')) return 'events';
|
||||
if (pathname.startsWith('/analytics')) return 'analytics';
|
||||
if (pathname.startsWith('/profile')) return 'profile';
|
||||
return 'home';
|
||||
};
|
||||
|
||||
const value = getCurrentValue();
|
||||
|
||||
const handleChange = (_event: React.SyntheticEvent, newValue: string) => {
|
||||
switch (newValue) {
|
||||
case 'home':
|
||||
router.push('/');
|
||||
break;
|
||||
case 'events':
|
||||
router.push('/events');
|
||||
break;
|
||||
case 'analytics':
|
||||
router.push('/analytics');
|
||||
break;
|
||||
case 'profile':
|
||||
router.push('/profile');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
elevation={3}
|
||||
>
|
||||
<MuiBottomNavigation
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
showLabels
|
||||
sx={{
|
||||
height: { xs: 56, sm: 64 },
|
||||
'& .MuiBottomNavigationAction-root': {
|
||||
minWidth: 'auto',
|
||||
px: 1,
|
||||
},
|
||||
'& .MuiBottomNavigationAction-label': {
|
||||
fontSize: { xs: '0.7rem', sm: '0.75rem' },
|
||||
fontWeight: 600,
|
||||
mt: 0.5,
|
||||
},
|
||||
'& .MuiBottomNavigationAction-root.Mui-selected': {
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<BottomNavigationAction
|
||||
label="홈"
|
||||
value="home"
|
||||
icon={<Home />}
|
||||
sx={{
|
||||
'& .MuiSvgIcon-root': {
|
||||
fontSize: { xs: 24, sm: 28 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<BottomNavigationAction
|
||||
label="이벤트"
|
||||
value="events"
|
||||
icon={<Celebration />}
|
||||
sx={{
|
||||
'& .MuiSvgIcon-root': {
|
||||
fontSize: { xs: 24, sm: 28 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<BottomNavigationAction
|
||||
label="분석"
|
||||
value="analytics"
|
||||
icon={<Analytics />}
|
||||
sx={{
|
||||
'& .MuiSvgIcon-root': {
|
||||
fontSize: { xs: 24, sm: 28 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<BottomNavigationAction
|
||||
label="프로필"
|
||||
value="profile"
|
||||
icon={<Person />}
|
||||
sx={{
|
||||
'& .MuiSvgIcon-root': {
|
||||
fontSize: { xs: 24, sm: 28 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</MuiBottomNavigation>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AppBar, Toolbar, IconButton, Typography, Box } from '@mui/material';
|
||||
import { ArrowBack, Menu, AccountCircle } from '@mui/icons-material';
|
||||
|
||||
interface HeaderProps {
|
||||
title?: string;
|
||||
showBack?: boolean;
|
||||
showMenu?: boolean;
|
||||
showProfile?: boolean;
|
||||
onBackClick?: () => void;
|
||||
onMenuClick?: () => void;
|
||||
onProfileClick?: () => void;
|
||||
}
|
||||
|
||||
export default function Header({
|
||||
title = '',
|
||||
showBack = true,
|
||||
showMenu = false,
|
||||
showProfile = true,
|
||||
onBackClick,
|
||||
onMenuClick,
|
||||
onProfileClick,
|
||||
}: HeaderProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const handleBackClick = () => {
|
||||
if (onBackClick) {
|
||||
onBackClick();
|
||||
} else {
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
|
||||
const handleProfileClick = () => {
|
||||
if (onProfileClick) {
|
||||
onProfileClick();
|
||||
} else {
|
||||
router.push('/profile');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position="fixed"
|
||||
elevation={0}
|
||||
sx={{
|
||||
bgcolor: 'background.paper',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
sx={{
|
||||
minHeight: { xs: 56, sm: 64 },
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
{/* Left Section */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||
{showMenu && (
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="메뉴"
|
||||
onClick={onMenuClick}
|
||||
sx={{ mr: 1, color: 'text.primary' }}
|
||||
>
|
||||
<Menu />
|
||||
</IconButton>
|
||||
)}
|
||||
{showBack && (
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="뒤로가기"
|
||||
onClick={handleBackClick}
|
||||
sx={{ mr: 1, color: 'text.primary' }}
|
||||
>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
)}
|
||||
{title && (
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: 'text.primary',
|
||||
fontSize: { xs: '1.1rem', sm: '1.25rem' },
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Right Section */}
|
||||
{showProfile && (
|
||||
<IconButton
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="프로필"
|
||||
onClick={handleProfileClick}
|
||||
sx={{ color: 'text.primary' }}
|
||||
>
|
||||
<AccountCircle />
|
||||
</IconButton>
|
||||
)}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Box, CircularProgress, Typography } from '@mui/material';
|
||||
|
||||
interface LoadingProps {
|
||||
message?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export function Loading({ message = '로딩 중...', size = 40 }: LoadingProps) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '200px',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={size} />
|
||||
{message && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{message}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { Snackbar, Alert } from '@mui/material';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
|
||||
export function Toast() {
|
||||
const { toast, hideToast } = useUIStore();
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={toast.open}
|
||||
autoHideDuration={3000}
|
||||
onClose={hideToast}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
sx={{ bottom: { xs: 80, sm: 24 } }}
|
||||
>
|
||||
<Alert
|
||||
onClose={hideToast}
|
||||
severity={toast.severity}
|
||||
variant="filled"
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{toast.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user