폰트 수정, UI수정 및 대시보드 수정

subtitle
김성경 2026-03-13 10:13:26 +09:00
parent 65ea09ffd5
commit 7aedf07203
9 changed files with 313 additions and 339 deletions

486
index.css
View File

@ -670,7 +670,7 @@
}
.sidebar-item-label {
font-size: var(--text-sm);
font-size: 16px;
font-weight: 700;
white-space: nowrap;
}
@ -802,7 +802,7 @@
position: relative;
z-index: 1;
padding: 4px 0;
font-size: 11px;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.03em;
color: rgba(255, 255, 255, 0.4);
@ -2083,7 +2083,7 @@
}
.comp2-title-row {
padding: 0 0 16px;
padding: 16px;
text-align: center;
flex-shrink: 0;
}
@ -2098,7 +2098,7 @@
}
.comp2-page-subtitle {
font-size: 14px;
font-size: 16px;
font-weight: 400;
color: #9BCACC;
line-height: 1.29;
@ -2313,7 +2313,7 @@
}
.comp2-info-label {
font-size: 12px;
font-size: 14px;
font-weight: 400;
color: #9BCACC;
line-height: 1.29;
@ -2362,7 +2362,7 @@
}
.comp2-filesize {
font-size: 12px;
font-size: 14px;
font-weight: 400;
color: #9BCACC;
line-height: 1.29;
@ -2386,9 +2386,9 @@
}
.comp2-meta-label {
font-size: 12px;
font-size: 16px;
font-weight: 400;
color: #E5F1F2;
color: #9BCACC;
line-height: 1.29;
}
@ -2406,7 +2406,7 @@
}
.comp2-lyrics-text {
font-size: 12px;
font-size: 16px;
font-weight: 400;
color: #E5F1F2;
line-height: 1.625;
@ -2500,7 +2500,7 @@
flex: 1;
height: 40px;
border-radius: 8px;
font-size: 12px;
font-size: 14px;
font-weight: 600;
letter-spacing: -0.006em;
line-height: 1.19;
@ -3269,15 +3269,19 @@
}
/* =====================================================
Brand Intelligence v2 ( )
Brand Intelligence v2
===================================================== */
.bi2-page {
display: flex;
width: 100%;
min-height: 100vh;
background: linear-gradient(to bottom, #002224, #01191a);
color: #E5F1F2;
padding-top: 64px;
padding-bottom: 160px;
flex-direction: column;
align-items: center;
background: linear-gradient(to bottom, #002224, #01191a);
color: #E5F1F2;
font-family: 'Pretendard', sans-serif;
overflow-x: auto;
}
@ -3333,11 +3337,22 @@
/* 타이틀 영역 */
.bi2-page-title-section {
display: flex;
max-width: 1440px;
padding: 68px 16px 50px 16px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 24px;
padding: 8px 0 40px;
align-self: stretch;
margin: 0 auto;
}
.bi2-page-divider {
height: 0;
width: calc(100% - 32px);
max-width: 1440px;
border: none;
border-top: 1px solid var(--Color-teal-500, #067C80);
margin: 0 auto;
}
@ -3357,39 +3372,44 @@
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
gap: 20px;
align-self: stretch;
}
.bi2-main-title {
font-size: 48px;
font-weight: 600;
color: #FFFFFF;
letter-spacing: -0.006em;
line-height: 1.19;
color: var(--Color-white, #FFF);
text-align: center;
font-family: Pretendard;
font-size: 48px;
font-style: normal;
font-weight: 600;
line-height: normal;
letter-spacing: -0.288px;
align-self: stretch;
}
.bi2-subtitle {
font-size: 14px;
font-size: 21px;
font-weight: 400;
color: #E5F1F2;
letter-spacing: -0.006em;
line-height: 1.19;
text-align: center;
align-self: stretch;
}
/* 메인 컨테이너 */
.bi2-main-container {
display: flex;
max-width: 1440px;
min-width: 1000px;
margin: 0 auto;
background: #013032;
border: 1px solid #034A4D;
border-radius: 40px;
padding: 32px 32px 112px;
display: flex;
padding: 68px 32px 112px 32px;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 88px;
align-self: stretch;
margin: 0 auto;
}
/* 매장 헤더 */
@ -3400,33 +3420,42 @@
}
.bi2-store-name {
font-size: 30px;
font-weight: 700;
color: #FFFFFF;
letter-spacing: -0.006em;
line-height: 1.3;
color: var(--Color-white, #FFF);
font-family: Pretendard;
font-size: 40px;
font-style: normal;
font-weight: 600;
line-height: 130%; /* 52px */
letter-spacing: -0.24px;
}
.bi2-store-address {
font-size: 14px;
font-weight: 400;
color: #9BCACC;
line-height: 1.29;
color: var(--Color-teal-200, #9BCACC);
font-family: Pretendard;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 22px; /* 137.5% */
letter-spacing: -0.096px;
}
/* 섹션 공통 */
.bi2-section {
display: flex;
flex-direction: column;
gap: 16px;
align-items: flex-start;
gap: 20px;
align-self: stretch;
}
.bi2-section-title {
font-size: 20px;
font-weight: 700;
color: #E5F1F2;
letter-spacing: -0.006em;
line-height: 1.3;
color: var(--Color-white, #FFF);
font-family: Pretendard;
font-size: 28px;
font-style: normal;
font-weight: 600;
line-height: 130%; /* 36.4px */
letter-spacing: -0.168px;
}
/* 브랜드 정체성 카드 */
@ -3454,25 +3483,34 @@
}
.bi2-label-sm {
font-size: 14px;
font-weight: 400;
color: #E5F1F2;
line-height: 1.29;
color: var(--Color-teal-50, #E5F1F2);
/* Body_600 */
font-family: Pretendard;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 22px; /* 137.5% */
letter-spacing: -0.096px;
}
.bi2-core-value {
color: var(--Color-mint-50, #F4FFFC);
font-family: Pretendard;
font-size: 24px;
font-style: normal;
font-weight: 700;
color: #F4FFFC;
letter-spacing: -0.006em;
line-height: 1.3;
line-height: 130%; /* 31.2px */
letter-spacing: -0.144px;
}
.bi2-category-text {
font-size: 14px;
font-weight: 400;
color: #E5F1F2;
line-height: 1.29;
color: var(--Color-teal-50, #E5F1F2);
font-family: Pretendard;
font-size: 18px;
font-style: normal;
font-weight: 600;
line-height: 22px; /* 122.222% */
letter-spacing: -0.108px;
}
.bi2-identity-divider {
@ -3494,23 +3532,26 @@
}
.bi2-body-text {
color: var(--Color-teal-100, #CEE5E6);
font-family: Pretendard;
font-size: 18px;
font-style: normal;
font-weight: 400;
color: #CEE5E6;
line-height: 1.6;
letter-spacing: -0.006em;
line-height: 160%; /* 28.8px */
letter-spacing: -0.108px;
}
/* 셀링 포인트 카드 */
.bi2-selling-card {
display: flex;
align-items: center;
padding: 16px 20px;
justify-content: center;
gap: clamp(20px, 4vw, 80px);
background: #034245;
border: 1px solid #034A4D;
align-items: center;
gap: 100px;
align-self: stretch;
border-radius: 20px;
padding: 16px 32px;
border: 1px solid var(--Color-teal-700, #034A4D);
background: var(--Color-teal-750, #01393B);
}
.bi2-selling-chart {
@ -3525,8 +3566,8 @@
@media (max-width: 768px) {
.bi2-radar-container svg {
width: min(95vw, 420px) !important;
height: min(95vw, 420px) !important;
width: min(95vw, 440px) !important;
height: min(95vw, 360px) !important;
}
.bi2-radar-label-text {
@ -3593,24 +3634,29 @@
display: flex;
align-items: stretch;
gap: 16px;
width: 100%;
}
.bi2-persona-card {
flex: 1;
background: #034245;
border: 1px solid #034A4D;
border-radius: 20px;
padding: 20px;
display: flex;
padding: 20px;
flex-direction: column;
align-items: stretch;
gap: 16px;
flex: 1 0 0;
align-self: stretch;
border-radius: 20px;
border: 1px solid var(--Color-teal-700, #034A4D);
background: var(--Color-teal-750, #01393B);
}
.bi2-persona-header {
display: flex;
flex-direction: column;
gap: 8px;
padding-bottom: 8px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
align-self: stretch;
}
.bi2-persona-info {
@ -3620,27 +3666,33 @@
}
.bi2-persona-name {
font-size: 18px;
color: var(--Color-teal-50, #E5F1F2);
font-family: Pretendard;
font-size: 20px;
font-style: normal;
font-weight: 700;
color: #E5F1F2;
letter-spacing: -0.006em;
line-height: 1.3;
line-height: 130%; /* 26px */
letter-spacing: -0.12px;
}
.bi2-persona-age {
color: var(--Color-teal-200, #9BCACC);
/* 14_600 */
font-family: Pretendard;
font-size: 14px;
font-weight: 400;
color: #9BCACC;
line-height: 1.29;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 128.571% */
}
.bi2-persona-desc {
color: var(--Color-teal-100, #CEE5E6);
/* 14_600 */
font-family: Pretendard;
font-size: 14px;
font-weight: 400;
color: #CEE5E6;
line-height: 1.29;
word-break: keep-all;
overflow-wrap: break-word;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 128.571% */
}
.bi2-persona-divider {
@ -3656,29 +3708,33 @@
}
.bi2-persona-detail.grow {
flex: 1;
display: flex;
padding: 0 4px;
align-items: flex-start;
gap: 4px;
align-self: stretch;
}
.bi2-label-xs {
color: var(--Color-teal-100, #CEE5E6);
font-family: Pretendard;
font-size: 14px;
font-weight: 400;
color: #CEE5E6;
line-height: 1.29;
white-space: nowrap;
flex-shrink: 0;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 128.571% */
}
.bi2-persona-detail-text {
font-size: 16px;
font-weight: 600;
color: #E5F1F2;
line-height: 26px;
letter-spacing: -0.006em;
white-space: pre-line;
text-align: right;
word-break: keep-all;
overflow-wrap: break-word;
flex: 1;
color: var(--Color-teal-50, #E5F1F2);
text-align: right;
font-family: Pretendard;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 26px;
letter-spacing: -0.096px;
white-space: pre-line;
}
/* 추천 타겟 키워드 */
@ -3689,16 +3745,23 @@
}
.bi2-keyword-subtitle {
font-size: 14px;
font-weight: 400;
color: #9BCACC;
line-height: 1.29;
color: var(--Color-teal-200, #9BCACC);
/* Body_600 */
font-family: Pretendard;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 22px; /* 137.5% */
letter-spacing: -0.096px;
}
.bi2-keyword-tags {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
align-content: flex-start;
gap: 8px;
align-self: stretch;
flex-wrap: wrap;
}
.bi2-keyword-pill {
@ -3768,8 +3831,8 @@
.bi2-main-container {
min-width: unset;
margin: 0 16px;
padding: 24px 20px 80px;
gap: 48px;
padding: 24px 0px 80px;
gap: 64px;
border-radius: 24px;
}
@ -3779,8 +3842,12 @@
}
.bi2-persona-grid {
display: flex;
flex-direction: column;
}
align-items: flex-start;
gap: 20px;
align-self: stretch;
}
.bi2-identity-bottom {
flex-direction: column;
@ -3788,11 +3855,18 @@
}
.bi2-main-title {
font-size: 28px;
}
color: var(--Color-white, #FFF);
text-align: center;
font-family: Pretendard;
font-size: 45px;
font-style: normal;
font-weight: 600;
line-height: normal;
letter-spacing: -0.288px;
}
.bi2-store-name {
font-size: 24px;
font-size: 32px;
}
.bi2-selling-name {
@ -5664,6 +5738,7 @@
/* Dashboard Container */
.dashboard-container {
width: 100%;
min-width: 375px;
padding: 0.75rem;
display: flex;
flex-direction: column;
@ -5704,35 +5779,17 @@
}
.dashboard-title {
font-size: var(--text-lg);
font-size: 32px;
font-weight: 700;
letter-spacing: -0.025em;
}
@media (min-width: 640px) {
.dashboard-title {
font-size: var(--text-xl);
}
}
@media (min-width: 768px) {
.dashboard-title {
font-size: 1.5rem;
}
}
.dashboard-description {
font-size: 15px;
font-size: 16px;
color: var(--color-text-gray-500);
margin-top: 0.125rem;
}
@media (min-width: 640px) {
.dashboard-description {
font-size: 15px;
}
}
/* Stats Grid */
.stats-grid {
flex-shrink: 0;
@ -5787,65 +5844,29 @@
}
.stat-label {
font-size: 8px;
font-size: 14px;
font-weight: 700;
color: var(--color-text-gray-400);
text-transform: uppercase;
letter-spacing: 0.1em;
}
@media (min-width: 640px) {
.stat-label {
font-size: 9px;
}
}
@media (min-width: 768px) {
.stat-label {
font-size: 10px;
}
}
.stat-value {
font-size: var(--text-lg);
font-size: 24px;
font-weight: 700;
color: var(--color-text-white);
line-height: 1.25;
}
@media (min-width: 640px) {
.stat-value {
font-size: var(--text-xl);
}
}
@media (min-width: 768px) {
.stat-value {
font-size: 1.5rem;
}
}
@media (min-width: 1024px) {
.stat-value {
font-size: var(--text-3xl);
}
}
.stat-trend {
display: inline-flex;
align-items: center;
gap: 2px;
font-size: 8px;
font-size: 12px;
color: var(--color-mint);
font-weight: 500;
}
@media (min-width: 640px) {
.stat-trend {
font-size: 9px;
}
}
/* Chart Card */
.chart-card {
flex: 1;
@ -5903,18 +5924,6 @@
letter-spacing: 0.1em;
}
@media (min-width: 640px) {
.chart-title {
font-size: 9px;
}
}
@media (min-width: 768px) {
.chart-title {
font-size: 10px;
}
}
.chart-legend {
display: flex;
gap: 0.375rem;
@ -5929,16 +5938,10 @@
}
.chart-legend-text {
font-size: 8px;
font-size: 14px;
color: var(--color-text-gray-400);
}
@media (min-width: 640px) {
.chart-legend-text {
font-size: 9px;
}
}
.chart-container {
flex: 1;
position: relative;
@ -5967,12 +5970,6 @@
box-shadow: 0 20px 25px -5px rgba(166, 255, 234, 0.2);
}
@media (min-width: 640px) {
.chart-badge-value {
font-size: 10px;
}
}
.chart-badge-line {
width: 2px;
height: 0.5rem;
@ -6034,15 +6031,15 @@
}
.dashboard-last-updated {
font-size: 8px;
font-size: 12px;
color: var(--color-text-gray-500);
display: none;
}
@media (min-width: 640px) {
@media (min-width: 768px) {
.dashboard-last-updated {
display: block;
font-size: 9px;
font-size: 12px;
}
}
@ -6112,19 +6109,12 @@
}
.dashboard-section-title {
font-size: var(--text-sm);
font-size: 24px;
font-weight: 600;
color: var(--color-text-white);
margin-bottom: 0.5rem;
}
@media (min-width: 768px) {
.dashboard-section-title {
font-size: var(--text-base);
margin-bottom: 0.75rem;
}
}
/* Chart Legend Dual (for YoY comparison) */
.chart-legend-dual {
display: flex;
@ -6189,7 +6179,7 @@
@media (min-width: 640px) {
.platform-tab {
padding: 0.5rem 1rem;
font-size: var(--text-sm);
/* font-size: var(--text-sm); */
gap: 0.5rem;
}
}
@ -6231,7 +6221,7 @@
padding: 0.375rem 0.875rem;
background-color: transparent;
color: var(--color-text-gray-400);
font-size: var(--text-sm);
font-size: 16px;
white-space: nowrap;
font-weight: 600;
cursor: pointer;
@ -6334,30 +6324,12 @@
gap: 0.25rem;
}
@media (min-width: 640px) {
.metric-card-value {
font-size: var(--text-2xl);
}
}
@media (min-width: 768px) {
.metric-card-value {
font-size: 1.75rem;
}
}
.metric-card-unit {
font-size: var(--text-xs);
color: var(--color-text-gray-500);
font-weight: 400;
}
@media (min-width: 640px) {
.metric-card-unit {
font-size: var(--text-sm);
}
}
.metric-card-trend {
display: flex;
align-items: center;
@ -6366,12 +6338,6 @@
font-weight: 500;
}
@media (min-width: 640px) {
.metric-card-trend {
font-size: var(--text-xs);
}
}
.metric-card-trend.up {
color: var(--color-mint);
}
@ -6394,9 +6360,10 @@
/* YoY Chart */
.yoy-chart-card {
min-width: 351px;
background-color: var(--color-bg-card);
border-radius: var(--radius-xl);
padding: 0.75rem;
padding: 0.5rem;
border: 1px solid var(--color-border-white-5);
box-shadow: var(--shadow-xl);
margin-bottom: 0.75rem;
@ -6695,14 +6662,14 @@
@media (min-width: 768px) {
.top-content-card {
padding: 1.5rem;
padding: 1rem;
}
}
.top-content-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: 0.5rem;
}
.top-content-item {
@ -6768,7 +6735,8 @@
}
.top-content-title {
font-size: var(--text-xs);
max-width: 215px;
font-size: 14px;
font-weight: 600;
color: var(--color-text-white);
white-space: nowrap;
@ -6776,12 +6744,6 @@
text-overflow: ellipsis;
}
@media (min-width: 640px) {
.top-content-title {
font-size: var(--text-sm);
}
}
.top-content-stats {
display: flex;
gap: 0.75rem;
@ -6791,31 +6753,19 @@
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 10px;
font-size: 12px;
color: var(--color-text-gray-400);
}
@media (min-width: 640px) {
.top-content-stat {
font-size: var(--text-xs);
}
}
.top-content-stat svg {
color: var(--color-text-gray-500);
}
.top-content-date {
font-size: 9px;
font-size: 12px;
color: var(--color-text-gray-500);
}
@media (min-width: 640px) {
.top-content-date {
font-size: 10px;
}
}
/* Audience Section */
.audience-section {
margin-bottom: 0.75rem;
@ -6856,7 +6806,7 @@
}
.audience-card-title {
font-size: var(--text-sm);
font-size: 20px;
font-weight: 600;
color: var(--color-text-gray-300);
margin-bottom: 1rem;
@ -6876,7 +6826,7 @@
}
.audience-bar-label {
font-size: var(--text-xs);
font-size: 14px;
color: var(--color-text-gray-400);
width: 60px;
flex-shrink: 0;
@ -6898,7 +6848,7 @@
}
.audience-bar-value {
font-size: var(--text-xs);
font-size: 14px;
color: var(--color-text-white);
width: 36px;
text-align: right;
@ -6949,7 +6899,7 @@
display: flex;
align-items: center;
gap: 0.375rem;
font-size: var(--text-xs);
font-size: 14px;
color: var(--color-text-gray-400);
}
@ -7620,6 +7570,7 @@
align-items: center;
gap: 1.25rem;
flex-shrink: 0;
font-size: 14px;
}
.sound-column > *:not(.btn-generate-sound):not(.error-message-new):not(.status-message-new) {
@ -7674,7 +7625,7 @@
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
color: var(--color-text-white);
font-size: var(--text-sm);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-normal);
@ -7959,7 +7910,7 @@
background: transparent;
border: none;
color: var(--color-text-white);
font-size: var(--text-sm);
font-size: 16px;
font-family: inherit;
resize: none;
outline: none;
@ -8097,7 +8048,7 @@
}
/* Responsive Styles for Sound Studio */
@media (max-width: 639px) {
@media (min-width: 768px) {
.progress-indicator {
width: 100%;
}
@ -8108,7 +8059,7 @@
.sound-type-btn {
padding: 0.75rem 0.5rem;
font-size: var(--text-xs);
font-size: 14px;
}
.genre-row {
@ -8117,7 +8068,7 @@
.genre-btn {
padding: 0.5rem 0.75rem;
font-size: var(--text-xs);
font-size: 14px;
}
}
@ -8143,6 +8094,7 @@
flex: 1;
width: auto;
min-height: 0;
font-size: 14px;
}
.lyrics-column {
@ -9593,7 +9545,7 @@
}
.social-posting-title {
font-size: 1.125rem;
font-size: 24px;
font-weight: 600;
color: #FFFFFF;
margin: 0;
@ -9726,7 +9678,7 @@
}
.social-posting-video-specs {
font-size: 0.75rem;
font-size: 14px;
color: rgba(255, 255, 255, 0.5);
margin: 0;
}
@ -9739,7 +9691,7 @@
}
.social-posting-label {
font-size: 0.8rem;
font-size: 16px;
font-weight: 500;
color: rgba(255, 255, 255, 0.8);
}
@ -9757,7 +9709,7 @@
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: #FFFFFF;
font-size: 0.875rem;
font-size: 14px;
font-family: 'Pretendard', sans-serif;
transition: border-color 0.2s;
}
@ -9836,7 +9788,7 @@
}
.social-posting-radio .radio-label {
font-size: 0.85rem;
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
}

View File

@ -8,10 +8,11 @@ const Footer: React.FC = () => {
<footer className="landing-footer">
<div className="footer-content">
<div className="footer-left">
<img src="/assets/images/logo.svg" alt="CASTAD" className="footer-logo" />
<img src="/assets/images/ado2-sidebar-logo.svg" alt="ADO2" className="footer-logo" />
<p className="footer-copyright">Copyright O2O Inc. All rights reserved</p>
</div>
<div className="footer-right">
<p className="footer-info">{t('footer.company')}</p>
<p className="footer-info">{t('footer.businessNumber')}</p>
<p className="footer-info">{t('footer.headquarters')}</p>
<p className="footer-info">{t('footer.researchCenter')}</p>

View File

@ -132,6 +132,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false);
const [isLoadingAutoDescription, setIsLoadingAutoDescription] = useState(false);
const [isHorizontalVideo, setIsHorizontalVideo] = useState(false);
const [videoMeta, setVideoMeta] = useState<{ width: number; height: number; duration: number } | null>(null);
const channelDropdownRef = useRef<HTMLDivElement>(null);
const privacyDropdownRef = useRef<HTMLDivElement>(null);
@ -175,12 +176,6 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
if (isOpen) {
loadSocialAccounts();
loadAutocomplete();
// // 비디오 정보로 기본 제목 설정
// if (video) {
// const date = new Date(video.created_at);
// const formattedDate = `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
// setTitle(`${video.store_name} ${formattedDate}`);
// }
}
}, [isOpen, video]);
@ -436,6 +431,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
onLoadedMetadata={(e) => {
const v = e.currentTarget;
setIsHorizontalVideo(v.videoWidth > v.videoHeight);
setVideoMeta({ width: v.videoWidth, height: v.videoHeight, duration: v.duration });
}}
/>
</div>
@ -453,7 +449,11 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
<p className="social-posting-video-title">
{video.store_name} {new Date(video.created_at).toLocaleString('ko-KR')}
</p>
<p className="social-posting-video-specs">{t('social.videoSpecs')}</p>
<p className="social-posting-video-specs">
{videoMeta
? `${videoMeta.width}×${videoMeta.height} · ${Math.floor(videoMeta.duration / 60)}:${String(Math.floor(videoMeta.duration % 60)).padStart(2, '0')}`
: t('social.videoSpecs')}
</p>
</div>
{/* Channel Selector - Custom Dropdown */}

View File

@ -16,6 +16,7 @@
"logout": "Log Out"
},
"footer": {
"company":"O2O Inc.",
"businessNumber": "Business Registration No. : 620-87-00810 | CEO : Ahn Sungmin",
"headquarters": "HQ : 41593 Unicorn Lab Daegu A05, 5F, 111 Oksan-ro, Buk-gu, Daegu, Korea",
"researchCenter": "R&D : 13453 Rooms 504-505 (East), KT Pangyo Bldg, 32 Geumto-ro, Sujeong-gu, Seongnam-si, Gyeonggi-do, Korea",
@ -25,7 +26,7 @@
"social": {
"title": "Social Media Posting",
"postNumber": "Post 1",
"videoSpecs": "1080x1920 · 10 sec",
"videoSpecs": "720x1280 · 1:01",
"channelLabel": "Post Channel",
"loadingAccounts": "Loading accounts...",
"noAccounts": "No connected social accounts.",

View File

@ -16,6 +16,7 @@
"logout": "로그아웃"
},
"footer": {
"company":"㈜에이아이오투오",
"businessNumber": "사업자 등록번호 : 620-87-00810 | 대표 : 안성민",
"headquarters": "본사 : 41593 대구광역시 북구 옥산로 111, 5층 유니콘랩 대구 A05호",
"researchCenter": "연구소 : 13453 경기 성남시 수정구 금토로 32 (금토동) (주)KT 판교빌딩 504호~505호 (East)",
@ -25,7 +26,7 @@
"social": {
"title": "소셜 미디어 포스팅",
"postNumber": "게시물 1",
"videoSpecs": "1080x1920 · 10초",
"videoSpecs": "720x1280 · 1:01",
"channelLabel": "게시 채널",
"loadingAccounts": "계정 로딩 중...",
"noAccounts": "연결된 소셜 계정이 없습니다.",
@ -345,11 +346,11 @@
"conceptScalability": "컨셉 확장성",
"noInfo": "정보 없음",
"marketPositioning": "시장 포지셔닝",
"coreValue": "핵심 가치 (Core Value)",
"coreValue": "핵심 가치",
"categoryDefinition": "카테고리 정의",
"targetPersona": "타겟 페르소나",
"targetPersona": "주요 고객 유형",
"ageSuffix": "세",
"sellingPoints": "주요 셀링 포인트 (USP)",
"sellingPoints": "주요 셀링 포인트",
"recommendedKeywords": "추천 타겟 키워드",
"generateContent": "콘텐츠 생성",
"pageDescBefore": "을 통해 도출된 ",

View File

@ -48,6 +48,8 @@ const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, o
</div>
</div>
<hr className="bi2-page-divider" />
{/* Main Content Container */}
<div className="bi2-main-container">
{/* 매장명 & 주소 */}
@ -130,9 +132,11 @@ const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, o
<div className="bi2-persona-divider"></div>
<div className="bi2-persona-detail grow">
<span className="bi2-label-xs">{t('analysis.favorKeywords', { defaultValue: '선호 키워드' })}</span>
<p className="bi2-persona-detail-text">
{persona.favor_target.join('\n')}
</p>
<div className="bi2-persona-detail-text">
{persona.favor_target.map((item, i) => (
<p key={i}>{item}</p>
))}
</div>
</div>
<div className="bi2-persona-divider"></div>
<div className="bi2-persona-detail">

View File

@ -76,11 +76,27 @@ export const GeometricChart: React.FC<GeometricChartProps> = ({ data }) => {
style={{ overflow: 'visible', width: '440px', height: '440px' }}
>
<defs>
<filter id="radar-glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="4" result="coloredBlur" />
{/* 레이더 폴리곤 fill: radial gradient */}
<radialGradient id="radar-fill-gradient" cx="280" cy="280" r="160" fx="280" fy="280" gradientUnits="userSpaceOnUse">
<stop offset="60%" stopColor="#034245" stopOpacity="0" />
<stop offset="100%" stopColor="#94FBE0" stopOpacity="0.30" />
</radialGradient>
{/* 레이더 폴리곤 filter: outer drop-shadow + inset glow */}
<filter id="radar-polygon-filter" x="-30%" y="-30%" width="160%" height="160%">
{/* Outer drop shadow */}
<feDropShadow dx="0" dy="0" stdDeviation="6" floodColor="#94FBE0" floodOpacity="0.5" result="dropShadow" />
{/* Inset glow: invert alpha → blur → composite inside */}
<feComponentTransfer in="SourceAlpha" result="invertedAlpha">
<feFuncA type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<feGaussianBlur in="invertedAlpha" stdDeviation="6" result="blurredOuter" />
<feComposite in="blurredOuter" in2="SourceAlpha" operator="in" result="insetMask" />
<feFlood floodColor="#94FBE0" floodOpacity="0.5" result="insetColor" />
<feComposite in="insetColor" in2="insetMask" operator="in" result="insetGlow" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="dropShadow" />
<feMergeNode in="SourceGraphic" />
<feMergeNode in="insetGlow" />
</feMerge>
</filter>
<filter id="badge-glow" x="-50%" y="-50%" width="200%" height="200%">
@ -118,11 +134,11 @@ export const GeometricChart: React.FC<GeometricChartProps> = ({ data }) => {
{/* 데이터 폴리곤 */}
<polygon
points={dataPolygon}
fill="rgba(45, 212, 191, 0.15)"
stroke="#2DD4BF"
fill="url(#radar-fill-gradient)"
stroke="var(--Color-mint-500, #94FBE0)"
strokeWidth="2"
strokeLinejoin="round"
filter="url(#radar-glow)"
filter="url(#radar-polygon-filter)"
/>
{/* 데이터 포인트 */}
@ -132,8 +148,8 @@ export const GeometricChart: React.FC<GeometricChartProps> = ({ data }) => {
cx={point.x}
cy={point.y}
r="4"
fill="#2DD4BF"
stroke="#1A1A2E"
fill="#ffffff"
stroke="#ffffff"
strokeWidth="2"
/>
))}
@ -145,7 +161,7 @@ export const GeometricChart: React.FC<GeometricChartProps> = ({ data }) => {
const rank = rankMap.get(i) || i + 1;
const isTopThree = rank <= 3;
const isLeftSide = Math.cos(angle) < -0.1;
const textX = isLeftSide ? -14 : 14;
const textX = isLeftSide ? -20 : 20;
const textAnchor: 'start' | 'end' = isLeftSide ? 'end' : 'start';
return (

View File

@ -385,7 +385,7 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
<div className="comp2-info-content">
<div className="comp2-file-info">
<h3 className="comp2-filename">{getFileName()}</h3>
<p className="comp2-filesize">19.6MB</p>
{/* <p className="comp2-filesize">19.6MB</p> */}
</div>
<div className="comp2-meta-grid">
<div className="comp2-meta-item">

View File

@ -618,21 +618,22 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
// 블러 조건: 1)계정 미연결 2)업로드 영상 없음 3)데이터 없음
const isBlurred = accounts.length === 0 || isEmptyState || !dashboardData;
// API 데이터 우선 사용, 없거나 영상 없음(isEmptyState) 시 Mock 데이터로 폴백
const contentMetrics = (!isEmptyState && dashboardData?.contentMetrics?.length) ? dashboardData.contentMetrics : MOCK_CONTENT_METRICS;
const topContent = (!isEmptyState && dashboardData?.topContent?.length) ? dashboardData.topContent : MOCK_TOP_CONTENT;
const hasRealAgeGroups = !isEmptyState && !!dashboardData?.audienceData?.ageGroups?.some(g => g.percentage > 0);
const hasRealGender = !isEmptyState && ((dashboardData?.audienceData?.gender?.male ?? 0) + (dashboardData?.audienceData?.gender?.female ?? 0)) > 0;
const hasRealTopRegions = !isEmptyState && !!dashboardData?.audienceData?.topRegions?.some(r => r.percentage > 0);
// showMockData=true면 전체 mock 강제, 아니면 API 우선 / isEmptyState 시 mock 폴백
const useReal = !showMockData && !isEmptyState;
const contentMetrics = (useReal && dashboardData?.contentMetrics?.length) ? dashboardData.contentMetrics : MOCK_CONTENT_METRICS;
const topContent = (useReal && dashboardData?.topContent?.length) ? dashboardData.topContent : MOCK_TOP_CONTENT;
const hasRealAgeGroups = useReal && !!dashboardData?.audienceData?.ageGroups?.some(g => g.percentage > 0);
const hasRealGender = useReal && ((dashboardData?.audienceData?.gender?.male ?? 0) + (dashboardData?.audienceData?.gender?.female ?? 0)) > 0;
const hasRealTopRegions = useReal && !!dashboardData?.audienceData?.topRegions?.some(r => r.percentage > 0);
const hasRealAudienceData = hasRealAgeGroups && hasRealGender && hasRealTopRegions;
const audienceData = hasRealAudienceData ? dashboardData!.audienceData : MOCK_AUDIENCE_DATA;
// mode별 차트 데이터를 ChartDataPoint 통합 형식으로 변환
const chartData: ChartDataPoint[] = mode === 'month'
? ((!isEmptyState && dashboardData?.monthlyData?.length)
? ((useReal && dashboardData?.monthlyData?.length)
? dashboardData.monthlyData.map((d: MonthlyData) => ({ label: d.month, current: d.thisYear, previous: d.lastYear }))
: MOCK_MONTHLY_DATA.map((d: MonthlyData) => ({ label: d.month, current: d.thisYear, previous: d.lastYear })))
: ((!isEmptyState && dashboardData?.dailyData?.length)
: ((useReal && dashboardData?.dailyData?.length)
? dashboardData.dailyData.map((d: DailyData) => ({ label: d.date, current: d.thisPeriod, previous: d.lastPeriod }))
: MOCK_DAILY_DATA.map((d: DailyData) => ({ label: d.date, current: d.thisPeriod, previous: d.lastPeriod })));
@ -657,8 +658,8 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
<path d="M15 10l4.553-2.069A1 1 0 0121 8.87v6.26a1 1 0 01-1.447.899L15 14M3 8a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V8z" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<div>
<p style={{ color: '#a6ffea', fontWeight: 600, marginBottom: '2px' }}> .</p>
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: '13px' }}>ADO2 .</p>
<p style={{ color: '#a6ffea', fontSize: '20px', fontWeight: 600, marginBottom: '2px' }}> .</p>
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: '16px' }}>ADO2 .</p>
</div>
</div>
</div>
@ -786,7 +787,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="dashboard-section-title" style={{ marginBottom: '2px' }}>{t('dashboard.contentPerformance')}</h2>
<p style={{ fontSize: '12px', color: 'rgba(255,255,255,0.4)', margin: 0 }}>ADO2 30 .</p>
<p style={{ fontSize: '14px', color: 'rgba(255,255,255,0.4)', margin: 0 }}>ADO2 30 .</p>
</div>
<div className="mode-toggle">
<button
@ -860,7 +861,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
<AnimatedSection delay={1000} className="audience-section">
<div style={{ marginBottom: '16px' }}>
<h2 className="dashboard-section-title" style={{ marginBottom: '2px' }}>{t('dashboard.audienceInsights')}</h2>
<p style={{ fontSize: '12px', color: 'rgba(255,255,255,0.4)', margin: 0 }}> .</p>
<p style={{ fontSize: '14px', color: 'rgba(255,255,255,0.4)', margin: 0 }}> .</p>
</div>
<div style={{ position: 'relative' }}>
<div className="audience-cards" style={!hasRealAudienceData && !showMockData ? { filter: 'blur(4px)', pointerEvents: 'none', userSelect: 'none' } : {}}>
@ -883,7 +884,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
</div>
</AnimatedItem>
</div>
{dashboardData && !isEmptyState && !hasRealAudienceData && (
{dashboardData && !isEmptyState && !hasRealAudienceData && !showMockData && (
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px',
@ -892,7 +893,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
borderRadius: '12px',
padding: '20px 28px',
}}>
<p style={{ color: 'rgba(255,255,255,0.85)', fontSize: '14px', fontWeight: 500, margin: 0, textAlign: 'center' }}>
<p style={{ color: 'rgba(255,255,255,0.85)', fontSize: '16px', fontWeight: 500, margin: 0, textAlign: 'center' }}>
.
</p>
</div>
@ -901,38 +902,36 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
</div>
</AnimatedSection>
{/* 개발자 모드 전용: mock 데이터 블러 해제 버튼 */}
{isTestPage && isEmptyState && (
<button
onClick={() => setShowMockData(prev => !prev)}
style={{
position: 'fixed',
bottom: '24px',
right: '24px',
zIndex: 9999,
display: 'flex',
alignItems: 'center',
gap: '6px',
background: showMockData ? 'rgba(255,100,100,0.15)' : 'rgba(166,255,234,0.12)',
border: `1px solid ${showMockData ? 'rgba(255,100,100,0.4)' : 'rgba(166,255,234,0.4)'}`,
borderRadius: '10px',
color: showMockData ? 'rgba(255,150,150,0.9)' : '#a6ffea',
padding: '8px 14px',
fontSize: '12px',
fontWeight: 600,
cursor: 'pointer',
backdropFilter: 'blur(8px)',
}}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
{showMockData
? <><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></>
: <><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></>
}
</svg>
{showMockData ? 'Mock 숨기기' : 'Mock 보기'} [DEV]
</button>
)}
{/* mock 데이터 보기 버튼 */}
<button
onClick={() => setShowMockData(prev => !prev)}
style={{
position: 'fixed',
bottom: '24px',
right: '24px',
zIndex: 9999,
display: 'flex',
alignItems: 'center',
gap: '6px',
background: showMockData ? 'rgba(255,100,100,0.15)' : 'rgba(166,255,234,0.12)',
border: `1px solid ${showMockData ? 'rgba(255,100,100,0.4)' : 'rgba(166,255,234,0.4)'}`,
borderRadius: '10px',
color: showMockData ? 'rgba(255,150,150,0.9)' : '#a6ffea',
padding: '8px 14px',
fontSize: '12px',
fontWeight: 600,
cursor: 'pointer',
backdropFilter: 'blur(8px)',
}}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
{showMockData
? <><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></>
: <><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></>
}
</svg>
{showMockData ? 'Sample 숨기기' : 'Sample 보기'}
</button>
</div>
);
};