직접입력시 업종 추가
parent
2f720dfa92
commit
bd92eb2226
|
|
@ -334,14 +334,14 @@ const App: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 업체명·주소 수동 입력으로 마케팅 분석 API 호출
|
// 업체명·주소 수동 입력으로 마케팅 분석 API 호출
|
||||||
const handleManualInput = async (businessName: string, address: string) => {
|
const handleManualInput = async (businessName: string, address: string, category: string) => {
|
||||||
setAfterLoadTarget('generation_flow');
|
setAfterLoadTarget('generation_flow');
|
||||||
setViewMode('loading');
|
setViewMode('loading');
|
||||||
setIsAnalysisComplete(false);
|
setIsAnalysisComplete(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await marketingAnalysis(businessName, address);
|
const data = await marketingAnalysis(businessName, address, category);
|
||||||
|
|
||||||
if (!validateCrawlingResponse(data)) {
|
if (!validateCrawlingResponse(data)) {
|
||||||
throw new Error(t('app.autocompleteError'));
|
throw new Error(t('app.autocompleteError'));
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import CitySelectModal, { REGIONS } from './CitySelectModal';
|
||||||
|
|
||||||
interface BusinessNameInputModalProps {
|
interface BusinessNameInputModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (businessName: string, address: string) => void;
|
onSubmit: (businessName: string, address: string, category: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BusinessNameInputModal: React.FC<BusinessNameInputModalProps> = ({ onClose, onSubmit }) => {
|
const BusinessNameInputModal: React.FC<BusinessNameInputModalProps> = ({ onClose, onSubmit }) => {
|
||||||
|
|
@ -13,6 +13,7 @@ const BusinessNameInputModal: React.FC<BusinessNameInputModalProps> = ({ onClose
|
||||||
const [businessName, setBusinessName] = useState('');
|
const [businessName, setBusinessName] = useState('');
|
||||||
const [selectedCity, setSelectedCity] = useState('');
|
const [selectedCity, setSelectedCity] = useState('');
|
||||||
const [detailAddress, setDetailAddress] = useState('');
|
const [detailAddress, setDetailAddress] = useState('');
|
||||||
|
const [category, setCategory] = useState('');
|
||||||
const [isCityModalOpen, setIsCityModalOpen] = useState(false);
|
const [isCityModalOpen, setIsCityModalOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -41,7 +42,7 @@ const BusinessNameInputModal: React.FC<BusinessNameInputModalProps> = ({ onClose
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!isValid) return;
|
if (!isValid) return;
|
||||||
const fullAddress = `${selectedCity} ${detailAddress.trim()}`;
|
const fullAddress = `${selectedCity} ${detailAddress.trim()}`;
|
||||||
onSubmit(businessName.trim(), fullAddress);
|
onSubmit(businessName.trim(), fullAddress, category.trim());
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -104,6 +105,19 @@ const BusinessNameInputModal: React.FC<BusinessNameInputModalProps> = ({ onClose
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="manual-modal-field">
|
||||||
|
<label className="manual-modal-label">{t('landing.hero.manualLabelCategory')}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="manual-modal-input"
|
||||||
|
placeholder={t('landing.hero.manualPlaceholderCategory')}
|
||||||
|
value={category}
|
||||||
|
onChange={e => setCategory(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
maxLength={30}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="manual-modal-actions">
|
<div className="manual-modal-actions">
|
||||||
<button type="button" className="manual-modal-cancel" onClick={onClose}>
|
<button type="button" className="manual-modal-cancel" onClick={onClose}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ const isValidUrl = (string: string): boolean => {
|
||||||
interface SearchInputFormProps {
|
interface SearchInputFormProps {
|
||||||
onAnalyze?: (value: string, type: SearchType) => void;
|
onAnalyze?: (value: string, type: SearchType) => void;
|
||||||
onAutocomplete?: (data: AutocompleteRequest) => void;
|
onAutocomplete?: (data: AutocompleteRequest) => void;
|
||||||
onManualInput?: (businessName: string, address: string) => void;
|
onManualInput?: (businessName: string, address: string, category: string) => void;
|
||||||
/** 직접입력 버튼 클릭 시 기본 동작(모달 열기)을 대체합니다. 제공 시 모달을 직접 관리해야 합니다. */
|
/** 직접입력 버튼 클릭 시 기본 동작(모달 열기)을 대체합니다. 제공 시 모달을 직접 관리해야 합니다. */
|
||||||
onManualButtonClick?: () => void;
|
onManualButtonClick?: () => void;
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
|
|
@ -275,9 +275,9 @@ const SearchInputForm: React.FC<SearchInputFormProps> = ({
|
||||||
{!onManualButtonClick && isManualModalOpen && (
|
{!onManualButtonClick && isManualModalOpen && (
|
||||||
<BusinessNameInputModal
|
<BusinessNameInputModal
|
||||||
onClose={() => setIsManualModalOpen(false)}
|
onClose={() => setIsManualModalOpen(false)}
|
||||||
onSubmit={(businessName, address) => {
|
onSubmit={(businessName, address, category) => {
|
||||||
setIsManualModalOpen(false);
|
setIsManualModalOpen(false);
|
||||||
onManualInput?.(businessName, address);
|
onManualInput?.(businessName, address, category);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -203,10 +203,12 @@
|
||||||
"manualLabelAddress": "Address",
|
"manualLabelAddress": "Address",
|
||||||
"manualLabelRegion": "Region",
|
"manualLabelRegion": "Region",
|
||||||
"manualLabelDetail": "Detail Address",
|
"manualLabelDetail": "Detail Address",
|
||||||
|
"manualLabelCategory": "Category",
|
||||||
"manualPlaceholderName": "Enter the business name",
|
"manualPlaceholderName": "Enter the business name",
|
||||||
"manualPlaceholderAddress": "Enter the address",
|
"manualPlaceholderAddress": "Enter the address",
|
||||||
"manualPlaceholderRegion": "Select a region",
|
"manualPlaceholderRegion": "Select a region",
|
||||||
"manualPlaceholderDetail": "Enter detail address (e.g. Gangnam-gu Teheran-ro 123)"
|
"manualPlaceholderDetail": "Enter detail address (e.g. Gangnam-gu Teheran-ro 123)",
|
||||||
|
"manualPlaceholderCategory": "Enter the business category (e.g. pension, cafe, salon)"
|
||||||
},
|
},
|
||||||
"welcome": {
|
"welcome": {
|
||||||
"title": "Welcome to ADO2.AI",
|
"title": "Welcome to ADO2.AI",
|
||||||
|
|
@ -246,10 +248,12 @@
|
||||||
"manualLabelAddress": "Address",
|
"manualLabelAddress": "Address",
|
||||||
"manualLabelRegion": "Region",
|
"manualLabelRegion": "Region",
|
||||||
"manualLabelDetail": "Detail Address",
|
"manualLabelDetail": "Detail Address",
|
||||||
|
"manualLabelCategory": "Category",
|
||||||
"manualPlaceholderName": "Enter the business name",
|
"manualPlaceholderName": "Enter the business name",
|
||||||
"manualPlaceholderAddress": "Enter the address",
|
"manualPlaceholderAddress": "Enter the address",
|
||||||
"manualPlaceholderRegion": "Select a region",
|
"manualPlaceholderRegion": "Select a region",
|
||||||
"manualPlaceholderDetail": "Enter detail address (e.g. Gangnam-gu Teheran-ro 123)"
|
"manualPlaceholderDetail": "Enter detail address (e.g. Gangnam-gu Teheran-ro 123)",
|
||||||
|
"manualPlaceholderCategory": "Enter the business category (e.g. pension, cafe, salon)"
|
||||||
},
|
},
|
||||||
"assetManagement": {
|
"assetManagement": {
|
||||||
"title": "Brand Assets",
|
"title": "Brand Assets",
|
||||||
|
|
|
||||||
|
|
@ -202,10 +202,12 @@
|
||||||
"manualLabelAddress": "주소",
|
"manualLabelAddress": "주소",
|
||||||
"manualLabelRegion": "지역",
|
"manualLabelRegion": "지역",
|
||||||
"manualLabelDetail": "상세 주소",
|
"manualLabelDetail": "상세 주소",
|
||||||
|
"manualLabelCategory": "업종",
|
||||||
"manualPlaceholderName": "업체명을 입력하세요.",
|
"manualPlaceholderName": "업체명을 입력하세요.",
|
||||||
"manualPlaceholderAddress": "주소를 입력하세요.",
|
"manualPlaceholderAddress": "주소를 입력하세요.",
|
||||||
"manualPlaceholderRegion": "지역을 선택하세요.",
|
"manualPlaceholderRegion": "지역을 선택하세요.",
|
||||||
"manualPlaceholderDetail": "상세 주소를 입력하세요. (예: 강남구 테헤란로 123)"
|
"manualPlaceholderDetail": "상세 주소를 입력하세요. (예: 강남구 테헤란로 123)",
|
||||||
|
"manualPlaceholderCategory": "업종을 입력하세요. (예: 펜션, 카페, 미용실)"
|
||||||
},
|
},
|
||||||
"welcome": {
|
"welcome": {
|
||||||
"title": "ADO2.AI에 오신 것을 환영합니다.",
|
"title": "ADO2.AI에 오신 것을 환영합니다.",
|
||||||
|
|
@ -245,10 +247,12 @@
|
||||||
"manualLabelAddress": "주소",
|
"manualLabelAddress": "주소",
|
||||||
"manualLabelRegion": "지역",
|
"manualLabelRegion": "지역",
|
||||||
"manualLabelDetail": "상세 주소",
|
"manualLabelDetail": "상세 주소",
|
||||||
|
"manualLabelCategory": "업종",
|
||||||
"manualPlaceholderName": "업체명을 입력하세요.",
|
"manualPlaceholderName": "업체명을 입력하세요.",
|
||||||
"manualPlaceholderAddress": "주소를 입력하세요.",
|
"manualPlaceholderAddress": "주소를 입력하세요.",
|
||||||
"manualPlaceholderRegion": "지역을 선택하세요.",
|
"manualPlaceholderRegion": "지역을 선택하세요.",
|
||||||
"manualPlaceholderDetail": "상세 주소를 입력하세요. (예: 강남구 테헤란로 123)"
|
"manualPlaceholderDetail": "상세 주소를 입력하세요. (예: 강남구 테헤란로 123)",
|
||||||
|
"manualPlaceholderCategory": "업종을 입력하세요. (예: 펜션, 카페, 미용실)"
|
||||||
},
|
},
|
||||||
"assetManagement": {
|
"assetManagement": {
|
||||||
"title": "브랜드 에셋",
|
"title": "브랜드 에셋",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const LoadingSection: React.FC<LoadingSectionProps> = ({ onComplete, isComplete
|
||||||
const increment = prev < 70 ? 0.5 : prev < 90 ? 0.2 : 0.1;
|
const increment = prev < 70 ? 0.5 : prev < 90 ? 0.2 : 0.1;
|
||||||
return Math.min(prev + increment, 100);
|
return Math.min(prev + increment, 100);
|
||||||
});
|
});
|
||||||
}, 150);
|
}, 200);
|
||||||
return () => {
|
return () => {
|
||||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -263,13 +263,13 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
// 업체명·주소 수동 입력으로 마케팅 분석 API 호출
|
// 업체명·주소 수동 입력으로 마케팅 분석 API 호출
|
||||||
const handleManualInput = async (businessName: string, address: string) => {
|
const handleManualInput = async (businessName: string, address: string, category: string) => {
|
||||||
goToWizardStep(-1);
|
goToWizardStep(-1);
|
||||||
setIsAnalysisComplete(false);
|
setIsAnalysisComplete(false);
|
||||||
setAnalysisError(null);
|
setAnalysisError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await marketingAnalysis(businessName, address);
|
const data = await marketingAnalysis(businessName, address, category);
|
||||||
|
|
||||||
if (data.processed_info) {
|
if (data.processed_info) {
|
||||||
data.processed_info.customer_name = data.processed_info.customer_name || businessName;
|
data.processed_info.customer_name = data.processed_info.customer_name || businessName;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { generateLyric, waitForLyricComplete, generateSong, waitForSongComplete, downloadSong } from '../../utils/api';
|
import { generateLyric, waitForLyricComplete, generateSong, waitForSongComplete } from '../../utils/api';
|
||||||
import { LANGUAGE_MAP } from '../../types/api';
|
import { LANGUAGE_MAP } from '../../types/api';
|
||||||
|
|
||||||
interface BusinessInfo {
|
interface BusinessInfo {
|
||||||
|
|
@ -107,7 +107,6 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
const [selectedGenre, setSelectedGenre] = useState('자동 선택');
|
const [selectedGenre, setSelectedGenre] = useState('자동 선택');
|
||||||
const [usedGenre, setUsedGenre] = useState('');
|
const [usedGenre, setUsedGenre] = useState('');
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
const [status, setStatus] = useState<GenerationStatus>('idle');
|
const [status, setStatus] = useState<GenerationStatus>('idle');
|
||||||
const [lyrics, setLyrics] = useState('');
|
const [lyrics, setLyrics] = useState('');
|
||||||
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
||||||
|
|
@ -118,7 +117,6 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
const [retryCount, setRetryCount] = useState(0);
|
const [retryCount, setRetryCount] = useState(0);
|
||||||
const [songTaskId, setSongTaskId] = useState<string | null>(null);
|
const [songTaskId, setSongTaskId] = useState<string | null>(null);
|
||||||
const progressBarRef = useRef<HTMLDivElement>(null);
|
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
|
|
||||||
// 완료 데이터를 localStorage에 저장
|
// 완료 데이터를 localStorage에 저장
|
||||||
|
|
@ -142,27 +140,12 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
const parsed = JSON.parse(saved);
|
const parsed = JSON.parse(saved);
|
||||||
if (parsed.status !== 'complete' || !parsed.audioUrl || !parsed.songTaskId) return;
|
if (parsed.status !== 'complete' || !parsed.audioUrl || !parsed.songTaskId) return;
|
||||||
|
|
||||||
// 저장된 URL로 일단 복원 후, 서버에서 최신 URL 재조회 시도
|
// 저장된 URL로 복원 (public Blob URL이라 만료되지 않음)
|
||||||
|
// 백엔드에 /song/download 엔드포인트가 없으므로 재조회하지 않는다.
|
||||||
setSongTaskId(parsed.songTaskId);
|
setSongTaskId(parsed.songTaskId);
|
||||||
setAudioUrl(parsed.audioUrl);
|
setAudioUrl(parsed.audioUrl);
|
||||||
setLyrics(parsed.lyrics ?? '');
|
setLyrics(parsed.lyrics ?? '');
|
||||||
setStatus('complete');
|
setStatus('complete');
|
||||||
|
|
||||||
// downloadSong으로 최신 URL을 재조회해 갱신 (만료 방지)
|
|
||||||
downloadSong(parsed.songTaskId)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.success && res.song_result_url) {
|
|
||||||
setAudioUrl(res.song_result_url);
|
|
||||||
// 저장값도 갱신
|
|
||||||
localStorage.setItem(SONG_GENERATION_KEY, JSON.stringify({
|
|
||||||
...parsed,
|
|
||||||
audioUrl: res.song_result_url,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// 재조회 실패 시 저장된 URL 그대로 사용
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
// 파싱 실패 시 무시
|
// 파싱 실패 시 무시
|
||||||
}
|
}
|
||||||
|
|
@ -276,41 +259,6 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMove = (clientX: number) => {
|
|
||||||
if (!progressBarRef.current || !audioRef.current) return;
|
|
||||||
const rect = progressBarRef.current.getBoundingClientRect();
|
|
||||||
const newProgress = Math.max(0, Math.min(100, ((clientX - rect.left) / rect.width) * 100));
|
|
||||||
const newTime = (newProgress / 100) * duration;
|
|
||||||
audioRef.current.currentTime = newTime;
|
|
||||||
setProgress(newProgress);
|
|
||||||
setCurrentTime(newTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseDown = (e: React.MouseEvent) => {
|
|
||||||
if (!audioUrl) return;
|
|
||||||
setIsDragging(true);
|
|
||||||
handleMove(e.clientX);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
|
||||||
if (isDragging) handleMove(e.clientX);
|
|
||||||
};
|
|
||||||
const onMouseUp = () => {
|
|
||||||
setIsDragging(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isDragging) {
|
|
||||||
window.addEventListener('mousemove', onMouseMove);
|
|
||||||
window.addEventListener('mouseup', onMouseUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('mousemove', onMouseMove);
|
|
||||||
window.removeEventListener('mouseup', onMouseUp);
|
|
||||||
};
|
|
||||||
}, [isDragging]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audio = audioRef.current;
|
const audio = audioRef.current;
|
||||||
if (!audio) return;
|
if (!audio) return;
|
||||||
|
|
@ -489,6 +437,24 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
|
|
||||||
const isGenerating = status === 'generating_lyric' || status === 'generating_song' || status === 'polling';
|
const isGenerating = status === 'generating_lyric' || status === 'generating_song' || status === 'polling';
|
||||||
|
|
||||||
|
// 드래그로 인한 잦은 리렌더에서도 가사 파싱(정규식/배열 처리)이 매번 재실행되지 않도록 메모이제이션
|
||||||
|
const lyricsSections = useMemo(() => {
|
||||||
|
if (!lyrics) return [];
|
||||||
|
const lines = lyrics.split('\n').filter(l => l.trim());
|
||||||
|
const sections: { tag: string | null; lines: string[] }[] = [];
|
||||||
|
lines.forEach(line => {
|
||||||
|
const tagMatch = line.trim().match(/^\[(.+)\]$/);
|
||||||
|
if (tagMatch) {
|
||||||
|
sections.push({ tag: `[${tagMatch[1]}]`, lines: [] });
|
||||||
|
} else if (sections.length === 0) {
|
||||||
|
sections.push({ tag: null, lines: [line] });
|
||||||
|
} else {
|
||||||
|
sections[sections.length - 1].lines.push(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sections.filter(s => s.lines.length > 0);
|
||||||
|
}, [lyrics]);
|
||||||
|
|
||||||
// status 변경 시 부모에 알림
|
// status 변경 시 부모에 알림
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onStatusChange?.(status);
|
onStatusChange?.(status);
|
||||||
|
|
@ -701,9 +667,8 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* 진행 표시 전용 — Azure Blob 익명 응답에 Accept-Ranges가 없어 seek가 불안정하므로 드래그 미지원 */}
|
||||||
<div
|
<div
|
||||||
ref={progressBarRef}
|
|
||||||
onMouseDown={onMouseDown}
|
|
||||||
className={`audio-progress-container ${!audioUrl ? 'disabled' : ''}`}
|
className={`audio-progress-container ${!audioUrl ? 'disabled' : ''}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -719,30 +684,16 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
|
|
||||||
{/* Lyrics Display */}
|
{/* Lyrics Display */}
|
||||||
<div className="lyrics-display">
|
<div className="lyrics-display">
|
||||||
{lyrics ? (() => {
|
{lyrics ? (
|
||||||
const lines = lyrics.split('\n').filter(l => l.trim());
|
<div className="lyrics-paragraphs">
|
||||||
const sections: { tag: string | null; lines: string[] }[] = [];
|
{lyricsSections.map((section, i) => (
|
||||||
lines.forEach(line => {
|
<div key={i} className="lyrics-section">
|
||||||
const tagMatch = line.trim().match(/^\[(.+)\]$/);
|
{section.tag && <span className="lyrics-tag">{section.tag}</span>}
|
||||||
if (tagMatch) {
|
<p className="lyrics-paragraph">{section.lines.join('\n')}</p>
|
||||||
sections.push({ tag: `[${tagMatch[1]}]`, lines: [] });
|
</div>
|
||||||
} else if (sections.length === 0) {
|
))}
|
||||||
sections.push({ tag: null, lines: [line] });
|
</div>
|
||||||
} else {
|
) : (
|
||||||
sections[sections.length - 1].lines.push(line);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className="lyrics-paragraphs">
|
|
||||||
{sections.filter(s => s.lines.length > 0).map((section, i) => (
|
|
||||||
<div key={i} className="lyrics-section">
|
|
||||||
{section.tag && <span className="lyrics-tag">{section.tag}</span>}
|
|
||||||
<p className="lyrics-paragraph">{section.lines.join('\n')}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})() : (
|
|
||||||
<div className="lyrics-placeholder">
|
<div className="lyrics-placeholder">
|
||||||
{selectedType === '배경음악' ? t('soundStudio.lyricsPlaceholderBGM') : t('soundStudio.lyricsPlaceholder')}
|
{selectedType === '배경음악' ? t('soundStudio.lyricsPlaceholderBGM') : t('soundStudio.lyricsPlaceholder')}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import SearchInputForm, { SearchType } from '../../components/SearchInputForm';
|
||||||
interface UrlInputContentProps {
|
interface UrlInputContentProps {
|
||||||
onAnalyze: (value: string, type?: SearchType) => void;
|
onAnalyze: (value: string, type?: SearchType) => void;
|
||||||
onAutocomplete?: (data: AutocompleteRequest) => void;
|
onAutocomplete?: (data: AutocompleteRequest) => void;
|
||||||
onManualInput?: (businessName: string, address: string) => void;
|
onManualInput?: (businessName: string, address: string, category: string) => void;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ const orbConfigs: OrbConfig[] = [
|
||||||
interface HeroSectionProps {
|
interface HeroSectionProps {
|
||||||
onAnalyze?: (value: string, type?: SearchType) => void;
|
onAnalyze?: (value: string, type?: SearchType) => void;
|
||||||
onAutocomplete?: (data: AutocompleteRequest) => void;
|
onAutocomplete?: (data: AutocompleteRequest) => void;
|
||||||
onManualInput?: (businessName: string, address: string) => void;
|
onManualInput?: (businessName: string, address: string, category: string) => void;
|
||||||
onNext?: () => void;
|
onNext?: () => void;
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
scrollProgress?: number;
|
scrollProgress?: number;
|
||||||
|
|
@ -184,10 +184,10 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
||||||
{isManualModalOpen && (
|
{isManualModalOpen && (
|
||||||
<BusinessNameInputModal
|
<BusinessNameInputModal
|
||||||
onClose={() => setIsManualModalOpen(false)}
|
onClose={() => setIsManualModalOpen(false)}
|
||||||
onSubmit={(businessName, address) => {
|
onSubmit={(businessName, address, category) => {
|
||||||
if (tutorial.isActive) tutorial.nextHint();
|
if (tutorial.isActive) tutorial.nextHint();
|
||||||
setIsManualModalOpen(false);
|
setIsManualModalOpen(false);
|
||||||
onManualInput?.(businessName, address);
|
onManualInput?.(businessName, address, category);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #002224;
|
background-color: var(--color-bg-darker);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -910,7 +910,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.comp2-lyrics-tag {
|
.comp2-lyrics-tag {
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #379599;
|
color: #379599;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
|
|
||||||
|
|
@ -550,7 +550,7 @@
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.25rem;
|
gap: 0.5rem;
|
||||||
scrollbar-color: #046266 transparent;
|
scrollbar-color: #046266 transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -810,7 +810,7 @@
|
||||||
/* Asset Page Layout */
|
/* Asset Page Layout */
|
||||||
.asset-page {
|
.asset-page {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #002224;
|
background-color: var(--color-darker);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -911,7 +911,7 @@ export async function autocomplete(request: AutocompleteRequest): Promise<Crawli
|
||||||
}
|
}
|
||||||
|
|
||||||
// 업체명·주소 직접 입력으로 마케팅 분석
|
// 업체명·주소 직접 입력으로 마케팅 분석
|
||||||
export async function marketingAnalysis(storeName: string, address: string): Promise<CrawlingResponse> {
|
export async function marketingAnalysis(storeName: string, address: string, category = ''): Promise<CrawlingResponse> {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), CRAWL_TIMEOUT);
|
const timeoutId = setTimeout(() => controller.abort(), CRAWL_TIMEOUT);
|
||||||
|
|
||||||
|
|
@ -921,7 +921,7 @@ export async function marketingAnalysis(storeName: string, address: string): Pro
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ store_name: storeName, address }),
|
body: JSON.stringify({ store_name: storeName, address, category }),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue