fix: 로그인 유도 모달 개편과 공유 메뉴 방향·다운로드 파일명·정렬 수정
This commit is contained in:
parent
1c01dab748
commit
18b5271110
@ -26,22 +26,24 @@ const LoginPromptModal: React.FC<LoginPromptModalProps> = ({ onClose }) => {
|
|||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
inset: 0,
|
inset: 0,
|
||||||
background: 'rgba(255, 255, 255, 0.25)',
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
|
padding: '16px',
|
||||||
}}
|
}}
|
||||||
{...overlayCloseHandlers}
|
{...overlayCloseHandlers}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: '#01282A',
|
background: '#01282A',
|
||||||
border: '1px solid rgba(155, 202, 204, 0.2)',
|
border: '1px solid rgba(166, 255, 234, 0.15)',
|
||||||
borderRadius: '16px',
|
borderRadius: '20px',
|
||||||
padding: '40px 32px',
|
boxShadow: '0 24px 60px rgba(0, 0, 0, 0.5)',
|
||||||
width: '360px',
|
padding: '44px 32px 36px',
|
||||||
maxWidth: '90vw',
|
width: '380px',
|
||||||
|
maxWidth: '100%',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
@ -66,27 +68,49 @@ const LoginPromptModal: React.FC<LoginPromptModalProps> = ({ onClose }) => {
|
|||||||
<line x1="16" y1="4" x2="4" y2="16"/>
|
<line x1="16" y1="4" x2="4" y2="16"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<h2 style={{ color: '#FFFFFF', fontSize: '20px', fontWeight: 600, marginBottom: '12px' }}>
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '56px',
|
||||||
|
height: '56px',
|
||||||
|
margin: '0 auto 20px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'rgba(166, 255, 234, 0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#A6FFEA" strokeWidth="2">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style={{ color: '#FFFFFF', fontSize: '22px', fontWeight: 700, marginBottom: '10px', lineHeight: 1.4 }}>
|
||||||
{t('loginPrompt.title')}
|
{t('loginPrompt.title')}
|
||||||
</h2>
|
</h2>
|
||||||
|
<p style={{ color: '#9BCACC', fontSize: '14px', lineHeight: 1.5, marginBottom: '28px', whiteSpace: 'pre-line' }}>
|
||||||
|
{t('loginPrompt.description')}
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleLogin}
|
onClick={handleLogin}
|
||||||
style={{
|
style={{
|
||||||
width: '50%',
|
width: '100%',
|
||||||
margin: '0 auto',
|
padding: '15px',
|
||||||
padding: '14px',
|
|
||||||
background: '#FEE500',
|
background: '#FEE500',
|
||||||
color: '#3C1E1E',
|
color: '#3C1E1E',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '8px',
|
borderRadius: '10px',
|
||||||
fontSize: '15px',
|
fontSize: '16px',
|
||||||
fontWeight: 600,
|
fontWeight: 700,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
|
boxShadow: '0 4px 14px rgba(254, 229, 0, 0.25)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|||||||
@ -30,6 +30,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
|
|||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [shareMenuOpen, setShareMenuOpen] = useState(false);
|
const [shareMenuOpen, setShareMenuOpen] = useState(false);
|
||||||
|
const [shareMenuUpward, setShareMenuUpward] = useState(false);
|
||||||
const [isLandscape, setIsLandscape] = useState(false);
|
const [isLandscape, setIsLandscape] = useState(false);
|
||||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||||
|
|
||||||
@ -158,6 +159,23 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
|
|||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, [shareMenuOpen]);
|
}, [shareMenuOpen]);
|
||||||
|
|
||||||
|
// 모달 내부는 스크롤 영역(.video-detail-body)이 곧 화면 경계라, 버튼이 그 하단에
|
||||||
|
// 가까우면 아래로 펼치는 메뉴가 잘려서 안 보인다. 열기 직전 남은 공간을 재서 뒤집는다.
|
||||||
|
const handleToggleShareMenu = () => {
|
||||||
|
if (!shareMenuOpen) {
|
||||||
|
const btnRect = shareMenuRef.current?.getBoundingClientRect();
|
||||||
|
if (btnRect) {
|
||||||
|
const scrollBox = shareMenuRef.current?.closest('.video-detail-body');
|
||||||
|
const boundaryBottom = scrollBox
|
||||||
|
? scrollBox.getBoundingClientRect().bottom
|
||||||
|
: window.innerHeight;
|
||||||
|
const ESTIMATED_MENU_HEIGHT = 220;
|
||||||
|
setShareMenuUpward(btnRect.bottom + ESTIMATED_MENU_HEIGHT > boundaryBottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setShareMenuOpen(v => !v);
|
||||||
|
};
|
||||||
|
|
||||||
const likeDebounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
const likeDebounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const handleLike = () => {
|
const handleLike = () => {
|
||||||
@ -292,7 +310,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
|
|||||||
<div style={{ position: 'relative' }} ref={shareMenuRef}>
|
<div style={{ position: 'relative' }} ref={shareMenuRef}>
|
||||||
<button
|
<button
|
||||||
className="video-detail-copy-btn"
|
className="video-detail-copy-btn"
|
||||||
onClick={() => setShareMenuOpen(v => !v)}
|
onClick={handleToggleShareMenu}
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
|
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
|
||||||
@ -301,7 +319,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
|
|||||||
{copied ? t('videoDetail.copied') : t('videoDetail.share')}
|
{copied ? t('videoDetail.copied') : t('videoDetail.share')}
|
||||||
</button>
|
</button>
|
||||||
{shareMenuOpen && (
|
{shareMenuOpen && (
|
||||||
<div className="video-detail-share-menu">
|
<div className={`video-detail-share-menu ${shareMenuUpward ? 'upward' : ''}`}>
|
||||||
{/* 카카오톡 */}
|
{/* 카카오톡 */}
|
||||||
<button className="video-detail-share-item" onClick={handleKakaoShare}>
|
<button className="video-detail-share-item" onClick={handleKakaoShare}>
|
||||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|||||||
@ -559,7 +559,8 @@
|
|||||||
},
|
},
|
||||||
"loginPrompt": {
|
"loginPrompt": {
|
||||||
"title": "Login Required",
|
"title": "Login Required",
|
||||||
"loginBtn": "Login"
|
"description": "Sign in with Kakao in a few seconds\nto keep going.",
|
||||||
|
"loginBtn": "Continue with Kakao"
|
||||||
},
|
},
|
||||||
"videoDetail": {
|
"videoDetail": {
|
||||||
"dateFormat": "{{month}}/{{day}}/{{year}}",
|
"dateFormat": "{{month}}/{{day}}/{{year}}",
|
||||||
|
|||||||
@ -607,7 +607,8 @@
|
|||||||
},
|
},
|
||||||
"loginPrompt": {
|
"loginPrompt": {
|
||||||
"title": "로그인이 필요합니다.",
|
"title": "로그인이 필요합니다.",
|
||||||
"loginBtn": "로그인"
|
"description": "카카오 계정으로 간편하게 로그인하고\n이어서 진행해보세요.",
|
||||||
|
"loginBtn": "카카오 로그인"
|
||||||
},
|
},
|
||||||
"videoDetail": {
|
"videoDetail": {
|
||||||
"dateFormat": "{{year}}년 {{month}}월 {{day}}일",
|
"dateFormat": "{{year}}년 {{month}}월 {{day}}일",
|
||||||
|
|||||||
@ -434,7 +434,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
|
|||||||
|
|
||||||
// ── 탭 바 ────────────────────────────────────────────────────
|
// ── 탭 바 ────────────────────────────────────────────────────
|
||||||
const renderTabs = () => (
|
const renderTabs = () => (
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'center', width: '100%' }}>
|
||||||
{tabs.map(tab => (
|
{tabs.map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ const SsulDetailContent: React.FC<SsulDetailContentProps> = ({
|
|||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [shareMenuOpen, setShareMenuOpen] = useState(false);
|
const [shareMenuOpen, setShareMenuOpen] = useState(false);
|
||||||
|
const [shareMenuUpward, setShareMenuUpward] = useState(false);
|
||||||
const [isLandscape, setIsLandscape] = useState(false);
|
const [isLandscape, setIsLandscape] = useState(false);
|
||||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||||
|
|
||||||
@ -184,6 +185,23 @@ const SsulDetailContent: React.FC<SsulDetailContentProps> = ({
|
|||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, [shareMenuOpen]);
|
}, [shareMenuOpen]);
|
||||||
|
|
||||||
|
// 모달 내부는 스크롤 영역(.video-detail-body)이 곧 화면 경계라, 버튼이 그 하단에
|
||||||
|
// 가까우면 아래로 펼치는 메뉴가 잘려서 안 보인다. 열기 직전 남은 공간을 재서 뒤집는다.
|
||||||
|
const handleToggleShareMenu = () => {
|
||||||
|
if (!shareMenuOpen) {
|
||||||
|
const btnRect = shareMenuRef.current?.getBoundingClientRect();
|
||||||
|
if (btnRect) {
|
||||||
|
const scrollBox = shareMenuRef.current?.closest('.video-detail-body');
|
||||||
|
const boundaryBottom = scrollBox
|
||||||
|
? scrollBox.getBoundingClientRect().bottom
|
||||||
|
: window.innerHeight;
|
||||||
|
const ESTIMATED_MENU_HEIGHT = 220;
|
||||||
|
setShareMenuUpward(btnRect.bottom + ESTIMATED_MENU_HEIGHT > boundaryBottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setShareMenuOpen(v => !v);
|
||||||
|
};
|
||||||
|
|
||||||
const likeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const likeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const handleLike = () => {
|
const handleLike = () => {
|
||||||
@ -334,7 +352,7 @@ const SsulDetailContent: React.FC<SsulDetailContentProps> = ({
|
|||||||
<div style={{ position: 'relative' }} ref={shareMenuRef}>
|
<div style={{ position: 'relative' }} ref={shareMenuRef}>
|
||||||
<button
|
<button
|
||||||
className="video-detail-copy-btn"
|
className="video-detail-copy-btn"
|
||||||
onClick={() => setShareMenuOpen((v) => !v)}
|
onClick={handleToggleShareMenu}
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
<circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" />
|
<circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" />
|
||||||
@ -343,7 +361,7 @@ const SsulDetailContent: React.FC<SsulDetailContentProps> = ({
|
|||||||
{copied ? t('videoDetail.copied') : t('videoDetail.share')}
|
{copied ? t('videoDetail.copied') : t('videoDetail.share')}
|
||||||
</button>
|
</button>
|
||||||
{shareMenuOpen && (
|
{shareMenuOpen && (
|
||||||
<div className="video-detail-share-menu">
|
<div className={`video-detail-share-menu ${shareMenuUpward ? 'upward' : ''}`}>
|
||||||
<button className="video-detail-share-item" onClick={handleKakaoShare}>
|
<button className="video-detail-share-item" onClick={handleKakaoShare}>
|
||||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect width="20" height="20" rx="4" fill="#FEE500" />
|
<rect width="20" height="20" rx="4" fill="#FEE500" />
|
||||||
|
|||||||
@ -47,8 +47,10 @@ const SsulResultContent: React.FC<SsulResultContentProps> = ({ taskId, scenario
|
|||||||
};
|
};
|
||||||
}, [taskId]);
|
}, [taskId]);
|
||||||
|
|
||||||
// 파일명은 프론트가 정한다(백엔드는 title 을 갖지 않는다)
|
// 파일명은 ADO2 완료 화면과 같은 방식이다 — 업장명 그대로 + .mp4.
|
||||||
const fileName = `ssulbox-${scenario}-${taskId}.mp4`;
|
// store_name 은 완료(done) 시점에 서버가 반드시 채워 보내지만, 조회 실패로
|
||||||
|
// task 가 없을 수 있어 ADO2 와 동일한 폴백('콘텐츠')을 둔다.
|
||||||
|
const fileName = `${task?.store_name || '콘텐츠'}.mp4`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 영상 다운로드.
|
* 영상 다운로드.
|
||||||
|
|||||||
@ -747,7 +747,7 @@
|
|||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
margin: 0;
|
margin-left: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-detail-back-btn {
|
.video-detail-back-btn {
|
||||||
@ -769,7 +769,7 @@
|
|||||||
|
|
||||||
.video-detail-content {
|
.video-detail-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 32px;
|
gap: 16px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -867,6 +867,12 @@
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 버튼 아래 공간이 부족하면(모달 스크롤 영역 하단 근처) 위로 펼친다 */
|
||||||
|
.video-detail-share-menu.upward {
|
||||||
|
top: auto;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
}
|
||||||
|
|
||||||
.video-detail-share-item {
|
.video-detail-share-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -369,8 +369,13 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
/* 부모(.wizard-page-container)가 height:100% flex column 이다.
|
/* ssulbox.css 의 베이스 `.ssul-scope { height:100% }` (원본 전체화면 셸 규칙)가
|
||||||
`0 auto` 면 수평만 중앙이라 스텝퍼 바로 아래에 붙고 세로 여백이 전부
|
여기까지 상속되어 이 박스 자체가 부모 높이를 꽉 채워버린다. 그러면 아래
|
||||||
|
margin:auto 가 나눠 가질 여유 공간이 0 이 되어 컨텐츠가 위로 쏠린다.
|
||||||
|
height:auto 로 되돌려 컨텐츠 크기만큼만 차지하게 해야 margin:auto 가 뜻대로
|
||||||
|
작동한다(부모 .wizard-page-container 가 height:100% flex column). */
|
||||||
|
height: auto;
|
||||||
|
/* `0 auto` 면 수평만 중앙이라 스텝퍼 바로 아래에 붙고 세로 여백이 전부
|
||||||
아래로 몰린다. `auto` 는 상하 여백도 균등 분배해 스텝퍼~바닥 사이
|
아래로 몰린다. `auto` 는 상하 여백도 균등 분배해 스텝퍼~바닥 사이
|
||||||
중앙에 놓인다. */
|
중앙에 놓인다. */
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|||||||
@ -1461,6 +1461,11 @@ export interface SsulTaskStatus {
|
|||||||
step: number;
|
step: number;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
video_url: string | null;
|
video_url: string | null;
|
||||||
|
/**
|
||||||
|
* 대상 업장명. `status === 'done'` 이면 서버가 채워 보낸다.
|
||||||
|
* 생성 중에는 비어 있을 수 있다 — 행은 요청 즉시 만들어지고 업장명은 그 뒤에 확정된다.
|
||||||
|
*/
|
||||||
|
store_name: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user