diff --git a/src/pages/subscription/Subscription.css b/src/pages/subscription/Subscription.css index 21c1f60..e6beded 100644 --- a/src/pages/subscription/Subscription.css +++ b/src/pages/subscription/Subscription.css @@ -1571,3 +1571,86 @@ input.sub-toggle:checked + .sub-toggle__label { color: var(--accent-subscription .ns-pass-count strong { font-weight: 700; } + +/* === 캘린더 뷰 ========================================================= */ +.sub-calendar { + background: var(--bg-secondary); + border: 1px solid var(--line); + border-radius: var(--radius-md); + overflow: hidden; +} +.sub-calendar__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + border-bottom: 1px solid var(--line); + font-weight: 600; + font-size: 14px; + color: var(--text-bright); +} +.sub-calendar__weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + background: var(--bg-tertiary); + border-bottom: 1px solid var(--line); +} +.sub-calendar__weekday { + text-align: center; + padding: 6px 0; + font-size: 10px; + color: var(--text-dim); + font-weight: 600; +} +.sub-calendar__grid { + display: grid; + grid-template-columns: repeat(7, 1fr); +} +.sub-calendar__day { + min-height: 58px; + padding: 5px 4px 4px; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); + display: flex; + flex-direction: column; + gap: 3px; + cursor: default; + transition: background 0.1s; +} +.sub-calendar__day:nth-child(7n) { border-right: none; } +.sub-calendar__day.is-empty { background: var(--bg-tertiary); opacity: 0.35; } +.sub-calendar__day.has-items { cursor: pointer; } +.sub-calendar__day.has-items:hover { background: var(--surface-raised); } +.sub-calendar__day.is-today .sub-calendar__day-num { + background: var(--accent-cyan, #00d4ff); + color: var(--bg-primary); + border-radius: 50%; +} +.sub-calendar__day-num { + font-size: 11px; + color: var(--text-muted); + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + font-weight: 500; +} +.sub-calendar__dots { + display: flex; + flex-wrap: wrap; + gap: 2px; + padding: 0 2px; +} +.sub-calendar__dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} +.sub-calendar__more { + font-size: 9px; + color: var(--text-dim); + line-height: 1.5; +} diff --git a/src/pages/subscription/Subscription.jsx b/src/pages/subscription/Subscription.jsx index 1dfa2c9..d8a0ae1 100644 --- a/src/pages/subscription/Subscription.jsx +++ b/src/pages/subscription/Subscription.jsx @@ -688,6 +688,73 @@ function AnnouncementDetail({ item, onBookmark }) { ); } +// ── CalendarView ───────────────────────────────────────────────────────────── +function CalendarView({ items, onDaySelect }) { + const [cur, setCur] = useState(() => { + const n = new Date(); return new Date(n.getFullYear(), n.getMonth(), 1); + }); + const year = cur.getFullYear(), month = cur.getMonth(); + + const dateMap = useMemo(() => { + const map = {}; + for (const item of items) { + const raw = item.receipt_start || item.spsply_start || item.gnrl_rank1_start; + if (!raw || raw.length < 8) continue; + const key = `${raw.slice(0,4)}-${raw.slice(4,6)}-${raw.slice(6,8)}`; + (map[key] = map[key] || []).push(item); + } + return map; + }, [items]); + + const firstDow = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const cells = []; + for (let i = 0; i < firstDow; i++) cells.push(null); + for (let d = 1; d <= daysInMonth; d++) cells.push(d); + while (cells.length % 7 !== 0) cells.push(null); + + const todayD = new Date(), todayKey = `${todayD.getFullYear()}-${String(todayD.getMonth()+1).padStart(2,'0')}-${String(todayD.getDate()).padStart(2,'0')}`; + + return ( +
+
+ + {year}년 {month+1}월 + +
+
+ {['일','월','화','수','목','금','토'].map(w => ( +
{w}
+ ))} +
+
+ {cells.map((d, i) => { + const key = d ? `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}` : null; + const dayItems = key ? (dateMap[key] || []) : []; + const isToday = key === todayKey; + return ( +
0 ? ' has-items' : ''}`} + onClick={() => dayItems.length > 0 && onDaySelect(dayItems, `${year}년 ${month+1}월 ${d}일`)} + > + {d && {d}} + {dayItems.length > 0 && ( +
+ {dayItems.slice(0, 3).map((it, j) => ( + + ))} + {dayItems.length > 3 && +{dayItems.length - 3}} +
+ )} +
+ ); + })} +
+
+ ); +} + // ── AnnouncementsTab ───────────────────────────────────────────────────────── function AnnouncementsTab() { const [items, setItems] = useState([]); @@ -699,8 +766,10 @@ function AnnouncementsTab() { const [selected, setSelected] = useState(null); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); + const [viewMode, setViewMode] = useState('list'); // 'list' | 'calendar' + const [calendarDay, setCalendarDay] = useState(null); // { label, items } - const size = 20; + const size = viewMode === 'calendar' ? 200 : 20; const load = async () => { setLoading(true); @@ -720,7 +789,7 @@ function AnnouncementsTab() { } }; - useEffect(() => { load(); }, [page, statusFilter, regionFilter, bookmarkFilter]); + useEffect(() => { load(); }, [page, statusFilter, regionFilter, bookmarkFilter, viewMode]); const handleSelect = async (item) => { setSelected(item.id); @@ -790,6 +859,14 @@ function AnnouncementsTab() { onChange={(e) => { setRegionFilter(e.target.value); setPage(1); }} style={{ width: 160, padding: '6px 12px', fontSize: 12 }} /> + + +
+ {calendarDay.items.map(item => ( +
{ setViewMode('list'); handleSelect(item); }} + > +
+ {item.house_nm} + + {item.status} + +
+ {item.region_name} · 접수 {item.receipt_start} +
+ ))} +
+ + )} + ) : (
{/* Card Grid */}