refactor: 일정 페이지 상태 관리를 zustand store로 변경

- Schedule.jsx에서 useState 대신 useScheduleStore 사용
- 상세 페이지 이동 후에도 선택한 날짜/카테고리/검색어 유지
- X 상세 페이지 UI 개선 (X 아이콘 제거, 날짜 형식 변경)
- X 프로필 URL 디코딩 로직 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-15 15:19:44 +09:00
parent 4f0cf724d0
commit edf6b60b4a
4 changed files with 64 additions and 31 deletions

View file

@ -150,17 +150,17 @@ function extractProfileFromHtml(html) {
avatarUrl: null,
};
// Display name 추출: <a class="profile-card-fullname" ...>이름</a>
// Display name 추출: <a class="profile-card-fullname" ... title="이름">이름</a>
const nameMatch = html.match(
/<a[^>]*class="profile-card-fullname"[^>]*>([^<]+)<\/a>/
/class="profile-card-fullname"[^>]*title="([^"]+)"/
);
if (nameMatch) {
profile.displayName = nameMatch[1].trim();
}
// Avatar URL 추출: <a class="profile-card-avatar" ...><img src="...">
// Avatar URL 추출: <a class="profile-card-avatar" ...><img src="/pic/...">
const avatarMatch = html.match(
/<a[^>]*class="profile-card-avatar"[^>]*>[\s\S]*?<img[^>]*src="([^"]+)"/
/class="profile-card-avatar"[^>]*>[\s\S]*?<img[^>]*src="([^"]+)"/
);
if (avatarMatch) {
profile.avatarUrl = avatarMatch[1];
@ -174,10 +174,17 @@ function extractProfileFromHtml(html) {
*/
async function cacheXProfile(username, profile, nitterUrl) {
try {
// Nitter URL이 상대 경로인 경우 절대 경로로 변환
// Nitter 프록시 URL에서 원본 Twitter 이미지 URL 추출
let avatarUrl = profile.avatarUrl;
if (avatarUrl && avatarUrl.startsWith("/")) {
avatarUrl = `${nitterUrl}${avatarUrl}`;
if (avatarUrl) {
// /pic/https%3A%2F%2Fpbs.twimg.com%2F... 형식에서 원본 URL 추출
const encodedMatch = avatarUrl.match(/\/pic\/(.+)/);
if (encodedMatch) {
avatarUrl = decodeURIComponent(encodedMatch[1]);
} else if (avatarUrl.startsWith("/")) {
// 상대 경로인 경우 Nitter URL 추가
avatarUrl = `${nitterUrl}${avatarUrl}`;
}
}
const data = {

View file

@ -7,6 +7,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
import { useInView } from 'react-intersection-observer';
import { getTodayKST } from '../../../utils/date';
import { getSchedules, getCategories, searchSchedules as searchSchedulesApi } from '../../../api/public/schedules';
import useScheduleStore from '../../../stores/useScheduleStore';
// HTML
const decodeHtmlEntities = (text) => {
@ -18,18 +19,32 @@ const decodeHtmlEntities = (text) => {
function Schedule() {
const navigate = useNavigate();
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedDate, setSelectedDate] = useState(getTodayKST()); // KST
// (zustand store)
const {
currentDate,
setCurrentDate,
selectedDate: storedSelectedDate,
setSelectedDate: setStoredSelectedDate,
selectedCategories,
setSelectedCategories,
isSearchMode,
setIsSearchMode,
searchInput,
setSearchInput,
searchTerm,
setSearchTerm,
} = useScheduleStore();
// (store )
const selectedDate = storedSelectedDate === undefined ? getTodayKST() : storedSelectedDate;
const setSelectedDate = setStoredSelectedDate;
const [showYearMonthPicker, setShowYearMonthPicker] = useState(false);
const [viewMode, setViewMode] = useState('yearMonth');
const [slideDirection, setSlideDirection] = useState(0);
const pickerRef = useRef(null);
//
const [selectedCategories, setSelectedCategories] = useState([]);
// (useQuery)
const { data: categories = [] } = useQuery({
queryKey: ['scheduleCategories'],
@ -49,12 +64,9 @@ function Schedule() {
const categoryRef = useRef(null);
const scrollContainerRef = useRef(null); //
const searchContainerRef = useRef(null); // ( )
//
const [isSearchMode, setIsSearchMode] = useState(false);
const [searchInput, setSearchInput] = useState(''); //
// (store )
const [originalSearchQuery, setOriginalSearchQuery] = useState(''); // ()
const [searchTerm, setSearchTerm] = useState('');
const [showSuggestions, setShowSuggestions] = useState(false);
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
const [suggestions, setSuggestions] = useState([]); //

View file

@ -48,6 +48,26 @@ const formatTime = (timeStr) => {
return timeStr.slice(0, 5);
};
// X / ( 2:30 · 2026 1 15)
const formatXDateTime = (dateStr, timeStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
let result = `${year}${month}${day}`;
if (timeStr) {
const [hours, minutes] = timeStr.split(':').map(Number);
const period = hours < 12 ? '오전' : '오후';
const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
result = `${period} ${hour12}:${String(minutes).padStart(2, '0')} · ${result}`;
}
return result;
};
// ()
function VideoInfo({ schedule, isShorts }) {
return (
@ -242,17 +262,13 @@ function XSection({ schedule }) {
<span className="text-sm text-gray-500">@{username}</span>
)}
</div>
{/* X 로고 */}
<svg className="w-6 h-6 text-gray-400" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</div>
</div>
{/* 본문 */}
<div className="p-5">
<p className="text-gray-900 text-[17px] leading-relaxed whitespace-pre-wrap">
{decodeHtmlEntities(schedule.title)}
{decodeHtmlEntities(schedule.description || schedule.title)}
</p>
</div>
@ -269,11 +285,9 @@ function XSection({ schedule }) {
{/* 날짜/시간 */}
<div className="px-5 py-4 border-t border-gray-100">
<div className="flex items-center gap-2 text-gray-500 text-[15px]">
<span>{formatTime(schedule.time)}</span>
{schedule.time && <span>·</span>}
<span>{formatFullDate(schedule.date)}</span>
</div>
<span className="text-gray-500 text-[15px]">
{formatXDateTime(schedule.date, schedule.time)}
</span>
</div>
{/* X에서 보기 버튼 */}

View file

@ -10,7 +10,7 @@ const useScheduleStore = create((set) => ({
// 필터 및 선택
selectedCategories: [],
selectedDate: null, // null이면 전체보기, undefined이면 getTodayKST() 사용
selectedDate: undefined, // undefined면 오늘 날짜 사용, null이면 전체보기
currentDate: new Date(),
// 스크롤 위치
@ -32,7 +32,7 @@ const useScheduleStore = create((set) => ({
searchTerm: "",
isSearchMode: false,
selectedCategories: [],
selectedDate: null,
selectedDate: undefined,
currentDate: new Date(),
scrollPosition: 0,
}),