Compare commits
No commits in common. "main" and "subtitle" have entirely different histories.
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": []
|
||||||
|
}
|
||||||
113
index.css
113
index.css
|
|
@ -860,39 +860,6 @@
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar Footer Actions */
|
|
||||||
.sidebar-footer-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-footer-actions .logout-btn {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-inquiry-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
color: var(--color-text-gray-400);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color var(--transition-normal);
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
font-weight: 700;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-inquiry-btn:hover {
|
|
||||||
color: var(--color-text-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-inquiry-btn.collapsed {
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logout Button */
|
/* Logout Button */
|
||||||
.logout-btn {
|
.logout-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -2075,9 +2042,31 @@
|
||||||
height: 64px;
|
height: 64px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comp2-inquiry-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comp2-inquiry-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.6);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.comp2-back-btn {
|
.comp2-back-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -3931,7 +3920,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 500px;
|
max-width: 600px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3972,7 +3961,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url-input-wrapper {
|
.url-input-wrapper {
|
||||||
|
|
@ -4005,21 +3994,19 @@
|
||||||
|
|
||||||
|
|
||||||
.url-input-button {
|
.url-input-button {
|
||||||
margin: auto;
|
padding: 12px 32px;
|
||||||
max-width: 400px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 11px 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-color: #AE72F9;
|
background-color: #AE72F9;
|
||||||
color: #ffffff;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 16px;
|
|
||||||
letter-spacing: -0.006em;
|
|
||||||
box-shadow: 0px 4px 24px 0px rgba(174, 114, 249, 0.4);
|
|
||||||
transition: all var(--transition-normal);
|
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-family: 'Pretendard', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #FFFFFF;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
animation: button-glow 1.5s ease-in-out infinite;
|
transition: background-color 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url-input-button:hover:not(:disabled) {
|
.url-input-button:hover:not(:disabled) {
|
||||||
|
|
@ -4043,8 +4030,7 @@
|
||||||
font-family: 'Pretendard', sans-serif;
|
font-family: 'Pretendard', sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #6AB0B3;
|
color: #6AB0B3;
|
||||||
margin: 0 0 10px;
|
margin: 24px 0 0 0;
|
||||||
white-space: pre-line;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* URL Input Dropdown */
|
/* URL Input Dropdown */
|
||||||
|
|
@ -4550,12 +4536,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-input-hint {
|
.hero-input-hint {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #CEE5E6;
|
color: #CEE5E6;
|
||||||
letter-spacing: -0.006em;
|
letter-spacing: -0.006em;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 16px;
|
||||||
white-space: pre-line;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-error {
|
.hero-error {
|
||||||
|
|
@ -10527,27 +10512,3 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Calendar panel scrollbar */
|
|
||||||
.calendar-panel-scroll::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-panel-scroll::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-panel-scroll::-webkit-scrollbar-thumb {
|
|
||||||
background: #067C80;
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-panel-scroll::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #088a8e;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Firefox */
|
|
||||||
.calendar-panel-scroll {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: #067C80 transparent;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ const Sidebar: React.FC<SidebarProps> = ({ activeItem, onNavigate, onHome, userI
|
||||||
{ id: '대시보드', label: t('sidebar.dashboard'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg> },
|
{ id: '대시보드', label: t('sidebar.dashboard'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg> },
|
||||||
{ id: '새 프로젝트 만들기', label: t('sidebar.newProject'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> },
|
{ id: '새 프로젝트 만들기', label: t('sidebar.newProject'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> },
|
||||||
{ id: 'ADO2 콘텐츠', label: t('sidebar.ado2Contents'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> },
|
{ id: 'ADO2 콘텐츠', label: t('sidebar.ado2Contents'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> },
|
||||||
{ id: '내 콘텐츠', label: t('sidebar.myContents'), disabled: true, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> },
|
// { id: '내 콘텐츠', label: t('sidebar.myContents'), disabled: true, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> },
|
||||||
{ id: '콘텐츠 캘린더', label: '콘텐츠 캘린더', disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> },
|
{ id: '콘텐츠 캘린더', label: '콘텐츠 캘린더', disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> },
|
||||||
{ id: '내 정보', label: t('sidebar.myInfo'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> },
|
{ id: '내 정보', label: t('sidebar.myInfo'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> },
|
||||||
];
|
];
|
||||||
|
|
@ -175,7 +175,6 @@ const Sidebar: React.FC<SidebarProps> = ({ activeItem, onNavigate, onHome, userI
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sidebar-footer-actions">
|
|
||||||
<button
|
<button
|
||||||
className={`logout-btn ${isCollapsed ? 'collapsed' : ''}`}
|
className={`logout-btn ${isCollapsed ? 'collapsed' : ''}`}
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
|
|
@ -186,19 +185,6 @@ const Sidebar: React.FC<SidebarProps> = ({ activeItem, onNavigate, onHome, userI
|
||||||
</svg>
|
</svg>
|
||||||
{!isCollapsed && <span className="logout-btn-label">{isLoggingOut ? t('sidebar.loggingOut') : t('sidebar.logout')}</span>}
|
{!isCollapsed && <span className="logout-btn-label">{isLoggingOut ? t('sidebar.loggingOut') : t('sidebar.logout')}</span>}
|
||||||
</button>
|
</button>
|
||||||
<a
|
|
||||||
href="https://forms.gle/4a8mGebBYtdesvby9"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={`sidebar-inquiry-btn ${isCollapsed ? 'collapsed' : ''}`}
|
|
||||||
title="고객의견"
|
|
||||||
>
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
||||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
||||||
</svg>
|
|
||||||
{!isCollapsed && <span>고객의견</span>}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
"hero": {
|
"hero": {
|
||||||
"searchTypeBusinessName": "Business Name",
|
"searchTypeBusinessName": "Business Name",
|
||||||
"placeholderBusinessName": "Enter a business name",
|
"placeholderBusinessName": "Enter a business name",
|
||||||
"guideUrl": "Enter the Naver Place URL.",
|
"guideUrl": "A video will be automatically generated from the information gathered from the URL.",
|
||||||
"guideBusinessName": "Search by business name to retrieve information.",
|
"guideBusinessName": "Search by business name to retrieve information.",
|
||||||
"errorUrlRequired": "Please enter a URL.",
|
"errorUrlRequired": "Please enter a URL.",
|
||||||
"errorNameRequired": "Please enter a business name.",
|
"errorNameRequired": "Please enter a business name.",
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
"urlInput": {
|
"urlInput": {
|
||||||
"searchTypeBusinessName": "Business Name",
|
"searchTypeBusinessName": "Business Name",
|
||||||
"placeholderBusinessName": "Enter a business name",
|
"placeholderBusinessName": "Enter a business name",
|
||||||
"guideUrl": "Select a place on Naver Maps, click Share,\nand paste the URL that appears.",
|
"guideUrl": "A video will be automatically generated from the information gathered from the URL.",
|
||||||
"guideBusinessName": "Search by business name to retrieve information.",
|
"guideBusinessName": "Search by business name to retrieve information.",
|
||||||
"searchButton": "Search",
|
"searchButton": "Search",
|
||||||
"searching": "Searching...",
|
"searching": "Searching...",
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,8 @@
|
||||||
"landing": {
|
"landing": {
|
||||||
"hero": {
|
"hero": {
|
||||||
"searchTypeBusinessName": "업체명",
|
"searchTypeBusinessName": "업체명",
|
||||||
"placeholderBusinessName": "업체명을 입력하세요.",
|
"placeholderBusinessName": "업체명을 입력하세요",
|
||||||
"guideUrl": "네이버 Place URL을 입력하세요.",
|
"guideUrl": "URL에서 가져온 정보로 영상이 자동 생성됩니다.",
|
||||||
"guideBusinessName": "업체명으로 검색하여 정보를 가져옵니다.",
|
"guideBusinessName": "업체명으로 검색하여 정보를 가져옵니다.",
|
||||||
"errorUrlRequired": "URL을 입력해주세요.",
|
"errorUrlRequired": "URL을 입력해주세요.",
|
||||||
"errorNameRequired": "업체명을 입력해주세요.",
|
"errorNameRequired": "업체명을 입력해주세요.",
|
||||||
|
|
@ -95,7 +95,7 @@
|
||||||
"title": "ADO2.AI에 오신 것을 환영합니다.",
|
"title": "ADO2.AI에 오신 것을 환영합니다.",
|
||||||
"subtitle": "분석, 제작, 배포까지 콘텐츠 마케팅의 전과정을 자동화",
|
"subtitle": "분석, 제작, 배포까지 콘텐츠 마케팅의 전과정을 자동화",
|
||||||
"feature1Title": "비즈니스 핵심 정보 분석",
|
"feature1Title": "비즈니스 핵심 정보 분석",
|
||||||
"feature1Desc": "홈페이지, 네이버 지도, 블로그 등의\nURL을 입력하세요.",
|
"feature1Desc": "홈페이지, 네이버 지도, 블로그 등의\nURL을 입력하세요",
|
||||||
"feature2Title": "홍보 콘텐츠 자동 제작",
|
"feature2Title": "홍보 콘텐츠 자동 제작",
|
||||||
"feature2Desc": "분석된 정보를 바탕으로\n비즈니스에 맞는 음악, 자막, 노래, 영상을\n자동으로 제작해요",
|
"feature2Desc": "분석된 정보를 바탕으로\n비즈니스에 맞는 음악, 자막, 노래, 영상을\n자동으로 제작해요",
|
||||||
"feature3Title": "멀티채널 자동 배포",
|
"feature3Title": "멀티채널 자동 배포",
|
||||||
|
|
@ -114,8 +114,8 @@
|
||||||
},
|
},
|
||||||
"urlInput": {
|
"urlInput": {
|
||||||
"searchTypeBusinessName": "업체명",
|
"searchTypeBusinessName": "업체명",
|
||||||
"placeholderBusinessName": "업체명을 입력하세요.",
|
"placeholderBusinessName": "업체명을 입력하세요",
|
||||||
"guideUrl": "네이버 Place URL을 입력하세요.",
|
"guideUrl": "URL에서 가져온 정보로 영상이 자동 생성됩니다.",
|
||||||
"guideBusinessName": "업체명으로 검색하여 정보를 가져옵니다.",
|
"guideBusinessName": "업체명으로 검색하여 정보를 가져옵니다.",
|
||||||
"searchButton": "검색하기",
|
"searchButton": "검색하기",
|
||||||
"searching": "검색 중...",
|
"searching": "검색 중...",
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,17 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
</svg>
|
</svg>
|
||||||
<span>{t('completion.back')}</span>
|
<span>{t('completion.back')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<a
|
||||||
|
href="https://forms.gle/4a8mGebBYtdesvby9"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="comp2-inquiry-btn"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||||
|
</svg>
|
||||||
|
<span>문의하기</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="comp2-title-row">
|
<div className="comp2-title-row">
|
||||||
|
|
|
||||||
|
|
@ -643,8 +643,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={panelRef}
|
ref={panelRef}
|
||||||
className="calendar-panel-scroll"
|
style={{ flex: 1, overflowY: 'auto', padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 20 }}
|
||||||
style={{ flex: 1, overflowY: 'auto', padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 20, maxHeight: 700 }}
|
|
||||||
>
|
>
|
||||||
{sortedDateKeys.map(dateKey => (
|
{sortedDateKeys.map(dateKey => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -736,7 +735,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||||
padding: '32px',
|
padding: '80px 32px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
minWidth: 1400,
|
minWidth: 1400,
|
||||||
height: '100%', boxSizing: 'border-box',
|
height: '100%', boxSizing: 'border-box',
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,21 @@ interface DailyData {
|
||||||
lastPeriod: number;
|
lastPeriod: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interface PlatformMetric { // 미사용 — platform_data 기능 예정
|
||||||
|
// id: string;
|
||||||
|
// label: string;
|
||||||
|
// value: string;
|
||||||
|
// unit?: string;
|
||||||
|
// trend: number;
|
||||||
|
// trendDirection: 'up' | 'down';
|
||||||
|
// }
|
||||||
|
|
||||||
|
// interface PlatformData { // 미사용 — platform_data 기능 예정
|
||||||
|
// platform: 'youtube' | 'instagram';
|
||||||
|
// displayName: string;
|
||||||
|
// metrics: PlatformMetric[];
|
||||||
|
// }
|
||||||
|
|
||||||
interface TopContent {
|
interface TopContent {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -50,12 +65,6 @@ interface AudienceData {
|
||||||
topRegions: { region: string; percentage: number }[];
|
topRegions: { region: string; percentage: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardError {
|
|
||||||
code: string;
|
|
||||||
message: string;
|
|
||||||
reconnect_url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DashboardResponse {
|
interface DashboardResponse {
|
||||||
contentMetrics: ContentMetric[];
|
contentMetrics: ContentMetric[];
|
||||||
monthlyData: MonthlyData[];
|
monthlyData: MonthlyData[];
|
||||||
|
|
@ -63,7 +72,7 @@ interface DashboardResponse {
|
||||||
topContent: TopContent[];
|
topContent: TopContent[];
|
||||||
audienceData: AudienceData;
|
audienceData: AudienceData;
|
||||||
hasUploads: boolean; // 업로드 영상 존재 여부 (false 시 mock 데이터 + 안내 메시지 표시)
|
hasUploads: boolean; // 업로드 영상 존재 여부 (false 시 mock 데이터 + 안내 메시지 표시)
|
||||||
error: DashboardError | null;
|
// platformData: PlatformData[]; // 미사용
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectedAccount {
|
interface ConnectedAccount {
|
||||||
|
|
@ -478,9 +487,8 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
||||||
|
|
||||||
const [dashboardData, setDashboardData] = useState<DashboardResponse | null>(null);
|
const [dashboardData, setDashboardData] = useState<DashboardResponse | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<DashboardError | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showMockData, setShowMockData] = useState(false);
|
const [showMockData, setShowMockData] = useState(false);
|
||||||
const [retryTrigger, setRetryTrigger] = useState(0);
|
|
||||||
|
|
||||||
// 계정 관련 state
|
// 계정 관련 state
|
||||||
const [accounts, setAccounts] = useState<ConnectedAccount[]>([]);
|
const [accounts, setAccounts] = useState<ConnectedAccount[]>([]);
|
||||||
|
|
@ -568,32 +576,25 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
|
|
||||||
|
if (errorData.code === 'YOUTUBE_NOT_CONNECTED') {
|
||||||
|
setError('YouTube 계정을 연동하여 데이터를 확인하세요.');
|
||||||
|
setDashboardData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (errorData.code === 'YOUTUBE_ACCOUNT_SELECTION_REQUIRED') {
|
if (errorData.code === 'YOUTUBE_ACCOUNT_SELECTION_REQUIRED') {
|
||||||
setDashboardData(null);
|
setDashboardData(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (errorData.code === 'YOUTUBE_API_FAILED') {
|
throw new Error(errorData.detail || `API Error: ${response.status}`);
|
||||||
setError({ code: 'YOUTUBE_API_FAILED', message: 'YouTube Analytics API 호출에 실패했습니다.' });
|
|
||||||
setDashboardData(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setError({ code: errorData.code || 'API_ERROR', message: errorData.detail || `API Error: ${response.status}`, reconnect_url: errorData.reconnect_url });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: DashboardResponse = await response.json();
|
const data: DashboardResponse = await response.json();
|
||||||
|
|
||||||
if (data.error) {
|
|
||||||
setError(data.error);
|
|
||||||
setDashboardData(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDashboardData(data);
|
setDashboardData(data);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Dashboard API Error:', err);
|
console.error('Dashboard API Error:', err);
|
||||||
setError({ code: 'UNKNOWN', message: err instanceof Error ? err.message : 'Unknown error' });
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||||
setDashboardData(null);
|
setDashboardData(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
@ -601,7 +602,7 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
}, [mode, selectedAccountId, accountsLoaded, retryTrigger]);
|
}, [mode, selectedAccountId, accountsLoaded]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -611,11 +612,11 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasUploads === false이고 error 없음: 업로드 영상 없음 → 전체 mock 데이터 표시 + 안내 배너
|
// hasUploads === false: 업로드 영상 없음 → 전체 mock 데이터 표시 + 안내 배너
|
||||||
const isEmptyState = dashboardData?.hasUploads === false && !dashboardData?.error;
|
const isEmptyState = dashboardData?.hasUploads === false;
|
||||||
|
|
||||||
// 블러 조건: 1)계정 미연결 2)업로드 영상 없음 3)데이터 없음 4)에러 있음
|
// 블러 조건: 1)계정 미연결 2)업로드 영상 없음 3)데이터 없음
|
||||||
const isBlurred = accounts.length === 0 || isEmptyState || !dashboardData || !!error;
|
const isBlurred = accounts.length === 0 || isEmptyState || !dashboardData;
|
||||||
|
|
||||||
// showMockData=true면 전체 mock 강제, 아니면 API 우선 / isEmptyState 시 mock 폴백
|
// showMockData=true면 전체 mock 강제, 아니면 API 우선 / isEmptyState 시 mock 폴백
|
||||||
const useReal = !showMockData && !isEmptyState;
|
const useReal = !showMockData && !isEmptyState;
|
||||||
|
|
@ -672,17 +673,11 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
||||||
<svg className="w-5 h-5 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
<p className="font-semibold">{error.message}</p>
|
<div>
|
||||||
|
<p className="font-semibold">실제 데이터를 보려면 계정을 연동하세요.</p>
|
||||||
</div>
|
</div>
|
||||||
{error.code === 'YOUTUBE_TOKEN_EXPIRED' && (
|
</div>
|
||||||
<button
|
{error.includes('YouTube') && (
|
||||||
onClick={() => onNavigate?.('내 정보')}
|
|
||||||
className="ml-4 px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
재연동하러 가기 →
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{error.code === 'YOUTUBE_NOT_CONNECTED' && (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onNavigate?.('내 정보')}
|
onClick={() => onNavigate?.('내 정보')}
|
||||||
className="ml-4 px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 whitespace-nowrap"
|
className="ml-4 px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 whitespace-nowrap"
|
||||||
|
|
@ -690,14 +685,6 @@ const DashboardContent: React.FC<DashboardContentProps> = ({ onNavigate }) => {
|
||||||
연동하러 가기 →
|
연동하러 가기 →
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{(error.code === 'YOUTUBE_API_FAILED' || error.code === 'DASHBOARD_DATA_ERROR') && (
|
|
||||||
<button
|
|
||||||
onClick={() => { setError(null); setRetryTrigger((n: number) => n + 1); }}
|
|
||||||
className="ml-4 px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
재시도 →
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
||||||
|
|
||||||
const getPlaceholder = () => {
|
const getPlaceholder = () => {
|
||||||
return searchType === 'url'
|
return searchType === 'url'
|
||||||
? 'https://naver.me/abcdef'
|
? 'https://www.castad.com'
|
||||||
: t('urlInput.placeholderBusinessName');
|
: t('urlInput.placeholderBusinessName');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -268,21 +268,21 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 안내 텍스트 */}
|
{/* 검색 버튼 */}
|
||||||
<p className="url-input-guide">
|
<button type="submit" className="url-input-button">
|
||||||
{getGuideText()}
|
{t('urlInput.searchButton')}
|
||||||
</p>
|
</button>
|
||||||
|
|
||||||
{/* 에러 메시지 */}
|
{/* 에러 메시지 */}
|
||||||
{error && (
|
{error && (
|
||||||
<p className="url-input-error">{error}</p>
|
<p className="url-input-error">{error}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 검색 버튼 */}
|
|
||||||
<button type="submit" className="url-input-button">
|
|
||||||
{t('landing.hero.analyzeButton')}
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{/* 안내 텍스트 */}
|
||||||
|
<p className="url-input-guide">
|
||||||
|
{getGuideText()}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 테스트 버튼 (VITE_IS_TESTPAGE=true일 때만 표시) */}
|
{/* 테스트 버튼 (VITE_IS_TESTPAGE=true일 때만 표시) */}
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
||||||
|
|
||||||
const getPlaceholder = () => {
|
const getPlaceholder = () => {
|
||||||
return searchType === 'url'
|
return searchType === 'url'
|
||||||
? 'https://naver.me/abcdef'
|
? 'https://www.castad.com'
|
||||||
: t('landing.hero.placeholderBusinessName');
|
: t('landing.hero.placeholderBusinessName');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue