fix: 로그인 유도 모달 개편과 공유 메뉴 방향·다운로드 파일명·정렬 수정

This commit is contained in:
김성경 2026-08-11 14:57:35 +09:00
parent 1c01dab748
commit 18b5271110
10 changed files with 106 additions and 26 deletions

View File

@ -26,22 +26,24 @@ const LoginPromptModal: React.FC<LoginPromptModalProps> = ({ onClose }) => {
style={{
position: 'fixed',
inset: 0,
background: 'rgba(255, 255, 255, 0.25)',
background: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999,
padding: '16px',
}}
{...overlayCloseHandlers}
>
<div
style={{
background: '#01282A',
border: '1px solid rgba(155, 202, 204, 0.2)',
borderRadius: '16px',
padding: '40px 32px',
width: '360px',
maxWidth: '90vw',
border: '1px solid rgba(166, 255, 234, 0.15)',
borderRadius: '20px',
boxShadow: '0 24px 60px rgba(0, 0, 0, 0.5)',
padding: '44px 32px 36px',
width: '380px',
maxWidth: '100%',
textAlign: 'center',
position: 'relative',
}}
@ -66,27 +68,49 @@ const LoginPromptModal: React.FC<LoginPromptModalProps> = ({ onClose }) => {
<line x1="16" y1="4" x2="4" y2="16"/>
</svg>
</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')}
</h2>
<p style={{ color: '#9BCACC', fontSize: '14px', lineHeight: 1.5, marginBottom: '28px', whiteSpace: 'pre-line' }}>
{t('loginPrompt.description')}
</p>
<button
onClick={handleLogin}
style={{
width: '50%',
margin: '0 auto',
padding: '14px',
width: '100%',
padding: '15px',
background: '#FEE500',
color: '#3C1E1E',
border: 'none',
borderRadius: '8px',
fontSize: '15px',
fontWeight: 600,
borderRadius: '10px',
fontSize: '16px',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
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">

View File

@ -30,6 +30,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
const [copied, setCopied] = useState(false);
const [shareMenuOpen, setShareMenuOpen] = useState(false);
const [shareMenuUpward, setShareMenuUpward] = useState(false);
const [isLandscape, setIsLandscape] = useState(false);
const [showLoginModal, setShowLoginModal] = useState(false);
@ -158,6 +159,23 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [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 handleLike = () => {
@ -292,7 +310,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
<div style={{ position: 'relative' }} ref={shareMenuRef}>
<button
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">
<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')}
</button>
{shareMenuOpen && (
<div className="video-detail-share-menu">
<div className={`video-detail-share-menu ${shareMenuUpward ? 'upward' : ''}`}>
{/* 카카오톡 */}
<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">

View File

@ -559,7 +559,8 @@
},
"loginPrompt": {
"title": "Login Required",
"loginBtn": "Login"
"description": "Sign in with Kakao in a few seconds\nto keep going.",
"loginBtn": "Continue with Kakao"
},
"videoDetail": {
"dateFormat": "{{month}}/{{day}}/{{year}}",

View File

@ -607,7 +607,8 @@
},
"loginPrompt": {
"title": "로그인이 필요합니다.",
"loginBtn": "로그인"
"description": "카카오 계정으로 간편하게 로그인하고\n이어서 진행해보세요.",
"loginBtn": "카카오 로그인"
},
"videoDetail": {
"dateFormat": "{{year}}년 {{month}}월 {{day}}일",

View File

@ -434,7 +434,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
// ── 탭 바 ────────────────────────────────────────────────────
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 => (
<button
key={tab}

View File

@ -45,6 +45,7 @@ const SsulDetailContent: React.FC<SsulDetailContentProps> = ({
const [copied, setCopied] = useState(false);
const [shareMenuOpen, setShareMenuOpen] = useState(false);
const [shareMenuUpward, setShareMenuUpward] = useState(false);
const [isLandscape, setIsLandscape] = useState(false);
const [showLoginModal, setShowLoginModal] = useState(false);
@ -184,6 +185,23 @@ const SsulDetailContent: React.FC<SsulDetailContentProps> = ({
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [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 handleLike = () => {
@ -334,7 +352,7 @@ const SsulDetailContent: React.FC<SsulDetailContentProps> = ({
<div style={{ position: 'relative' }} ref={shareMenuRef}>
<button
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">
<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')}
</button>
{shareMenuOpen && (
<div className="video-detail-share-menu">
<div className={`video-detail-share-menu ${shareMenuUpward ? 'upward' : ''}`}>
<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">
<rect width="20" height="20" rx="4" fill="#FEE500" />

View File

@ -47,8 +47,10 @@ const SsulResultContent: React.FC<SsulResultContentProps> = ({ taskId, scenario
};
}, [taskId]);
// 파일명은 프론트가 정한다(백엔드는 title 을 갖지 않는다)
const fileName = `ssulbox-${scenario}-${taskId}.mp4`;
// 파일명은 ADO2 완료 화면과 같은 방식이다 — 업장명 그대로 + .mp4.
// store_name 은 완료(done) 시점에 서버가 반드시 채워 보내지만, 조회 실패로
// task 가 없을 수 있어 ADO2 와 동일한 폴백('콘텐츠')을 둔다.
const fileName = `${task?.store_name || '콘텐츠'}.mp4`;
/**
* .

View File

@ -747,7 +747,7 @@
font-size: 24px;
font-weight: 600;
color: #FFFFFF;
margin: 0;
margin-left: 24px;
}
.video-detail-back-btn {
@ -769,7 +769,7 @@
.video-detail-content {
display: flex;
gap: 32px;
gap: 16px;
flex-wrap: wrap;
align-items: center;
flex: 1;
@ -867,6 +867,12 @@
gap: 2px;
}
/* 버튼 아래 공간이 부족하면(모달 스크롤 영역 하단 근처) 위로 펼친다 */
.video-detail-share-menu.upward {
top: auto;
bottom: calc(100% + 6px);
}
.video-detail-share-item {
display: flex;
align-items: center;

View File

@ -369,8 +369,13 @@
align-items: center;
width: 100%;
max-width: 420px;
/* 부모(.wizard-page-container) height:100% flex column 이다.
`0 auto` 수평만 중앙이라 스텝퍼 바로 아래에 붙고 세로 여백이 전부
/* ssulbox.css 베이스 `.ssul-scope { height:100% }` (원본 전체화면 규칙)
여기까지 상속되어 박스 자체가 부모 높이를 채워버린다. 그러면 아래
margin:auto 나눠 가질 여유 공간이 0 되어 컨텐츠가 위로 쏠린다.
height:auto 되돌려 컨텐츠 크기만큼만 차지하게 해야 margin:auto 뜻대로
작동한다(부모 .wizard-page-container height:100% flex column). */
height: auto;
/* `0 auto` 수평만 중앙이라 스텝퍼 바로 아래에 붙고 세로 여백이 전부
아래로 몰린다. `auto` 상하 여백도 균등 분배해 스텝퍼~바닥 사이
중앙에 놓인다. */
margin: auto;

View File

@ -1461,6 +1461,11 @@ export interface SsulTaskStatus {
step: number;
error: string | null;
video_url: string | null;
/**
* . `status === 'done'` .
* .
*/
store_name: string;
created_at: string;
}