Compare commits

...

3 commits

Author SHA1 Message Date
7184049186 썬데이 메이플 cron + 윈도우에 목요일 추가 (금요일 공휴일 대응)
금요일이 공휴일이면 목요일에 선공개되는 케이스 지원.

- cron 스케줄: 목/금 9시 ('0 9 * * 4,5')
- currentWeekFriday: 목요일이면 내일 금요일을 week_start 로 (이번 주 사이클로 흡수)
- isInSundayWindow: 목요일도 포함. 해당 주차 row 가 없으면 어차피 available: false

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:56:22 +09:00
928b46f13a 공지/업데이트 섹션 라벨에서 '메이플스토리' 접두 제거
NoticeWidget config 의 notice/update 라벨을 '공지사항' / '업데이트' 로 단축.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:53:58 +09:00
01bbbbd6af GlobalTooltip: delay 중 마우스 벗어나면 title 복원 안 되던 버그 수정
showFor 가 호출되면 title 속성을 즉시 제거하고 titleMap 에 저장한 뒤
setTimeout 으로 지연. 지연 중에 마우스가 벗어나 handleOut 가 호출되어도
triggerRef 는 아직 null 이라 hide() 가 title 을 복원하지 못해 해당 요소의
title 이 영구 제거되고 이후 [title] 선택자에 걸리지 않아 툴팁이 안 뜸.

- pendingRef 추가해 "delay 중 title 제거된 타겟" 추적
- hide / showFor / handleOver / handleOut 모두 pendingRef 고려해 복원
- 타이머 중 다른 타겟으로 이동 / 같은 타겟 반복 hover 케이스 처리

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 08:39:04 +09:00
4 changed files with 54 additions and 28 deletions

View file

@ -25,22 +25,24 @@ export function detectVariant(title) {
/** /**
* 금요일 기준의 이번 주차 시작일 (YYYY-MM-DD, KST) 반환 * 금요일 기준의 이번 주차 시작일 (YYYY-MM-DD, KST) 반환
* // 직전 금요일 * 다음 금요일 (금요일 공휴일로 목요일 선공개되는 경우 대응)
* /// 이전 금요일 * // 이번 금요일
* // 지난 금요일
*/ */
export function currentWeekFriday(now = dayjs().tz(KST)) { export function currentWeekFriday(now = dayjs().tz(KST)) {
const dow = now.day(); // 0=일 ... 5=금 6=토 const dow = now.day(); // 0=일 ... 4=목 5=금 6=토
// 금요일 기준 diff: 금(5)이면 0, 토(6)이면 -1, 일(0)이면 -2, 월(1)이면 -3 ... // 목요일은 내일이 금요일이므로 -1
const diff = dow >= 5 ? dow - 5 : dow + 2; const diff = dow === 4 ? -1 : (dow >= 5 ? dow - 5 : dow + 2);
return now.startOf('day').subtract(diff, 'day').format('YYYY-MM-DD'); return now.startOf('day').subtract(diff, 'day').format('YYYY-MM-DD');
} }
/** /**
* ~일요일인지 * 썬데이 메이플 표시 가능한 요일대 (~)
* 목요일은 금요일 공휴일 케이스 대응용. 해당 주차 row 없으면 어차피 available: false.
*/ */
export function isInSundayWindow(now = dayjs().tz(KST)) { export function isInSundayWindow(now = dayjs().tz(KST)) {
const dow = now.day(); const dow = now.day();
return dow === 5 || dow === 6 || dow === 0; return dow === 4 || dow === 5 || dow === 6 || dow === 0;
} }
/** /**

View file

@ -27,9 +27,9 @@ async function runPolling() {
} }
/** /**
* 매주 요일 09:00 KST 실행 * 매주 /09:00 KST 실행 (금요일이 공휴일이면 목요일에 게시되는 경우 대응)
*/ */
export function scheduleSundayMapleCron() { export function scheduleSundayMapleCron() {
cron.schedule('0 9 * * 5', runPolling, { timezone: 'Asia/Seoul' }); cron.schedule('0 9 * * 4,5', runPolling, { timezone: 'Asia/Seoul' });
console.log('[sunday-maple cron] 매주 요일 09:00 KST 스케줄 등록'); console.log('[sunday-maple cron] 매주 목/금 09:00 KST 스케줄 등록');
} }

View file

@ -17,42 +17,62 @@ const DELAY_DEFAULT = 200
*/ */
export default function GlobalTooltip() { export default function GlobalTooltip() {
const [state, setState] = useState({ open: false, text: '', placement: 'top', coords: null }) const [state, setState] = useState({ open: false, text: '', placement: 'top', coords: null })
const triggerRef = useRef(null) const triggerRef = useRef(null) //
const pendingRef = useRef(null) // delay title ( )
const tooltipRef = useRef(null) const tooltipRef = useRef(null)
const timerRef = useRef(null) const timerRef = useRef(null)
const titleMap = useRef(new WeakMap()) const titleMap = useRef(new WeakMap())
useEffect(() => { useEffect(() => {
function restoreTitle() { function stripTitle(el) {
const el = triggerRef.current const t = el.getAttribute('title')
if (t && !titleMap.current.has(el)) {
titleMap.current.set(el, t)
el.removeAttribute('title')
}
}
function restoreTitle(el) {
if (el && titleMap.current.has(el)) { if (el && titleMap.current.has(el)) {
const prev = titleMap.current.get(el) el.setAttribute('title', titleMap.current.get(el))
el.setAttribute('title', prev)
titleMap.current.delete(el) titleMap.current.delete(el)
} }
} }
function hide() { function hide() {
clearTimeout(timerRef.current) clearTimeout(timerRef.current)
restoreTitle() restoreTitle(pendingRef.current)
restoreTitle(triggerRef.current)
pendingRef.current = null
triggerRef.current = null triggerRef.current = null
setState((s) => (s.open ? { ...s, open: false } : s)) setState((s) => (s.open ? { ...s, open: false } : s))
} }
function showFor(target) { function showFor(target) {
//
if (triggerRef.current === target || pendingRef.current === target) return
// pending
if (pendingRef.current) {
clearTimeout(timerRef.current)
restoreTitle(pendingRef.current)
pendingRef.current = null
}
const nativeTitle = target.getAttribute('title') const nativeTitle = target.getAttribute('title')
const dataTooltip = target.getAttribute('data-tooltip') const dataTooltip = target.getAttribute('data-tooltip')
const text = nativeTitle || dataTooltip const text = nativeTitle || dataTooltip
if (!text) return if (!text) return
if (nativeTitle && !titleMap.current.has(target)) {
titleMap.current.set(target, nativeTitle) if (nativeTitle) stripTitle(target)
target.removeAttribute('title') pendingRef.current = target
}
const placement = target.getAttribute('data-tooltip-placement') || 'top' const placement = target.getAttribute('data-tooltip-placement') || 'top'
const delay = Number(target.getAttribute('data-tooltip-delay')) || DELAY_DEFAULT const delay = Number(target.getAttribute('data-tooltip-delay')) || DELAY_DEFAULT
clearTimeout(timerRef.current) clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
triggerRef.current = target triggerRef.current = target
pendingRef.current = null
setState({ open: true, text, placement, coords: null }) setState({ open: true, text, placement, coords: null })
}, delay) }, delay)
} }
@ -60,19 +80,22 @@ export default function GlobalTooltip() {
function handleOver(e) { function handleOver(e) {
const target = e.target.closest?.('[title], [data-tooltip]') const target = e.target.closest?.('[title], [data-tooltip]')
if (!target) return if (!target) return
if (triggerRef.current === target) return if (triggerRef.current === target || pendingRef.current === target) return
//
// trigger
if (triggerRef.current && triggerRef.current !== target) { if (triggerRef.current && triggerRef.current !== target) {
restoreTitle() restoreTitle(triggerRef.current)
triggerRef.current = null triggerRef.current = null
setState((s) => (s.open ? { ...s, open: false } : s))
} }
showFor(target) showFor(target)
} }
function handleOut(e) { function handleOut(e) {
if (!triggerRef.current) return const active = triggerRef.current || pendingRef.current
if (!active) return
const rt = e.relatedTarget const rt = e.relatedTarget
if (rt && triggerRef.current.contains(rt)) return if (rt && active.contains(rt)) return
hide() hide()
} }
@ -98,7 +121,8 @@ export default function GlobalTooltip() {
window.removeEventListener('scroll', hide, true) window.removeEventListener('scroll', hide, true)
window.removeEventListener('resize', hide) window.removeEventListener('resize', hide)
clearTimeout(timerRef.current) clearTimeout(timerRef.current)
restoreTitle() restoreTitle(pendingRef.current)
restoreTitle(triggerRef.current)
} }
}, []) }, [])

View file

@ -1,6 +1,6 @@
export const SECTIONS = { export const SECTIONS = {
notice: { label: '메이플스토리 공지사항', dataKey: 'notice', pageSize: 5, kind: 'text' }, notice: { label: '공지사항', dataKey: 'notice', pageSize: 5, kind: 'text' },
update: { label: '메이플스토리 업데이트', dataKey: 'update_notice', pageSize: 5, kind: 'text' }, update: { label: '업데이트', dataKey: 'update_notice', pageSize: 5, kind: 'text' },
event: { event: {
label: '진행 중인 이벤트', label: '진행 중인 이벤트',
dataKey: 'event_notice', dataKey: 'event_notice',