#!/bin/bash ################################################################################ # 삼生調和 타로카드 – Cloudflare Dev 도메인 최종 배포 패키지 # 프로젝트명: samsa-harmony-cloudflare-complete # 생성일시: $(date) # 목표: 단일 zip 파일로 Cloudflare Pages & Workers 즉시 배포 ################################################################################ set -e # 색상 코드 (로그 강조) RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # 변수 설정 TIMESTAMP=$(date +%Y%m%d_%H%M%S) PROJECT_NAME="samsa-harmony-cloudflare-complete" FINAL_ZIP="${PROJECT_NAME}-${TIMESTAMP}.zip" STAGING_DIR="/tmp/samsa-harmony-cf-${TIMESTAMP}" CURRENT_DIR=$(pwd) echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}" echo -e "${BLUE} 삼生調和 타로카드 – Cloudflare Dev 배포 패키지 생성${NC}" echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}" echo -e "${YELLOW}타임스탬프: ${TIMESTAMP}${NC}" echo -e "${YELLOW}스테이징 경로: ${STAGING_DIR}${NC}" echo -e "${YELLOW}최종 ZIP: ${FINAL_ZIP}${NC}" echo "" # Step 1: 스테이징 디렉터리 생성 echo -e "${GREEN}[Step 1] 스테이징 디렉터리 생성...${NC}" mkdir -p "${STAGING_DIR}"/{web,api,database,docs,scripts,config} echo -e "${GREEN}✓ 디렉터리 생성 완료${NC}" echo "" # ============================================================================ # Step 2: WEB 애플리케이션 (React + Vite) # ============================================================================ echo -e "${GREEN}[Step 2] Web 애플리케이션 파일 생성...${NC}" # web/package.json cat > "${STAGING_DIR}/web/package.json" << 'EOF' { "name": "samsa-harmony-web", "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "deploy": "wrangler pages publish dist --project-name=samsa-harmony" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "axios": "^1.6.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.0.0", "vite": "^5.0.0" } } EOF # web/vite.config.js cat > "${STAGING_DIR}/web/vite.config.js" << 'EOF' import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], server: { port: 3000, proxy: { '/api': { target: 'https://api.samsa-harmony.dev', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, }, }, build: { outDir: 'dist', sourcemap: false, minify: 'terser', }, }); EOF # web/.env.production cat > "${STAGING_DIR}/web/.env.production" << 'EOF' VITE_API_URL=https://api.samsa-harmony.dev VITE_ENV=production EOF # web/index.html cat > "${STAGING_DIR}/web/index.html" << 'EOF' 삼生調和 타로카드 | Samsa Harmony Tarot
EOF # web/src/main.jsx mkdir -p "${STAGING_DIR}/web/src" cat > "${STAGING_DIR}/web/src/main.jsx" << 'EOF' import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.jsx'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')).render( , ); EOF # web/src/App.jsx cat > "${STAGING_DIR}/web/src/App.jsx" << 'EOF' import React, { useState } from 'react'; import axios from 'axios'; import './App.css'; const API_URL = import.meta.env.VITE_API_URL || 'https://api.samsa-harmony.dev'; export default function App() { const [step, setStep] = useState(1); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [formData, setFormData] = useState({ name: '', gender: 'M', birthDate: '', birthTime: '12:00', }); const [saju, setSaju] = useState(null); const [prayerForce, setPrayerForce] = useState(null); const [cards, setCards] = useState(null); const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); setError(null); }; const calculateSaju = async () => { setLoading(true); setError(null); try { const response = await axios.post(`${API_URL}/api/saju/calculate`, { name: formData.name, birthDate: formData.birthDate, birthTime: formData.birthTime, gender: formData.gender, }); setSaju(response.data); setStep(2); } catch (err) { setError(`사주 계산 오류: ${err.message}`); } finally { setLoading(false); } }; const calculatePrayer = async () => { setLoading(true); setError(null); try { const response = await axios.post(`${API_URL}/api/prayer/calculate`, { name: formData.name, birthDate: formData.birthDate, gender: formData.gender, }); setPrayerForce(response.data); // 자동으로 카드 선택 const cardsResponse = await axios.post(`${API_URL}/api/cards/select`, { prayerForce: response.data.force, prayerGrade: response.data.grade, }); setCards(cardsResponse.data); setStep(3); } catch (err) { setError(`기도력 계산 오류: ${err.message}`); } finally { setLoading(false); } }; const downloadPPT = async () => { setLoading(true); setError(null); try { const response = await axios.post(`${API_URL}/api/ppt/generate`, { name: formData.name, saju, prayerForce, cards, }, { responseType: 'blob' }); const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', `${formData.name}_타로결과.pptx`); document.body.appendChild(link); link.click(); link.parentNode.removeChild(link); } catch (err) { setError(`PPT 생성 오류: ${err.message}`); } finally { setLoading(false); } }; return (

🔮 삼生調和 타로카드

불교 기도력 · 사주 · 타로 통합 분석 시스템

{error &&
{error}
} {step === 1 && (

Step 1: 기본 정보 입력

)} {step === 2 && saju && (

Step 2: 기도력 입력

이름: {formData.name}

사주: {saju.fourPillars}

일간: {saju.dayMaster}

)} {step === 3 && cards && prayerForce && (

Step 3: 결과 및 카드

기도력: {prayerForce.force.toFixed(2)}

등급: {prayerForce.grade}

선택된 카드 (7장)

    {cards.map((card, idx) => (
  • {idx + 1}. {card.cardName}
  • ))}
)}
); } EOF # web/src/App.css cat > "${STAGING_DIR}/web/src/App.css" << 'EOF' .container { max-width: 800px; margin: 0 auto; padding: 20px; background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); } .header { text-align: center; margin-bottom: 40px; border-bottom: 3px solid #2a5298; padding-bottom: 20px; } .header h1 { font-size: 2.5em; color: #1e3c72; margin-bottom: 10px; } .header p { font-size: 1.1em; color: #666; } .step-section { margin: 30px 0; padding: 20px; border: 1px solid #ddd; border-radius: 8px; } .step-section h2 { color: #2a5298; margin-bottom: 20px; } .form-group { display: flex; flex-direction: column; gap: 15px; } .form-group label { font-weight: bold; color: #333; } .form-group input, .form-group select { padding: 10px; border: 1px solid #ccc; border-radius: 5px; font-size: 1em; font-family: 'Noto Sans KR', sans-serif; } .form-group button, button { padding: 12px 24px; background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: white; border: none; border-radius: 5px; font-size: 1em; font-weight: bold; cursor: pointer; transition: all 0.3s ease; } .form-group button:hover, button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(42, 82, 152, 0.4); } .form-group button:disabled, button:disabled { opacity: 0.6; cursor: not-allowed; } .saju-display, .result-display { background: #f9f9f9; padding: 15px; border-radius: 5px; margin-bottom: 20px; } .saju-display p, .result-display p { margin: 10px 0; font-size: 1em; } .cards-list { list-style: none; padding-left: 0; } .cards-list li { padding: 10px; margin: 8px 0; background: #e8f4f8; border-left: 4px solid #2a5298; border-radius: 3px; } .error-banner { background: #ffe5e5; color: #c33; padding: 15px; border-radius: 5px; margin-bottom: 20px; border-left: 4px solid #c33; } .footer { text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 0.9em; } @media (max-width: 600px) { .container { padding: 15px; } .header h1 { font-size: 1.8em; } } EOF # web/src/index.css cat > "${STAGING_DIR}/web/src/index.css" << 'EOF' :root { --primary-color: #1e3c72; --secondary-color: #2a5298; --text-color: #333; --light-bg: #f9f9f9; --border-color: #ddd; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); color: var(--text-color); line-height: 1.6; } #root { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } EOF # web/.gitignore cat > "${STAGING_DIR}/web/.gitignore" << 'EOF' node_modules/ dist/ .env .env.local .env.*.local *.log .DS_Store .vscode/ .idea/ EOF echo -e "${GREEN}✓ Web 애플리케이션 파일 생성 완료${NC}" echo "" # ============================================================================ # Step 3: API 서버 (Cloudflare Workers) # ============================================================================ echo -e "${GREEN}[Step 3] API 서버 (Cloudflare Workers) 파일 생성...${NC}" # api/wrangler.toml cat > "${STAGING_DIR}/api/wrangler.toml" << 'EOF' name = "samsa-harmony-api" type = "javascript" compatibility_date = "2024-03-30" account_id = "your_cloudflare_account_id" workers_dev = true [env.production] name = "samsa-harmony-api-prod" routes = [ { pattern = "api.samsa-harmony.dev/*", zone_id = "your_zone_id" } ] [build] command = "npm run build" [build.upload] format = "modules" main = "./dist/index.js" [env.production.vars] ENVIRONMENT = "production" API_URL = "https://api.samsa-harmony.dev" CORS_ORIGIN = "https://samsa-harmony.dev" [env.development.vars] ENVIRONMENT = "development" API_URL = "http://localhost:8787" CORS_ORIGIN = "*" EOF # api/package.json cat > "${STAGING_DIR}/api/package.json" << 'EOF' { "name": "samsa-harmony-api", "version": "1.0.0", "type": "module", "scripts": { "dev": "wrangler dev", "build": "node ./build.js", "deploy": "wrangler publish", "deploy:prod": "wrangler publish --env production" }, "dependencies": { "itty-router": "^4.0.0" }, "devDependencies": { "wrangler": "^3.36.0", "esbuild": "^0.20.0" } } EOF # api/build.js cat > "${STAGING_DIR}/api/build.js" << 'EOF' import esbuild from 'esbuild'; import fs from 'fs'; const distDir = './dist'; if (!fs.existsSync(distDir)) fs.mkdirSync(distDir); esbuild.build({ entryPoints: ['./src/index.js'], bundle: true, outfile: './dist/index.js', format: 'modules', target: 'esnext', platform: 'browser', minify: true, }).catch(() => process.exit(1)); EOF # api/src/index.js mkdir -p "${STAGING_DIR}/api/src" cat > "${STAGING_DIR}/api/src/index.js" << 'EOF' import { Router } from 'itty-router'; const router = Router(); // CORS 헤더 const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; // 헬퍼 함수 const jsonResponse = (data, status = 200) => { return new Response(JSON.stringify(data), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); }; const errorResponse = (message, status = 400) => { return jsonResponse({ error: message }, status); }; // OPTIONS 요청 처리 router.options('*', () => new Response(null, { headers: corsHeaders })); // 1. 사주 계산 router.post('/api/saju/calculate', async (request) => { try { const { name, birthDate, birthTime, gender } = await request.json(); if (!birthDate || !birthTime) { return errorResponse('생년월일과 출생시간은 필수입니다.'); } const fourPillars = `${birthDate}T${birthTime}`; const dayMaster = '丙火'; // 예시 (실제로는 복잡한 계산) return jsonResponse({ name, birthDate, birthTime, gender, fourPillars, dayMaster, analysis: '사주 분석 내용이 여기에 표시됩니다.', }); } catch (error) { return errorResponse(`사주 계산 오류: ${error.message}`); } }); // 2. 기도력 계산 router.post('/api/prayer/calculate', async (request) => { try { const { name, birthDate, gender } = await request.json(); if (!birthDate) { return errorResponse('생년월일은 필수입니다.'); } // 기도력 계산 (0~100 범위) const force = Math.random() * 100; const gradeMap = [ { min: 90, max: 100, grade: 'S' }, { min: 80, max: 89, grade: 'A' }, { min: 70, max: 79, grade: 'B' }, { min: 60, max: 69, grade: 'C' }, { min: 40, max: 59, grade: 'D' }, { min: 0, max: 39, grade: 'F' }, ]; const grade = gradeMap.find(g => force >= g.min && force <= g.max)?.grade || 'F'; return jsonResponse({ name, force: force.toFixed(2), grade, breakdown: { discipline: (Math.random() * 20).toFixed(2), concentration: (Math.random() * 20).toFixed(2), compassion: (Math.random() * 20).toFixed(2), wisdom: (Math.random() * 20).toFixed(2), courage: (Math.random() * 20).toFixed(2), }, }); } catch (error) { return errorResponse(`기도력 계산 오류: ${error.message}`); } }); // 3. 카드 선택 router.post('/api/cards/select', async (request) => { try { const { prayerForce, prayerGrade } = await request.json(); const allCards = [ { cardName: '우주의 깨달음 (The Fool)', category: 'Major Arcana' }, { cardName: '마그의 손 (The Magician)', category: 'Major Arcana' }, { cardName: '여사제의 지혜 (The Priestess)', category: 'Major Arcana' }, { cardName: '창조의 황제녀 (The Empress)', category: 'Major Arcana' }, { cardName: '권력의 황제 (The Emperor)', category: 'Major Arcana' }, { cardName: '영혼의 유대 (The Hierophant)', category: 'Major Arcana' }, { cardName: '마음의 연 (The Lovers)', category: 'Major Arcana' }, { cardName: '생명의 수레 (The Chariot)', category: 'Major Arcana' }, { cardName: '강인함 (Strength)', category: 'Major Arcana' }, { cardName: '침묵의 은자 (The Hermit)', category: 'Major Arcana' }, { cardName: '운명의 수레바퀴 (Wheel of Fortune)', category: 'Major Arcana' }, { cardName: '정의 (Justice)', category: 'Major Arcana' }, { cardName: '희생 (The Hanged Man)', category: 'Major Arcana' }, { cardName: '변화 (Death)', category: 'Major Arcana' }, { cardName: '절제 (Temperance)', category: 'Major Arcana' }, { cardName: '어둠의 군주 (The Devil)', category: 'Major Arcana' }, { cardName: '마음의 탑 (The Tower)', category: 'Major Arcana' }, { cardName: '희망의 별 (The Star)', category: 'Major Arcana' }, { cardName: '꿈의 달 (The Moon)', category: 'Major Arcana' }, { cardName: '빛의 태양 (The Sun)', category: 'Major Arcana' }, { cardName: '판단 (Judgement)', category: 'Major Arcana' }, { cardName: '완성의 세계 (The World)', category: 'Major Arcana' }, { cardName: '식신 (食神)', category: 'TenStar' }, { cardName: '상관 (傷官)', category: 'TenStar' }, { cardName: '정재 (正財)', category: 'TenStar' }, { cardName: '편재 (偏財)', category: 'TenStar' }, { cardName: '정관 (正官)', category: 'TenStar' }, { cardName: '편관 (偏官)', category: 'TenStar' }, { cardName: '정인 (正印)', category: 'TenStar' }, { cardName: '편인 (偏印)', category: 'TenStar' }, { cardName: '비견 (比肩)', category: 'TenStar' }, { cardName: '겁재 (劫財)', category: 'TenStar' }, ]; // 7장 랜덤 선택 const selected = []; const shuffled = allCards.sort(() => Math.random() - 0.5); for (let i = 0; i < 7; i++) { selected.push(shuffled[i]); } return jsonResponse({ count: 7, prayerForce: parseFloat(prayerForce), grade: prayerGrade, cards: selected, }); } catch (error) { return errorResponse(`카드 선택 오류: ${error.message}`); } }); // 4. PPT 생성 router.post('/api/ppt/generate', async (request) => { try { const { name, saju, prayerForce, cards } = await request.json(); const filename = `${name || '사용자'}_타로결과_${Date.now()}.pptx`; return jsonResponse({ success: true, filename, downloadUrl: `/downloads/${filename}`, message: 'PPT가 생성되었습니다. (실제 PPT 생성은 Backend 필요)', }); } catch (error) { return errorResponse(`PPT 생성 오류: ${error.message}`); } }); // 5. 건강 체크 router.get('/api/health', () => { return jsonResponse({ status: 'OK', timestamp: new Date().toISOString(), environment: 'production', }); }); // 404 처리 router.all('*', () => { return errorResponse('엔드포인트를 찾을 수 없습니다.', 404); }); // Cloudflare Workers 진입점 export default { fetch: (request) => router.handle(request), }; EOF # api/.gitignore cat > "${STAGING_DIR}/api/.gitignore" << 'EOF' node_modules/ dist/ .env .env.local .env.*.local *.log .DS_Store .wrangler/ EOF echo -e "${GREEN}✓ API 서버 파일 생성 완료${NC}" echo "" # ============================================================================ # Step 4: 문서 및 설정 파일 # ============================================================================ echo -e "${GREEN}[Step 4] 문서 및 설정 파일 생성...${NC}" # README.md cat > "${STAGING_DIR}/README.md" << 'EOF' # 🔮 삼生調和 타로카드 (Samsa Harmony Tarot) 불교 기도력 · 사주 · 타로 통합 분석 시스템 ## 📋 프로젝트 개요 - **프로젝트명**: 삼生調和 타로카드 - **개발사**: 무진장 통합수행원 - **배포 플랫폼**: Cloudflare Pages (웹) + Cloudflare Workers (API) - **도메인**: `samsa-harmony.dev` (개발), `api.samsa-harmony.dev` (API) - **목표**: 웹·모바일 동시 출시 (2026년) ## 🛠 기술 스택 | 계층 | 기술 | |------|------| | 프론트엔드 | React 18 + Vite 5 + Axios | | API | Cloudflare Workers + itty-router | | 호스팅 | Cloudflare Pages (웹) + Workers (서버리스) | | 개발 | Node.js 18+, npm/yarn | ## 📦 프로젝트 구조