public files add

This commit is contained in:
unknown 2025-06-11 15:18:38 +09:00
parent 1ee5e6122a
commit 5f3617e132
4 changed files with 677 additions and 0 deletions

185
public/index.html Normal file
View File

@ -0,0 +1,185 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>₩ON - AI 마케팅 서비스</title>
<meta name="description" content="소상공인을 위한 AI 기반 마케팅 솔루션">
<meta name="keywords" content="소상공인, 마케팅, AI, 콘텐츠 생성, 매장 관리">
<meta name="author" content="₩ON Team">
<!-- Favicon -->
<link rel="icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/logo192.png">
<!-- Web App Manifest -->
<link rel="manifest" href="/manifest.json">
<!-- Meta tags for mobile -->
<meta name="theme-color" content="#1976D2">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="₩ON">
<!-- PWA Meta Tags -->
<meta name="application-name" content="₩ON">
<meta name="msapplication-TileColor" content="#1976D2">
<!-- Runtime Environment Configuration -->
<script src="/runtime-env.js"></script>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Material Design Icons -->
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" rel="stylesheet">
<!-- CSS Variables for Theme -->
<style>
:root {
--primary-color: #1976D2;
--primary-dark: #1565C0;
--primary-light: #BBDEFB;
--secondary-color: #FFC107;
--success-color: #4CAF50;
--warning-color: #FF9800;
--error-color: #F44336;
--info-color: #2196F3;
--background-color: #FAFAFA;
--surface-color: #FFFFFF;
--text-primary: #212121;
--text-secondary: #757575;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: 'Noto Sans KR', sans-serif;
background-color: var(--background-color);
color: var(--text-primary);
overflow-x: hidden;
}
#app {
height: 100%;
display: flex;
flex-direction: column;
}
/* Loading Spinner */
.loading-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--surface-color);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-spinner {
width: 60px;
height: 60px;
border: 4px solid var(--primary-light);
border-top: 4px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 16px;
color: var(--text-secondary);
font-size: 14px;
}
/* Mobile Optimizations */
@media (max-width: 768px) {
body {
font-size: 14px;
}
.v-application {
line-height: 1.5;
}
}
/* Prevent zoom on input focus in iOS */
@media screen and (max-width: 768px) {
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
textarea {
font-size: 16px !important;
}
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary-dark);
}
</style>
</head>
<body>
<noscript>
<div style="text-align: center; padding: 50px;">
<h2>JavaScript가 필요합니다</h2>
<p>이 애플리케이션을 사용하려면 브라우저에서 JavaScript를 활성화해주세요.</p>
</div>
</noscript>
<div id="app">
<!-- Initial Loading Screen -->
<div class="loading-container" id="initial-loading">
<div style="text-align: center;">
<div class="loading-spinner"></div>
<div class="loading-text">₩ON AI 마케팅 서비스를 불러오는 중...</div>
</div>
</div>
</div>
<!-- Service Worker Registration -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW registered: ', registration);
})
.catch((registrationError) => {
console.log('SW registration failed: ', registrationError);
});
});
}
</script>
</body>
</html>

91
public/manifest.json Normal file
View File

@ -0,0 +1,91 @@
{
"name": "₩ON - AI 마케팅 서비스",
"short_name": "₩ON",
"description": "소상공인을 위한 AI 기반 마케팅 솔루션",
"version": "1.0.0",
"lang": "ko",
"start_url": "/",
"display": "standalone",
"orientation": "portrait-primary",
"theme_color": "#1976D2",
"background_color": "#FAFAFA",
"categories": ["business", "productivity", "marketing"],
"icons": [
{
"src": "/images/logo-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/images/logo-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/favicon.ico",
"sizes": "64x64",
"type": "image/x-icon"
}
],
"screenshots": [
{
"src": "/images/screenshot-1.png",
"sizes": "1280x720",
"type": "image/png",
"label": "메인 대시보드 화면"
},
{
"src": "/images/screenshot-2.png",
"sizes": "1280x720",
"type": "image/png",
"label": "AI 콘텐츠 생성 화면"
}
],
"shortcuts": [
{
"name": "대시보드",
"short_name": "대시보드",
"description": "매출 현황 및 성과 확인",
"url": "/dashboard",
"icons": [
{
"src": "/images/shortcut-dashboard.png",
"sizes": "96x96",
"type": "image/png"
}
]
},
{
"name": "콘텐츠 생성",
"short_name": "콘텐츠",
"description": "AI 마케팅 콘텐츠 생성",
"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"
}
]
}
],
"related_applications": [],
"prefer_related_applications": false,
"scope": "/",
"id": "won-ai-marketing"
}

177
public/runtime-env.js Normal file
View File

@ -0,0 +1,177 @@
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',
// 피처 플래그
FEATURES: {
AI_RECOMMENDATION: true,
VOICE_INPUT: false,
DARK_MODE: true,
OFFLINE_MODE: false,
PUSH_NOTIFICATIONS: true,
REAL_TIME_UPDATES: true,
MULTI_STORE_SUPPORT: false,
ADVANCED_ANALYTICS: true
},
// 테마 설정
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';
}
// 설정 유효성 검사
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);

224
public/sw.js Normal file
View File

@ -0,0 +1,224 @@
// Service Worker for ₩ON AI Marketing Service
const CACHE_NAME = 'won-marketing-v1.0.0';
const OFFLINE_URL = '/offline.html';
// 캐시할 정적 자원들
const STATIC_CACHE_URLS = [
'/',
'/offline.html',
'/manifest.json',
'/runtime-env.js',
'/images/logo.png',
'/images/logo-192.png',
'/images/logo-512.png',
'https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap',
'https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css'
];
// 캐시하지 않을 URL 패턴
const NO_CACHE_PATTERNS = [
/\/api\//,
/\/auth\//,
/\/admin\//
];
// Service Worker 설치
self.addEventListener('install', (event) => {
console.log('Service Worker 설치 중...');
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('정적 자원 캐시 중...');
return cache.addAll(STATIC_CACHE_URLS);
})
.then(() => {
return self.skipWaiting();
})
);
});
// Service Worker 활성화
self.addEventListener('activate', (event) => {
console.log('Service Worker 활성화 중...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('이전 캐시 삭제:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => {
return self.clients.claim();
})
);
});
// 네트워크 요청 처리
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// 캐시하지 않을 패턴 체크
const shouldNotCache = NO_CACHE_PATTERNS.some(pattern => pattern.test(url.pathname));
if (shouldNotCache) {
// API 요청 등은 캐시하지 않고 네트워크만 사용
event.respondWith(fetch(request));
return;
}
// GET 요청만 캐시 전략 적용
if (request.method === 'GET') {
event.respondWith(
caches.match(request)
.then((cachedResponse) => {
if (cachedResponse) {
// 캐시된 응답이 있으면 반환하고, 백그라운드에서 업데이트
fetch(request)
.then((response) => {
if (response.status === 200) {
const responseClone = response.clone();
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(request, responseClone);
});
}
})
.catch(() => {
// 네트워크 오류 무시
});
return cachedResponse;
}
// 캐시된 응답이 없으면 네트워크에서 가져오기
return fetch(request)
.then((response) => {
if (response.status === 200) {
const responseClone = response.clone();
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(request, responseClone);
});
}
return response;
})
.catch(() => {
// 네트워크 오류 시 오프라인 페이지 표시
if (request.destination === 'document') {
return caches.match(OFFLINE_URL);
}
// 이미지 요청 실패 시 기본 이미지 반환
if (request.destination === 'image') {
return new Response(
'<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#f0f0f0"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="#999">이미지 없음</text></svg>',
{ headers: { 'Content-Type': 'image/svg+xml' } }
);
}
throw error;
});
})
);
} else {
// POST, PUT, DELETE 등은 네트워크만 사용
event.respondWith(fetch(request));
}
});
// 백그라운드 동기화
self.addEventListener('sync', (event) => {
if (event.tag === 'background-sync') {
console.log('백그라운드 동기화 실행');
event.waitUntil(doBackgroundSync());
}
});
// 푸시 알림 처리
self.addEventListener('push', (event) => {
console.log('푸시 알림 수신:', event);
const options = {
body: event.data ? event.data.text() : '새로운 알림이 있습니다.',
icon: '/images/logo-192.png',
badge: '/images/logo-192.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: '확인',
icon: '/images/checkmark.png'
},
{
action: 'close',
title: '닫기',
icon: '/images/close.png'
}
]
};
event.waitUntil(
self.registration.showNotification('₩ON 알림', options)
);
});
// 알림 클릭 처리
self.addEventListener('notificationclick', (event) => {
console.log('알림 클릭:', event);
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/')
);
} else if (event.action === 'close') {
// 알림만 닫기
} else {
// 기본 동작 - 앱 열기
event.waitUntil(
clients.openWindow('/')
);
}
});
// 백그라운드 동기화 함수
async function doBackgroundSync() {
try {
// 오프라인 상태에서 저장된 데이터를 서버로 전송
const pendingRequests = await getStoredRequests();
for (const request of pendingRequests) {
try {
await fetch(request.url, request.options);
await removeStoredRequest(request.id);
} catch (error) {
console.log('동기화 실패:', error);
}
}
} catch (error) {
console.log('백그라운드 동기화 오류:', error);
}
}
// 저장된 요청 조회
async function getStoredRequests() {
// IndexedDB나 Cache API를 사용하여 오프라인 요청 저장/조회
return [];
}
// 저장된 요청 삭제
async function removeStoredRequest(id) {
// IndexedDB에서 요청 삭제
return Promise.resolve();
}