썬데이 메이플 다이얼로그 UX 개선
- backdrop 폭/높이 100vw/100dvh 명시 → 뷰포트 하단까지 블러 적용 - 배경 스크롤 잠금 + OverlayScrollbars overscroll-behavior:contain → 다이얼로그 스크롤이 뒷 페이지로 전파되지 않음 - 다이얼로그 닫힘 exit 애니메이션 정상 동작 (AnimatePresence를 부모로 이동) - '공식 공지 보기' 하단 링크 제거 → 우상단 외부 링크 아이콘 버튼 추가 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
18cc1855ac
commit
4720e33f26
1 changed files with 80 additions and 46 deletions
|
|
@ -1,63 +1,96 @@
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'
|
||||||
import { api } from '../../api/client'
|
import { api } from '../../api/client'
|
||||||
|
|
||||||
function SundayMapleDialog({ data, onClose }) {
|
function SundayMapleDialog({ data, onClose }) {
|
||||||
|
// 배경 스크롤 잠금
|
||||||
|
useEffect(() => {
|
||||||
|
const prevBody = document.body.style.overflow
|
||||||
|
const prevHtml = document.documentElement.style.overflow
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
document.documentElement.style.overflow = 'hidden'
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = prevBody
|
||||||
|
document.documentElement.style.overflow = prevHtml
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const iconBtn = "w-8 h-8 rounded-lg backdrop-blur-sm border flex items-center justify-center hover:bg-[var(--row-hover-bg)]"
|
||||||
|
const iconBtnStyle = {
|
||||||
|
background: 'var(--btn-bg)',
|
||||||
|
borderColor: 'var(--btn-border)',
|
||||||
|
color: 'var(--text-emphasis)',
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<motion.div
|
||||||
|
key="backdrop"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.18 }}
|
||||||
|
className="fixed top-0 left-0 z-50 flex items-center justify-center p-4 backdrop-blur-md"
|
||||||
|
style={{
|
||||||
|
background: 'var(--dialog-backdrop)',
|
||||||
|
width: '100vw',
|
||||||
|
height: '100dvh',
|
||||||
|
}}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
key="backdrop"
|
key="dialog"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0, scale: 0.94, y: 8 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0, scale: 0.96, y: 4 }}
|
||||||
transition={{ duration: 0.18 }}
|
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-md"
|
className="relative w-full max-w-[640px] max-h-[90vh] flex flex-col rounded-2xl border shadow-2xl overflow-hidden"
|
||||||
style={{ background: 'var(--dialog-backdrop)' }}
|
style={{
|
||||||
onClick={onClose}
|
background: 'var(--panel-bg)',
|
||||||
|
borderColor: 'var(--panel-border)',
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<motion.div
|
<div className="absolute top-3 right-3 z-10 flex items-center gap-2">
|
||||||
key="dialog"
|
{data.event_post_url && (
|
||||||
initial={{ opacity: 0, scale: 0.94, y: 8 }}
|
<a
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
href={data.event_post_url}
|
||||||
exit={{ opacity: 0, scale: 0.96, y: 4 }}
|
target="_blank"
|
||||||
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
|
rel="noopener noreferrer"
|
||||||
className="relative w-full max-w-[420px] max-h-[90vh] overflow-y-auto rounded-2xl border shadow-2xl"
|
className={iconBtn}
|
||||||
style={{
|
style={iconBtnStyle}
|
||||||
background: 'var(--panel-bg)',
|
aria-label="공식 공지로 이동"
|
||||||
borderColor: 'var(--panel-border)',
|
title="공식 공지로 이동"
|
||||||
}}
|
>
|
||||||
onClick={(e) => e.stopPropagation()}
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||||
>
|
<path d="M10 5H5a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2v-5M14 3h7m0 0v7m0-7L10 14"
|
||||||
|
stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute top-3 right-3 z-10 w-8 h-8 rounded-lg backdrop-blur-sm border flex items-center justify-center text-xl leading-none hover:bg-[var(--row-hover-bg)]"
|
className={`${iconBtn} text-xl leading-none`}
|
||||||
style={{
|
style={iconBtnStyle}
|
||||||
background: 'var(--btn-bg)',
|
|
||||||
borderColor: 'var(--btn-border)',
|
|
||||||
color: 'var(--text-emphasis)',
|
|
||||||
}}
|
|
||||||
aria-label="닫기"
|
aria-label="닫기"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
className="flex-1 min-h-0"
|
||||||
|
style={{ overscrollBehavior: 'contain' }}
|
||||||
|
options={{
|
||||||
|
scrollbars: { theme: 'os-theme-maple os-theme-dark', autoHide: 'leave', autoHideDelay: 800 },
|
||||||
|
overflow: { x: 'hidden', y: 'scroll' },
|
||||||
|
}}
|
||||||
|
defer
|
||||||
|
>
|
||||||
<img src={data.image_url} alt="썬데이 메이플" className="w-full block" />
|
<img src={data.image_url} alt="썬데이 메이플" className="w-full block" />
|
||||||
{data.event_post_url && (
|
</OverlayScrollbarsComponent>
|
||||||
<div className="px-4 py-3 border-t text-center" style={{ borderColor: 'var(--panel-border)' }}>
|
|
||||||
<a
|
|
||||||
href={data.event_post_url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-sm hover:text-[var(--accent-hover-text)]"
|
|
||||||
style={{ color: 'var(--accent)' }}
|
|
||||||
>
|
|
||||||
공식 공지 보기 →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</motion.div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,7 +103,6 @@ export default function SundayMapleBanner() {
|
||||||
staleTime: 10 * 60 * 1000,
|
staleTime: 10 * 60 * 1000,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 이미지 관리에 업로드한 아이콘 URL (variant별)
|
|
||||||
const iconName = data?.variant === 'special' ? '스페셜 썬데이 메이플' : '썬데이 메이플'
|
const iconName = data?.variant === 'special' ? '스페셜 썬데이 메이플' : '썬데이 메이플'
|
||||||
const { data: iconData } = useQuery({
|
const { data: iconData } = useQuery({
|
||||||
queryKey: ['image', iconName],
|
queryKey: ['image', iconName],
|
||||||
|
|
@ -118,7 +150,9 @@ export default function SundayMapleBanner() {
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && <SundayMapleDialog data={data} onClose={() => setOpen(false)} />}
|
<AnimatePresence>
|
||||||
|
{open && <SundayMapleDialog data={data} onClose={() => setOpen(false)} />}
|
||||||
|
</AnimatePresence>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue