diff --git a/index.css b/index.css index dbea805..227d64b 100644 --- a/index.css +++ b/index.css @@ -2049,19 +2049,23 @@ display: flex; align-items: center; gap: 4px; - padding: 8px 20px 8px 8px; - background: #462E64; + background-color: #462E64; + color: #CFABFB; + font-size: 0.875rem; + font-weight: 600; + padding: 0 1.25rem 0 0.5rem; + height: 36px; border: 1px solid #694596; border-radius: 999px; - color: #CFABFB; - font-size: 14px; - font-weight: 600; cursor: pointer; - transition: opacity 0.2s; + white-space: nowrap; + transition: background-color 0.2s; + line-height: 1.19; + letter-spacing: -0.006em; } .comp2-back-btn:hover { - opacity: 0.85; + background-color: #5a3a80; } .comp2-container { @@ -3270,18 +3274,40 @@ .bi2-page { min-height: 100vh; - background-color: #002224; + background: linear-gradient(to bottom, #002224, #01191a); color: #E5F1F2; + padding-top: 64px; padding-bottom: 160px; font-family: 'Pretendard', sans-serif; + overflow-x: auto; } /* 헤더 */ .bi2-header { + position: fixed; + top: 0; + left: 0; + right: 0; display: flex; align-items: center; - padding: 8px 32px; + justify-content: flex-start; + padding: 8px 1rem; height: 64px; + z-index: 30; +} + +@media (min-width: 768px) { + .bi2-header { + padding: 8px 2rem; + } + + body:has(.sidebar.expanded) .bi2-header { + left: 15rem; + } + + body:has(.sidebar.collapsed) .bi2-header { + left: 5rem; + } } .bi2-back-btn { @@ -3316,13 +3342,14 @@ } .bi2-title-icon { - width: 40px; - height: 40px; + width: 80px; + height: 80px; } .bi2-star-icon { - width: 40px; - height: 40px; + width: 80px; + height: 80px; + aspect-ratio: 1/1; animation: twinkle 2.5s ease-in-out infinite; } @@ -3334,7 +3361,7 @@ } .bi2-main-title { - font-size: 40px; + font-size: 48px; font-weight: 600; color: #FFFFFF; letter-spacing: -0.006em; @@ -3354,6 +3381,7 @@ /* 메인 컨테이너 */ .bi2-main-container { max-width: 1440px; + min-width: 1000px; margin: 0 auto; background: #013032; border: 1px solid #034A4D; @@ -3404,11 +3432,13 @@ /* 브랜드 정체성 카드 */ .bi2-identity-card { background: #034245; + border: 1px solid #94FBE0; border-radius: 20px; padding: 22px 24px; display: flex; flex-direction: column; gap: 22px; + box-shadow: 0 0 40px rgba(148, 251, 224, 0.1); } .bi2-identity-top { @@ -3433,7 +3463,7 @@ .bi2-core-value { font-size: 24px; font-weight: 700; - color: #FFFFFF; + color: #F4FFFC; letter-spacing: -0.006em; line-height: 1.3; } @@ -3464,10 +3494,10 @@ } .bi2-body-text { - font-size: 16px; + font-size: 18px; font-weight: 400; color: #CEE5E6; - line-height: 1.625; + line-height: 1.6; letter-spacing: -0.006em; } @@ -3476,7 +3506,7 @@ display: flex; align-items: center; justify-content: center; - gap: 160px; + gap: clamp(20px, 4vw, 80px); background: #034245; border: 1px solid #034A4D; border-radius: 20px; @@ -3493,6 +3523,17 @@ align-items: center; } +@media (max-width: 768px) { + .bi2-radar-container svg { + width: min(95vw, 420px) !important; + height: min(95vw, 420px) !important; + } + + .bi2-radar-label-text { + display: none; + } +} + .bi2-selling-list { display: flex; flex-direction: column; @@ -3532,19 +3573,19 @@ } .bi2-selling-name { - font-size: 16px; - font-weight: 400; + font-size: 20px; + font-weight: 600; color: #E5F1F2; letter-spacing: -0.006em; line-height: 1.375; } .bi2-selling-desc { - font-size: 12px; - font-weight: 500; + font-size: 14px; + font-weight: 600; color: #9BCACC; letter-spacing: -0.006em; - line-height: 1; + line-height: 1.29; } /* 고객 유형 카드 */ @@ -3629,9 +3670,9 @@ .bi2-persona-detail-text { font-size: 16px; - font-weight: 400; + font-weight: 600; color: #E5F1F2; - line-height: 1.625; + line-height: 26px; letter-spacing: -0.006em; white-space: pre-line; text-align: right; @@ -3665,18 +3706,67 @@ align-items: center; justify-content: center; padding: 8px 16px; - background: #034245; + background: #01393B; border-radius: 999px; - font-size: 16px; - font-weight: 400; + font-size: 20px; + font-weight: 600; color: #E5F1F2; letter-spacing: -0.006em; line-height: 1.375; } -/* 반응형 */ -@media (max-width: 1024px) { +/* 하단 고정 버튼 */ +.bi2-bottom-button-container { + position: fixed; + bottom: 32px; + left: 0; + right: 0; + display: flex; + justify-content: center; + z-index: 50; + pointer-events: none; +} + +@media (min-width: 768px) { + body:has(.sidebar.expanded) .bi2-bottom-button-container { + left: 15rem; + } + + body:has(.sidebar.collapsed) .bi2-bottom-button-container { + left: 5rem; + } +} + +.bi2-generate-btn { + pointer-events: auto; + display: flex; + align-items: center; + gap: 8px; + padding: 12px 48px; + background: #A65EFF; + color: #FFFFFF; + font-size: 16px; + font-weight: 700; + border: none; + border-radius: 999px; + cursor: pointer; + box-shadow: 0 4px 24px rgba(174, 114, 249, 0.4); + transition: background-color 0.2s, transform 0.2s; +} + +.bi2-generate-btn:hover { + background: #8B3FE8; + transform: scale(1.05); +} + +/* 반응형 – 태블릿/모바일 (≤768px) */ +@media (max-width: 768px) { + .bi2-page { + padding-top: 64px; + } + .bi2-main-container { + min-width: unset; margin: 0 16px; padding: 24px 20px 80px; gap: 48px; @@ -3704,6 +3794,14 @@ .bi2-store-name { font-size: 24px; } + + .bi2-selling-name { + font-size: 16px; + } + + .bi2-keyword-pill { + font-size: 16px; + } } /* ===================================================== @@ -7857,7 +7955,7 @@ .lyrics-textarea { width: 100%; - height: 200px; + min-height: 200px; background: transparent; border: none; color: var(--color-text-white); diff --git a/public/assets/images/star-icon(old).svg b/public/assets/images/star-icon(old).svg new file mode 100644 index 0000000..a40bfa9 --- /dev/null +++ b/public/assets/images/star-icon(old).svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/images/star-icon.svg b/public/assets/images/star-icon.svg index a40bfa9..9f104a3 100644 --- a/public/assets/images/star-icon.svg +++ b/public/assets/images/star-icon.svg @@ -1,3 +1,21 @@ - - + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/Analysis/AnalysisResultSection.tsx b/src/pages/Analysis/AnalysisResultSection.tsx index 5ce9ea0..3e63ded 100755 --- a/src/pages/Analysis/AnalysisResultSection.tsx +++ b/src/pages/Analysis/AnalysisResultSection.tsx @@ -1,7 +1,8 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import { CrawlingResponse, TargetPersona, SellingPoint } from '../../types/api'; +import { CrawlingResponse, TargetPersona } from '../../types/api'; +import { GeometricChart } from './GeometricChart'; interface AnalysisResultSectionProps { onBack: () => void; @@ -9,200 +10,9 @@ interface AnalysisResultSectionProps { data: CrawlingResponse; } -// 레이더 차트 컴포넌트 -interface RadarChartProps { - data: SellingPoint[]; - size?: number; -} - -const RadarChart: React.FC = ({ data, size = 360 }) => { - const [animatedScores, setAnimatedScores] = useState(data.map(() => 0)); - const [isAnimating, setIsAnimating] = useState(true); - - useEffect(() => { - const targetScores = data.map(item => item.score); - const duration = 1500; - const startTime = Date.now(); - - const animate = () => { - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / duration, 1); - const easeProgress = 1 - Math.pow(1 - progress, 3); - - const newScores = targetScores.map(target => target * easeProgress); - setAnimatedScores(newScores); - - if (progress < 1) { - requestAnimationFrame(animate); - } else { - setIsAnimating(false); - } - }; - - requestAnimationFrame(animate); - }, [data]); - - const padding = 80; - const center = size / 2 + padding; - const maxRadius = size / 2 - 20; - const levels = 5; - const angleStep = (2 * Math.PI) / data.length; - - const getPoint = (index: number, score: number) => { - const angle = angleStep * index - Math.PI / 2; - const radius = (score / 100) * maxRadius; - return { - x: center + radius * Math.cos(angle), - y: center + radius * Math.sin(angle), - }; - }; - - const dataPoints = animatedScores.map((score, i) => getPoint(i, score)); - const dataPath = dataPoints.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`)).join(' ') + ' Z'; - - const levelPaths = Array.from({ length: levels }, (_, levelIndex) => { - const levelRadius = ((levelIndex + 1) / levels) * maxRadius; - const points = data.map((_, i) => { - const angle = angleStep * i - Math.PI / 2; - return { - x: center + levelRadius * Math.cos(angle), - y: center + levelRadius * Math.sin(angle), - }; - }); - return points.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`)).join(' ') + ' Z'; - }); - - const axisLines = data.map((_, i) => { - const angle = angleStep * i - Math.PI / 2; - const endX = center + maxRadius * Math.cos(angle); - const endY = center + maxRadius * Math.sin(angle); - return `M ${center} ${center} L ${endX} ${endY}`; - }); - - const svgSize = size + padding * 2; - - // 순위별로 정렬하여 라벨 위치에 순위 번호 배치 - const sortedIndices = [...data] - .map((item, i) => ({ index: i, score: item.score })) - .sort((a, b) => b.score - a.score) - .map((item, rank) => ({ ...item, rank: rank + 1 })); - - const rankMap = new Map(); - sortedIndices.forEach(item => rankMap.set(item.index, item.rank)); - - return ( -
- - {levelPaths.map((path, i) => ( - - ))} - - {axisLines.map((path, i) => ( - - ))} - - - - {dataPoints.map((point, i) => ( - - ))} - - {data.map((item, i) => { - const angle = angleStep * i - Math.PI / 2; - const labelRadius = maxRadius + 40; - const pos = { - x: center + labelRadius * Math.cos(angle), - y: center + labelRadius * Math.sin(angle), - }; - const rank = rankMap.get(i) || i + 1; - const isTopThree = rank <= 3; - - return ( - - - - - {rank} - - - {item.korean_category} - - - - ); - })} - -
- ); -}; - const AnalysisResultSection: React.FC = ({ onBack, onGenerate, data }) => { const { t } = useTranslation(); const { processed_info, marketing_analysis } = data; - const containerRef = useRef(null); - const [buttonPosition, setButtonPosition] = useState({ left: 0, width: 0 }); const brandIdentity = marketing_analysis?.brand_identity; const marketPositioning = marketing_analysis?.market_positioning; @@ -213,26 +23,6 @@ const AnalysisResultSection: React.FC = ({ onBack, o // 셀링 포인트를 score 내림차순으로 정렬 const sortedSellingPoints = [...sellingPoints].sort((a, b) => b.score - a.score); - useEffect(() => { - const updateButtonPosition = () => { - if (containerRef.current) { - const rect = containerRef.current.getBoundingClientRect(); - setButtonPosition({ left: rect.left, width: rect.width }); - } - }; - - updateButtonPosition(); - window.addEventListener('resize', updateButtonPosition); - - const observer = new MutationObserver(updateButtonPosition); - observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['class'] }); - - return () => { - window.removeEventListener('resize', updateButtonPosition); - observer.disconnect(); - }; - }, []); - return (
{/* Header */} @@ -253,13 +43,13 @@ const AnalysisResultSection: React.FC = ({ onBack, o

{t('analysis.pageTitle')}

- {t('analysis.pageDescHighlight')}{t('analysis.pageDescBefore')}{processed_info?.customer_name || t('analysis.defaultBrandName')}{t('analysis.pageDescAfter')} + {t('analysis.pageDescHighlight')}{t('analysis.pageDescBefore')}{processed_info?.customer_name || t('analysis.defaultBrandName')}{t('analysis.pageDescAfter')}

{/* Main Content Container */} -
+
{/* 매장명 & 주소 */}

{processed_info?.customer_name || t('analysis.brandNameFallback')}

@@ -298,7 +88,7 @@ const AnalysisResultSection: React.FC = ({ onBack, o {/* 레이더 차트 */}
{sellingPoints.length > 0 && ( - + )}
{/* 셀링 포인트 리스트 */} @@ -369,13 +159,10 @@ const AnalysisResultSection: React.FC = ({ onBack, o
{/* 콘텐츠 생성 버튼 */} -
+
- - {errorMessage && ( -
- {errorMessage} -
- )} - - {isGenerating && statusMessage && ( + {/* Generate Button / Status Message (교체) */} + {isGenerating && statusMessage ? (
@@ -551,6 +527,20 @@ const SoundStudioContent: React.FC = ({ {statusMessage}
+ ) : ( + + )} + + {errorMessage && ( +
+ {errorMessage} +
)}