diff --git a/.gitignore b/.gitignore index a9dbb96..780b4de 100644 --- a/.gitignore +++ b/.gitignore @@ -8,103 +8,63 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* -# Dependency directories -node_modules/ -jspm_packages/ +# Dependencies +node_modules +.pnpm +dist +dist-ssr +*.local -# Production build -dist/ -dist-ssr/ -build/ +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? -# Environment variables +# Environment files .env .env.local .env.development.local .env.test.local .env.production.local -# IDE and Editor files -.vscode/ -!.vscode/extensions.json -.idea/ -*.swp -*.swo -*~ +# Runtime files +runtime-env.js.bak +config.local.js + +# Build artifacts +coverage/ +.nyc_output/ +build/ +dist/ # OS generated files +Thumbs.db .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db -Thumbs.db -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Coverage directory used by tools like istanbul -coverage/ -*.lcov - -# nyc test coverage -.nyc_output - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Vite cache -.vite/ - -# Vue.js specific -.vue/ +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ # Temporary files *.tmp *.temp +.cache/ -# Test coverage -tests/unit/coverage/ -tests/e2e/coverage/ - -# Local Netlify folder -.netlify - -# AI Marketing Frontend specific -/public/runtime-env.js.backup -/src/assets/temp/ -/public/images/uploads/ -*.backup - -# Vue DevTools -.vue-devtools - -# Cypress -cypress/videos/ -cypress/screenshots/ - -# ESLint -.eslintcache - -# Stylelint -.stylelintcache \ No newline at end of file +# Package manager files +package-lock.json +yarn.lock +pnpm-lock.yaml \ No newline at end of file diff --git a/public/index.html b/public/index.html index dbe5779..d6addd7 100644 --- a/public/index.html +++ b/public/index.html @@ -1,185 +1,59 @@ +//* public/index.html - - - - ₩ON - AI 마케팅 서비스 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
-
₩ON AI 마케팅 서비스를 불러오는 중...
-
-
-
+ + + + + + - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + AI 마케팅 - 소상공인을 위한 스마트 마케팅 솔루션 + + +
+ + + + + + + + + + + diff --git a/public/manifest.json b/public/manifest.json index 5a7ed54..8838c06 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,91 +1,68 @@ +//* public/manifest.json { - "name": "₩ON - AI 마케팅 서비스", - "short_name": "₩ON", - "description": "소상공인을 위한 AI 기반 마케팅 솔루션", - "version": "1.0.0", - "lang": "ko", + "name": "AI 마케팅 - 소상공인을 위한 스마트 마케팅 솔루션", + "short_name": "AI 마케팅", + "description": "AI를 활용한 소상공인 전용 마케팅 자동화 서비스", "start_url": "/", "display": "standalone", - "orientation": "portrait-primary", + "background_color": "#ffffff", "theme_color": "#1976D2", - "background_color": "#FAFAFA", - "categories": ["business", "productivity", "marketing"], + "orientation": "portrait-primary", + "scope": "/", + "lang": "ko-KR", "icons": [ { - "src": "/images/logo-192.png", + "src": "/images/logo192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { - "src": "/images/logo-512.png", + "src": "/images/logo512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" - }, - { - "src": "/favicon.ico", - "sizes": "64x64", - "type": "image/x-icon" } ], + "categories": ["business", "productivity", "marketing"], "screenshots": [ { - "src": "/images/screenshot-1.png", + "src": "/images/screenshot1.png", "sizes": "1280x720", "type": "image/png", - "label": "메인 대시보드 화면" + "form_factor": "wide" }, { - "src": "/images/screenshot-2.png", - "sizes": "1280x720", + "src": "/images/screenshot2.png", + "sizes": "750x1334", "type": "image/png", - "label": "AI 콘텐츠 생성 화면" + "form_factor": "narrow" } ], "shortcuts": [ { "name": "대시보드", "short_name": "대시보드", - "description": "매출 현황 및 성과 확인", + "description": "메인 대시보드로 이동", "url": "/dashboard", "icons": [ { - "src": "/images/shortcut-dashboard.png", - "sizes": "96x96", - "type": "image/png" + "src": "/images/dashboard-icon.png", + "sizes": "96x96" } ] }, { "name": "콘텐츠 생성", "short_name": "콘텐츠", - "description": "AI 마케팅 콘텐츠 생성", + "description": "새 콘텐츠 생성", "url": "/content/create", "icons": [ { - "src": "/images/shortcut-content.png", - "sizes": "96x96", - "type": "image/png" - } - ] - }, - { - "name": "매장 관리", - "short_name": "매장", - "description": "매장 및 메뉴 정보 관리", - "url": "/store", - "icons": [ - { - "src": "/images/shortcut-store.png", - "sizes": "96x96", - "type": "image/png" + "src": "/images/content-icon.png", + "sizes": "96x96" } ] } - ], - "related_applications": [], - "prefer_related_applications": false, - "scope": "/", - "id": "won-ai-marketing" -} \ No newline at end of file + ] +} diff --git a/public/runtime-env.js b/public/runtime-env.js index 605b1fa..8e75086 100644 --- a/public/runtime-env.js +++ b/public/runtime-env.js @@ -1,177 +1,28 @@ +//* public/runtime-env.js window.__runtime_config__ = { - // API Gateway URL (단일 진입점) - GATEWAY_URL: 'http://20.1.2.3', - - // 각 마이크로서비스별 URL (Ingress 라우팅) - MEMBER_URL: 'http://20.1.2.3/api/member', - AUTH_URL: 'http://20.1.2.3/api/auth', - STORE_URL: 'http://20.1.2.3/api/store', - CONTENT_URL: 'http://20.1.2.3/api/content', - RECOMMEND_URL: 'http://20.1.2.3/api/recommendation', - - // 애플리케이션 설정 - APP_VERSION: '1.0.0', - APP_NAME: '₩ON', - - // 환경 설정 - ENVIRONMENT: 'production', - DEBUG_MODE: false, - - // API 설정 - API_TIMEOUT: 30000, - RETRY_ATTEMPTS: 3, - - // 파일 업로드 설정 - MAX_FILE_SIZE: 10485760, // 10MB - ALLOWED_IMAGE_TYPES: ['image/jpeg', 'image/png', 'image/webp'], - ALLOWED_VIDEO_TYPES: ['video/mp4', 'video/webm'], - - // 인증 설정 - TOKEN_REFRESH_MARGIN: 300, // 5분 전 토큰 갱신 - SESSION_TIMEOUT: 1800, // 30분 - - // UI 설정 - MOBILE_BREAKPOINT: 768, - TABLET_BREAKPOINT: 1024, - - // 페이지네이션 설정 - DEFAULT_PAGE_SIZE: 20, - MAX_PAGE_SIZE: 100, - - // 알림 설정 - NOTIFICATION_TIMEOUT: 5000, - MAX_NOTIFICATIONS: 5, - - // 캐시 설정 - CACHE_DURATION: 300000, // 5분 - - // 지도 API (필요한 경우) - // KAKAO_MAP_API_KEY: '', - // NAVER_MAP_API_KEY: '', - - // 소셜 로그인 (향후 확장) - // KAKAO_APP_KEY: '', - // NAVER_CLIENT_ID: '', - // GOOGLE_CLIENT_ID: '', - - // 분석 도구 (필요한 경우) - // GOOGLE_ANALYTICS_ID: '', - // HOTJAR_ID: '', - - // CDN 설정 - IMAGE_CDN_URL: 'http://20.1.2.3/images', - STATIC_CDN_URL: 'http://20.1.2.3/static', - - // 피처 플래그 + // API 서버 URL들 + AUTH_URL: 'http://20.1.2.3/auth', + STORE_URL: 'http://20.1.2.3/store', + CONTENT_URL: 'http://20.1.2.3/content', + RECOMMEND_URL: 'http://20.1.2.3/recommend', + + // 외부 API 설정 + CLAUDE_AI_ENABLED: true, + WEATHER_API_ENABLED: true, + + // 기능 플래그 FEATURES: { - AI_RECOMMENDATION: true, - VOICE_INPUT: false, - DARK_MODE: true, - OFFLINE_MODE: false, + ANALYTICS: true, PUSH_NOTIFICATIONS: true, - REAL_TIME_UPDATES: true, - MULTI_STORE_SUPPORT: false, - ADVANCED_ANALYTICS: true + SOCIAL_LOGIN: false, + MULTI_LANGUAGE: false, }, - - // 테마 설정 - THEME: { - PRIMARY_COLOR: '#1976D2', - SECONDARY_COLOR: '#FFC107', - SUCCESS_COLOR: '#4CAF50', - WARNING_COLOR: '#FF9800', - ERROR_COLOR: '#F44336', - INFO_COLOR: '#2196F3' - }, - - // 국제화 설정 - LOCALE: 'ko-KR', - TIMEZONE: 'Asia/Seoul', - CURRENCY: 'KRW', - - // 콘텐츠 생성 설정 - AI_CONTENT: { - MAX_REGENERATION_COUNT: 5, - CONTENT_TYPES: ['sns_post', 'poster', 'banner'], - PLATFORMS: ['instagram', 'naver_blog', 'facebook'], - TONE_OPTIONS: ['friendly', 'professional', 'humorous', 'elegant'], - EMOTION_LEVELS: ['calm', 'normal', 'passionate', 'exaggerated'] - }, - - // 메뉴 카테고리 기본값 - DEFAULT_MENU_CATEGORIES: ['면류', '밥류', '튀김', '분식', '음료', '디저트', '기타'], - - // 매장 업종 기본값 - BUSINESS_TYPES: [ - '한식', '중식', '일식', '양식', '분식', '치킨', '피자', '카페', - '베이커리', '아이스크림', '패스트푸드', '기타' - ], - - // 운영시간 기본값 - DEFAULT_OPERATING_HOURS: { - WEEKDAY: { open: '09:00', close: '22:00' }, - WEEKEND: { open: '10:00', close: '21:00' } - }, - - // 에러 메시지 - ERROR_MESSAGES: { - NETWORK_ERROR: '네트워크 연결을 확인해주세요.', - TIMEOUT_ERROR: '요청 시간이 초과되었습니다.', - UNAUTHORIZED: '로그인이 필요합니다.', - FORBIDDEN: '접근 권한이 없습니다.', - NOT_FOUND: '요청한 페이지를 찾을 수 없습니다.', - SERVER_ERROR: '서버 오류가 발생했습니다.', - VALIDATION_ERROR: '입력 정보를 확인해주세요.', - FILE_SIZE_ERROR: '파일 크기가 너무 큽니다.', - FILE_TYPE_ERROR: '지원하지 않는 파일 형식입니다.' - }, - - // 성공 메시지 - SUCCESS_MESSAGES: { - LOGIN_SUCCESS: '로그인이 완료되었습니다.', - LOGOUT_SUCCESS: '로그아웃이 완료되었습니다.', - REGISTER_SUCCESS: '회원가입이 완료되었습니다.', - UPDATE_SUCCESS: '정보가 수정되었습니다.', - DELETE_SUCCESS: '삭제가 완료되었습니다.', - SAVE_SUCCESS: '저장이 완료되었습니다.', - UPLOAD_SUCCESS: '업로드가 완료되었습니다.', - CONTENT_GENERATED: 'AI 콘텐츠가 생성되었습니다.' - }, - - // 확인 메시지 - CONFIRM_MESSAGES: { - DELETE_CONFIRM: '정말 삭제하시겠습니까?', - LOGOUT_CONFIRM: '로그아웃 하시겠습니까?', - CANCEL_CONFIRM: '작업을 취소하시겠습니까?', - OVERWRITE_CONFIRM: '기존 내용을 덮어쓰시겠습니까?' - } -}; -// 환경별 설정 오버라이드 -if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { - // 개발 환경 설정 - window.__runtime_config__.ENVIRONMENT = 'development'; - window.__runtime_config__.DEBUG_MODE = true; - window.__runtime_config__.GATEWAY_URL = 'http://localhost:8080'; - window.__runtime_config__.MEMBER_URL = 'http://localhost:8080/api/member'; - window.__runtime_config__.AUTH_URL = 'http://localhost:8080/api/auth'; - window.__runtime_config__.STORE_URL = 'http://localhost:8080/api/store'; - window.__runtime_config__.CONTENT_URL = 'http://localhost:8080/api/content'; - window.__runtime_config__.RECOMMEND_URL = 'http://localhost:8080/api/recommendation'; + // 환경 설정 + ENV: 'production', + DEBUG: false, + + // 버전 정보 + VERSION: '1.0.0', + BUILD_DATE: new Date().toISOString(), } - -// 설정 유효성 검사 -if (!window.__runtime_config__.GATEWAY_URL) { - console.error('GATEWAY_URL이 설정되지 않았습니다.'); -} - -// 전역 함수로 설정 노출 -window.getConfig = function(key) { - return window.__runtime_config__[key]; -}; - -window.getFeature = function(featureName) { - return window.__runtime_config__.FEATURES[featureName] || false; -}; - -console.log('₩ON Runtime Configuration loaded:', window.__runtime_config__.ENVIRONMENT); diff --git a/src/main.js b/src/main.js index 0bbbe1b..421af02 100644 --- a/src/main.js +++ b/src/main.js @@ -16,6 +16,9 @@ import * as directives from 'vuetify/directives' import { mdi } from 'vuetify/iconsets/mdi' import '@mdi/font/css/materialdesignicons.css' +// 전역 스타일 +import './styles/main.scss' + // Vuetify 테마 설정 const vuetify = createVuetify({ components, @@ -32,17 +35,32 @@ const vuetify = createVuetify({ info: '#2196F3', success: '#4CAF50', warning: '#FFC107', - background: '#F5F5F5', - surface: '#FFFFFF' - } - } - } + background: '#FFFFFF', + surface: '#FFFFFF', + 'surface-variant': '#F5F5F5', + }, + }, + dark: { + colors: { + primary: '#2196F3', + secondary: '#616161', + accent: '#82B1FF', + error: '#FF5252', + info: '#2196F3', + success: '#4CAF50', + warning: '#FFC107', + background: '#121212', + surface: '#1E1E1E', + 'surface-variant': '#424242', + }, + }, + }, }, icons: { defaultSet: 'mdi', sets: { - mdi - } + mdi, + }, }, display: { mobileBreakpoint: 'sm', @@ -51,15 +69,74 @@ const vuetify = createVuetify({ sm: 600, md: 960, lg: 1280, - xl: 1920 - } - } + xl: 1920, + }, + }, + defaults: { + VCard: { + elevation: 2, + rounded: 'lg', + }, + VBtn: { + rounded: 'lg', + }, + VTextField: { + variant: 'outlined', + density: 'comfortable', + }, + VSelect: { + variant: 'outlined', + density: 'comfortable', + }, + VTextarea: { + variant: 'outlined', + density: 'comfortable', + }, + }, }) +// Pinia 스토어 생성 +const pinia = createPinia() + +// Vue 앱 생성 const app = createApp(App) -app.use(createPinia()) +// 전역 속성 설정 +app.config.globalProperties.$config = window.__runtime_config__ || {} + +// 에러 핸들링 +app.config.errorHandler = (err, instance, info) => { + console.error('Vue 앱 에러:', err, info) + + // 프로덕션 환경에서 에러 리포팅 + if (import.meta.env.PROD) { + // 에러 리포팅 서비스에 전송 + // reportError(err, instance, info) + } +} + +// 플러그인 등록 +app.use(pinia) app.use(router) app.use(vuetify) -app.mount('#app') \ No newline at end of file +// 개발 모드 설정 +if (import.meta.env.DEV) { + app.config.performance = true + window.__VUE_DEVTOOLS_GLOBAL_HOOK__ = window.__VUE_DEVTOOLS_GLOBAL_HOOK__ || {} +} + +// 앱 마운트 +app.mount('#app') + +// 서비스 워커 등록 (프로덕션에서만) +if (import.meta.env.PROD && 'serviceWorker' in navigator) { + window.addEventListener('load', async () => { + try { + const registration = await navigator.serviceWorker.register('/sw.js') + console.log('서비스 워커 등록 성공:', registration) + } catch (error) { + console.log('서비스 워커 등록 실패:', error) + } + }) +} diff --git a/src/styles/main.scss b/src/styles/main.scss new file mode 100644 index 0000000..71fadb3 --- /dev/null +++ b/src/styles/main.scss @@ -0,0 +1,256 @@ +//* src/styles/main.scss +/** + * 메인 스타일시트 + * 전역 스타일 및 커스텀 스타일 정의 + */ + +// 변수 정의 +:root { + --primary-color: #1976d2; + --secondary-color: #424242; + --success-color: #4caf50; + --warning-color: #ffc107; + --error-color: #ff5252; + --info-color: #2196f3; + + --background-color: #ffffff; + --surface-color: #ffffff; + --surface-variant-color: #f5f5f5; + + --text-primary: #212121; + --text-secondary: #757575; + --text-disabled: #bdbdbd; + + --border-color: #e0e0e0; + --divider-color: #f5f5f5; + + --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.1); + + --border-radius-sm: 4px; + --border-radius-md: 8px; + --border-radius-lg: 12px; + --border-radius-xl: 16px; + + --transition-fast: 0.15s ease; + --transition-normal: 0.3s ease; + --transition-slow: 0.5s ease; +} + +// 다크 테마 변수 +[data-theme='dark'] { + --background-color: #121212; + --surface-color: #1e1e1e; + --surface-variant-color: #424242; + + --text-primary: #ffffff; + --text-secondary: #aaaaaa; + --text-disabled: #666666; + + --border-color: #333333; + --divider-color: #2a2a2a; +} + +// 기본 스타일 리셋 +* { + box-sizing: border-box; +} + +html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + scroll-behavior: smooth; +} + +body { + margin: 0; + padding: 0; + font-family: 'Roboto', 'Noto Sans KR', sans-serif; + background-color: var(--background-color); + color: var(--text-primary); + line-height: 1.6; +} + +// 링크 스타일 +a { + color: var(--primary-color); + text-decoration: none; + transition: var(--transition-fast); + + &:hover { + opacity: 0.8; + } +} + +// 버튼 공통 스타일 +.btn-gradient { + background: linear-gradient(135deg, var(--primary-color), #1565c0); + color: white; + border: none; + transition: var(--transition-normal); + + &:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); + } +} + +// 카드 스타일 +.card-hover { + transition: var(--transition-normal); + + &:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-lg); + } +} + +// 유틸리티 클래스 +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.text-truncate-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.text-truncate-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +// 스크롤바 스타일 +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--surface-variant-color); + border-radius: var(--border-radius-md); +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: var(--border-radius-md); + + &:hover { + background: var(--text-secondary); + } +} + +// 로딩 애니메이션 +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.loading-pulse { + animation: pulse 2s infinite; +} + +// 페이드 인 애니메이션 +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeIn var(--transition-normal); +} + +// 반응형 유틸리티 +@media (max-width: 600px) { + .mobile-hide { + display: none !important; + } + + .mobile-full-width { + width: 100% !important; + } +} + +@media (min-width: 601px) { + .desktop-hide { + display: none !important; + } +} + +// 접근성 개선 +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +// 포커스 스타일 개선 +.focus-outline { + &:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; + } +} + +// 인쇄 스타일 +@media print { + .no-print { + display: none !important; + } + + .v-navigation-drawer, + .v-app-bar, + .v-bottom-navigation { + display: none !important; + } + + .v-main { + padding: 0 !important; + } +} + +// 고대비 모드 지원 +@media (prefers-contrast: high) { + :root { + --border-color: #000000; + --text-secondary: #000000; + } + + [data-theme='dark'] { + --border-color: #ffffff; + --text-secondary: #ffffff; + } +} + +// 움직임 줄이기 설정 존중 +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/vite.config.js b/vite.config.js index c48ade5..d108403 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,26 +1,38 @@ //* vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' -import vuetify from 'vite-plugin-vuetify' import { fileURLToPath, URL } from 'node:url' export default defineConfig({ - plugins: [ - vue(), - vuetify({ - autoImport: true, - theme: { - defaultTheme: 'light' - } - }) - ], + plugins: [vue()], resolve: { alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)) - } + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, }, server: { port: 3000, - host: true - } -}) \ No newline at end of file + host: true, + open: true, + }, + build: { + outDir: 'dist', + sourcemap: true, + rollupOptions: { + output: { + manualChunks: { + vendor: ['vue', 'vue-router', 'pinia'], + vuetify: ['vuetify'], + icons: ['@mdi/font'], + }, + }, + }, + }, + css: { + preprocessorOptions: { + scss: { + additionalData: `@import "@/styles/variables.scss";`, + }, + }, + }, +})