브랜드 분석 페이지 css 수정, song id polling 적용 .
parent
e3ed840d12
commit
d5b6ad6958
181
index.css
181
index.css
|
|
@ -1821,32 +1821,51 @@
|
||||||
Analysis Result Page Components
|
Analysis Result Page Components
|
||||||
===================================================== */
|
===================================================== */
|
||||||
|
|
||||||
/* Analysis Container - 높이 제한 없음, 자연 스크롤 */
|
/* Analysis Page Wrapper - 전체 레이아웃 */
|
||||||
|
.analysis-page-wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
background-color: #002224;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Analysis Page Main - 스크롤 가능한 메인 영역 */
|
||||||
|
.analysis-page-main {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Analysis Container - 콘텐츠 래퍼 */
|
||||||
.analysis-container {
|
.analysis-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
color: var(--color-text-white);
|
color: var(--color-text-white);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #002224;
|
background-color: #002224;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding-bottom: 100px; /* 고정 버튼 영역 확보 */
|
padding: 0 16px 120px 16px; /* 고정 버튼 영역 확보 */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.analysis-container {
|
||||||
|
padding: 0 32px 120px 32px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header Area (Back Button + Title) */
|
/* Header Area (Back Button + Title) */
|
||||||
.analysis-header-area {
|
.analysis-header-area {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1440px;
|
max-width: 1440px;
|
||||||
padding: 8px 16px;
|
padding: 8px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.analysis-header-area {
|
|
||||||
padding: 8px 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Analysis Header */
|
/* Analysis Header */
|
||||||
.analysis-header {
|
.analysis-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -1854,13 +1873,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.analysis-header {
|
|
||||||
padding: 24px 0;
|
padding: 24px 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.analysis-icon {
|
.analysis-icon {
|
||||||
|
|
@ -1869,52 +1882,27 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.analysis-icon svg {
|
.analysis-icon svg {
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.analysis-icon svg {
|
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Analysis Grid */
|
/* Analysis Grid - 모바일: 세로 배치, 데스크톱: 가로 배치 */
|
||||||
.analysis-grid {
|
.analysis-grid {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1440px;
|
max-width: 1440px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 24px;
|
||||||
padding: 0 16px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
/* 데스크톱(1024px 이상)에서 가로 배치 */
|
||||||
.analysis-grid {
|
|
||||||
gap: 24px;
|
|
||||||
padding: 0 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.analysis-grid {
|
.analysis-grid {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
gap: 40px;
|
gap: 40px;
|
||||||
padding: 0 60px;
|
padding: 0 200px;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
.analysis-grid {
|
|
||||||
padding: 0 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1440px) {
|
|
||||||
.analysis-grid {
|
|
||||||
padding: 0 180px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1922,10 +1910,10 @@
|
||||||
.brand-identity-card {
|
.brand-identity-card {
|
||||||
background-color: #01393B;
|
background-color: #01393B;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 24px;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -1950,19 +1938,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-content {
|
.brand-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.brand-content {
|
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-name {
|
.brand-name {
|
||||||
|
|
@ -1982,37 +1963,24 @@
|
||||||
|
|
||||||
.brand-location {
|
.brand-location {
|
||||||
color: #6AB0B3;
|
color: #6AB0B3;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
letter-spacing: -0.006em;
|
letter-spacing: -0.006em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.brand-location {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-subtitle {
|
.brand-subtitle {
|
||||||
color: #6AB0B3;
|
color: #6AB0B3;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
letter-spacing: -0.006em;
|
letter-spacing: -0.006em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.brand-subtitle {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-info {
|
.brand-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Report Content */
|
/* Report Content */
|
||||||
|
|
@ -2036,19 +2004,17 @@
|
||||||
.analysis-cards-column {
|
.analysis-cards-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
/* 데스크톱(1024px 이상)에서만 sticky 적용 */
|
||||||
.analysis-cards-column {
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.analysis-cards-column {
|
.analysis-cards-column {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
position: sticky;
|
||||||
|
top: 40px;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2056,12 +2022,12 @@
|
||||||
.analysis-cards-column .feature-card {
|
.analysis-cards-column .feature-card {
|
||||||
background-color: #01393B;
|
background-color: #01393B;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
@ -2079,14 +2045,14 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selling Points Grid - 2열 그리드 */
|
/* Selling Points Grid - 모바일: 1열, 태블릿 이상: 2열 */
|
||||||
.selling-points-grid {
|
.selling-points-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 480px) {
|
@media (min-width: 640px) {
|
||||||
.selling-points-grid {
|
.selling-points-grid {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
|
@ -2096,67 +2062,50 @@
|
||||||
/* Selling Point Item */
|
/* Selling Point Item */
|
||||||
.selling-point-item {
|
.selling-point-item {
|
||||||
background-color: #034A4D;
|
background-color: #034A4D;
|
||||||
padding: 16px;
|
padding: 20px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 480px) {
|
@media (min-width: 640px) {
|
||||||
.selling-point-item {
|
.selling-point-item {
|
||||||
padding: 24px 20px;
|
padding: 24px 20px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
/* 첫 번째 행 왼쪽 */
|
||||||
/* 첫 번째 행 */
|
|
||||||
.selling-point-item:nth-child(1) {
|
.selling-point-item:nth-child(1) {
|
||||||
border-radius: 16px 0 0 0;
|
border-radius: 16px 0 0 0;
|
||||||
}
|
}
|
||||||
|
/* 첫 번째 행 오른쪽 */
|
||||||
.selling-point-item:nth-child(2) {
|
.selling-point-item:nth-child(2) {
|
||||||
border-radius: 0 16px 0 0;
|
border-radius: 0 16px 0 0;
|
||||||
}
|
}
|
||||||
|
/* 마지막 행 왼쪽 */
|
||||||
/* 마지막 행 */
|
|
||||||
.selling-point-item:nth-last-child(2):nth-child(odd) {
|
.selling-point-item:nth-last-child(2):nth-child(odd) {
|
||||||
border-radius: 0 0 0 16px;
|
border-radius: 0 0 0 16px;
|
||||||
}
|
}
|
||||||
|
/* 마지막 행 오른쪽 */
|
||||||
.selling-point-item:nth-last-child(1):nth-child(even) {
|
.selling-point-item:nth-last-child(1):nth-child(even) {
|
||||||
border-radius: 0 0 16px 0;
|
border-radius: 0 0 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 홀수 개일 때 마지막 아이템 */
|
|
||||||
.selling-point-item:last-child:nth-child(odd) {
|
|
||||||
border-radius: 0 0 0 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.selling-point-title {
|
.selling-point-title {
|
||||||
font-family: 'Pretendard', sans-serif;
|
font-family: 'Pretendard', sans-serif;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #6AB0B3;
|
color: #6AB0B3;
|
||||||
letter-spacing: -0.006em;
|
letter-spacing: -0.006em;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.selling-point-title {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selling-point-content {
|
.selling-point-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.selling-point-content {
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.selling-point-content p {
|
.selling-point-content p {
|
||||||
|
|
@ -2185,23 +2134,17 @@
|
||||||
.tags-wrapper {
|
.tags-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
align-content: flex-start;
|
align-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
/* Feature Tag - 피그마: pill 형태 */
|
||||||
.tags-wrapper {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Feature Tag - pill 형태 */
|
|
||||||
.feature-tag {
|
.feature-tag {
|
||||||
padding: 6px 12px;
|
padding: 8px 16px;
|
||||||
background-color: #034A4D;
|
background-color: #034A4D;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-family: 'Pretendard', sans-serif;
|
font-family: 'Pretendard', sans-serif;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #CEE5E6;
|
color: #CEE5E6;
|
||||||
letter-spacing: -0.006em;
|
letter-spacing: -0.006em;
|
||||||
|
|
@ -2210,7 +2153,6 @@
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.feature-tag {
|
.feature-tag {
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2221,18 +2163,17 @@
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 16px;
|
padding: 20px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(to top, #002224 60%, transparent);
|
background: linear-gradient(to top, #002224 70%, transparent);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar가 있을 때 버튼 위치 조정 (md breakpoint) */
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.analysis-bottom {
|
.analysis-bottom {
|
||||||
left: 280px;
|
|
||||||
padding: 24px 32px;
|
padding: 24px 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
|
||||||
background-color: #121a1d;
|
background-color: #121a1d;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
|
|
|
||||||
|
|
@ -314,16 +314,16 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
||||||
// 스크롤이 필요한 페이지: 대시보드, 비즈니스 설정, 브랜드 분석(0)
|
// 스크롤이 필요한 페이지: 대시보드, 비즈니스 설정, 브랜드 분석(0)
|
||||||
const needsScroll = activeItem === '대시보드' || activeItem === '비즈니스 설정' || isBrandAnalysis;
|
const needsScroll = activeItem === '대시보드' || activeItem === '비즈니스 설정' || isBrandAnalysis;
|
||||||
|
|
||||||
// 브랜드 분석일 때는 높이 제한 없이 자연 스크롤
|
// 브랜드 분석일 때는 전체 화면 스크롤
|
||||||
if (isBrandAnalysis) {
|
if (isBrandAnalysis) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full bg-[#0d1416] text-white">
|
<div className="analysis-page-wrapper">
|
||||||
{showSidebar && (
|
{showSidebar && (
|
||||||
<Sidebar activeItem={activeItem} onNavigate={setActiveItem} onHome={handleHome} />
|
<Sidebar activeItem={activeItem} onNavigate={setActiveItem} onHome={handleHome} />
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 relative">
|
<main className="analysis-page-main">
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ type GenerationStatus = 'idle' | 'generating_lyric' | 'generating_song' | 'polli
|
||||||
|
|
||||||
interface SavedGenerationState {
|
interface SavedGenerationState {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
|
songId: string;
|
||||||
lyrics: string;
|
lyrics: string;
|
||||||
status: GenerationStatus;
|
status: GenerationStatus;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|
@ -70,9 +71,10 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const languageDropdownRef = useRef<HTMLDivElement>(null);
|
const languageDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const saveToStorage = (taskId: string, currentLyrics: string, currentStatus: GenerationStatus) => {
|
const saveToStorage = (taskId: string, songId: string, currentLyrics: string, currentStatus: GenerationStatus) => {
|
||||||
const data: SavedGenerationState = {
|
const data: SavedGenerationState = {
|
||||||
taskId,
|
taskId,
|
||||||
|
songId,
|
||||||
lyrics: currentLyrics,
|
lyrics: currentLyrics,
|
||||||
status: currentStatus,
|
status: currentStatus,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
|
@ -112,7 +114,7 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
}
|
}
|
||||||
setStatus('polling');
|
setStatus('polling');
|
||||||
setStatusMessage('노래를 생성하고 있습니다... (새로고침 후 복구됨)');
|
setStatusMessage('노래를 생성하고 있습니다... (새로고침 후 복구됨)');
|
||||||
resumePolling(savedState.taskId, savedState.lyrics, 0);
|
resumePolling(savedState.taskId, savedState.songId, savedState.lyrics, 0);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -144,25 +146,25 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
}
|
}
|
||||||
}, [videoGenerationStatus, songTaskId, onNext]);
|
}, [videoGenerationStatus, songTaskId, onNext]);
|
||||||
|
|
||||||
const resumePolling = async (taskId: string, currentLyrics: string, currentRetryCount: number = 0) => {
|
const resumePolling = async (taskId: string, songId: string, currentLyrics: string, currentRetryCount: number = 0) => {
|
||||||
try {
|
try {
|
||||||
const downloadResponse = await waitForSongComplete(
|
const statusResponse = await waitForSongComplete(
|
||||||
taskId,
|
songId,
|
||||||
(pollStatus: string) => {
|
(pollStatus: string) => {
|
||||||
if (pollStatus === 'processing') {
|
if (pollStatus === 'streaming') {
|
||||||
setStatusMessage('노래를 생성하고 있습니다...');
|
setStatusMessage('노래를 생성하고 있습니다...');
|
||||||
} else if (pollStatus === 'uploading') {
|
} else if (pollStatus === 'queued') {
|
||||||
setStatusMessage('노래를 업로드하고 있습니다...');
|
setStatusMessage('노래 생성 대기 중...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!downloadResponse.success) {
|
if (!statusResponse.success) {
|
||||||
throw new Error(downloadResponse.error_message || '음악 다운로드에 실패했습니다.');
|
throw new Error(statusResponse.error_message || '음악 생성에 실패했습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
setAudioUrl(downloadResponse.song_result_url);
|
setAudioUrl(statusResponse.song_url);
|
||||||
setSongTaskId(downloadResponse.task_id);
|
setSongTaskId(taskId);
|
||||||
setStatus('complete');
|
setStatus('complete');
|
||||||
setStatusMessage('');
|
setStatusMessage('');
|
||||||
setRetryCount(0);
|
setRetryCount(0);
|
||||||
|
|
@ -218,8 +220,8 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
throw new Error(songResponse.error_message || '음악 생성 요청에 실패했습니다.');
|
throw new Error(songResponse.error_message || '음악 생성 요청에 실패했습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
saveToStorage(songResponse.task_id, currentLyrics, 'polling');
|
saveToStorage(songResponse.task_id, songResponse.song_id, currentLyrics, 'polling');
|
||||||
await resumePolling(songResponse.task_id, currentLyrics, currentRetryCount);
|
await resumePolling(songResponse.task_id, songResponse.song_id, currentLyrics, currentRetryCount);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Song regeneration failed:', error);
|
console.error('Song regeneration failed:', error);
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,15 @@ export interface SongGenerateResponse {
|
||||||
error_message: string | null;
|
error_message: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 노래 상태 조회 응답 (Suno Polling)
|
||||||
|
export interface SongStatusResponse {
|
||||||
|
success: boolean;
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
song_url: string | null;
|
||||||
|
error_message: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
// 노래 다운로드 상태 조회 응답 (DB Polling)
|
// 노래 다운로드 상태 조회 응답 (DB Polling)
|
||||||
export interface SongDownloadResponse {
|
export interface SongDownloadResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
LyricDetailResponse,
|
LyricDetailResponse,
|
||||||
SongGenerateRequest,
|
SongGenerateRequest,
|
||||||
SongGenerateResponse,
|
SongGenerateResponse,
|
||||||
|
SongStatusResponse,
|
||||||
SongDownloadResponse,
|
SongDownloadResponse,
|
||||||
VideoGenerateResponse,
|
VideoGenerateResponse,
|
||||||
VideoStatusResponse,
|
VideoStatusResponse,
|
||||||
|
|
@ -153,9 +154,21 @@ export async function generateSong(taskId: string, request: SongGenerateRequest)
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 노래 다운로드 상태 조회 API (DB Polling)
|
// 노래 상태 조회 API (Suno Polling)
|
||||||
// task_id를 기반으로 Song 테이블의 상태를 조회하고, completed인 경우 Project 정보와 노래 URL을 반환
|
export async function getSongStatus(songId: string): Promise<SongStatusResponse> {
|
||||||
export async function getSongDownloadStatus(taskId: string): Promise<SongDownloadResponse> {
|
const response = await fetch(`${API_URL}/song/status/${songId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 노래 다운로드 API
|
||||||
|
export async function downloadSong(taskId: string): Promise<SongDownloadResponse> {
|
||||||
const response = await fetch(`${API_URL}/song/download/${taskId}`, {
|
const response = await fetch(`${API_URL}/song/download/${taskId}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
@ -168,42 +181,37 @@ export async function getSongDownloadStatus(taskId: string): Promise<SongDownloa
|
||||||
}
|
}
|
||||||
|
|
||||||
// 노래 생성 완료까지 폴링 (5분 타임아웃, 3초 간격)
|
// 노래 생성 완료까지 폴링 (5분 타임아웃, 3초 간격)
|
||||||
// 새로운 API는 DB 상태를 직접 조회: processing, uploading, completed, failed, not_found, error
|
// Suno API 상태: PENDING, processing, SUCCESS, TEXT_SUCCESS, failed, error
|
||||||
const SONG_POLL_TIMEOUT = 5 * 60 * 1000; // 5분
|
const SONG_POLL_TIMEOUT = 5 * 60 * 1000; // 5분
|
||||||
const SONG_POLL_INTERVAL = 3000; // 3초
|
const SONG_POLL_INTERVAL = 3000; // 3초
|
||||||
|
|
||||||
export async function waitForSongComplete(
|
export async function waitForSongComplete(
|
||||||
taskId: string,
|
songId: string,
|
||||||
onStatusChange?: (status: string) => void
|
onStatusChange?: (status: string) => void
|
||||||
): Promise<SongDownloadResponse> {
|
): Promise<SongStatusResponse> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
const poll = async (): Promise<SongDownloadResponse> => {
|
const poll = async (): Promise<SongStatusResponse> => {
|
||||||
// 5분 타임아웃 체크
|
// 5분 타임아웃 체크
|
||||||
if (Date.now() - startTime > SONG_POLL_TIMEOUT) {
|
if (Date.now() - startTime > SONG_POLL_TIMEOUT) {
|
||||||
throw new Error('TIMEOUT');
|
throw new Error('TIMEOUT');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await getSongDownloadStatus(taskId);
|
const response = await getSongStatus(songId);
|
||||||
onStatusChange?.(response.status);
|
onStatusChange?.(response.status);
|
||||||
|
|
||||||
// completed: 모든 작업 완료, Blob URL 사용 가능
|
// SUCCESS 또는 TEXT_SUCCESS: Suno API 노래 생성 완료
|
||||||
if (response.status === 'completed' && response.success) {
|
if ((response.status === 'SUCCESS' || response.status === 'TEXT_SUCCESS') && response.success) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// failed: 노래 생성 또는 업로드 실패
|
// failed 또는 error: Suno API 노래 생성 실패
|
||||||
if (response.status === 'failed' || response.status === 'error') {
|
if (response.status === 'failed' || response.status === 'error') {
|
||||||
throw new Error(response.error_message || '노래 생성에 실패했습니다.');
|
throw new Error(response.error_message || '노래 생성에 실패했습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// not_found: task_id에 해당하는 Song 없음
|
// PENDING, processing 등은 대기 후 재시도
|
||||||
if (response.status === 'not_found') {
|
|
||||||
throw new Error('노래를 찾을 수 없습니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// processing, uploading 등은 대기 후 재시도
|
|
||||||
await new Promise(resolve => setTimeout(resolve, SONG_POLL_INTERVAL));
|
await new Promise(resolve => setTimeout(resolve, SONG_POLL_INTERVAL));
|
||||||
return poll();
|
return poll();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue