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:
parent
4f0cf724d0
commit
edf6b60b4a
4 changed files with 64 additions and 31 deletions
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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([]); // 추천 검색어 목록
|
||||
|
|
|
|||
|
|
@ -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에서 보기 버튼 */}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue