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,
|
avatarUrl: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Display name 추출: <a class="profile-card-fullname" ...>이름</a>
|
// Display name 추출: <a class="profile-card-fullname" ... title="이름">이름</a>
|
||||||
const nameMatch = html.match(
|
const nameMatch = html.match(
|
||||||
/<a[^>]*class="profile-card-fullname"[^>]*>([^<]+)<\/a>/
|
/class="profile-card-fullname"[^>]*title="([^"]+)"/
|
||||||
);
|
);
|
||||||
if (nameMatch) {
|
if (nameMatch) {
|
||||||
profile.displayName = nameMatch[1].trim();
|
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(
|
const avatarMatch = html.match(
|
||||||
/<a[^>]*class="profile-card-avatar"[^>]*>[\s\S]*?<img[^>]*src="([^"]+)"/
|
/class="profile-card-avatar"[^>]*>[\s\S]*?<img[^>]*src="([^"]+)"/
|
||||||
);
|
);
|
||||||
if (avatarMatch) {
|
if (avatarMatch) {
|
||||||
profile.avatarUrl = avatarMatch[1];
|
profile.avatarUrl = avatarMatch[1];
|
||||||
|
|
@ -174,10 +174,17 @@ function extractProfileFromHtml(html) {
|
||||||
*/
|
*/
|
||||||
async function cacheXProfile(username, profile, nitterUrl) {
|
async function cacheXProfile(username, profile, nitterUrl) {
|
||||||
try {
|
try {
|
||||||
// Nitter URL이 상대 경로인 경우 절대 경로로 변환
|
// Nitter 프록시 URL에서 원본 Twitter 이미지 URL 추출
|
||||||
let avatarUrl = profile.avatarUrl;
|
let avatarUrl = profile.avatarUrl;
|
||||||
if (avatarUrl && avatarUrl.startsWith("/")) {
|
if (avatarUrl) {
|
||||||
avatarUrl = `${nitterUrl}${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 = {
|
const data = {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { getTodayKST } from '../../../utils/date';
|
import { getTodayKST } from '../../../utils/date';
|
||||||
import { getSchedules, getCategories, searchSchedules as searchSchedulesApi } from '../../../api/public/schedules';
|
import { getSchedules, getCategories, searchSchedules as searchSchedulesApi } from '../../../api/public/schedules';
|
||||||
|
import useScheduleStore from '../../../stores/useScheduleStore';
|
||||||
|
|
||||||
// HTML 엔티티 디코딩 함수
|
// HTML 엔티티 디코딩 함수
|
||||||
const decodeHtmlEntities = (text) => {
|
const decodeHtmlEntities = (text) => {
|
||||||
|
|
@ -18,18 +19,32 @@ const decodeHtmlEntities = (text) => {
|
||||||
|
|
||||||
function Schedule() {
|
function Schedule() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
// 상태 관리 (zustand store)
|
||||||
const [selectedDate, setSelectedDate] = useState(getTodayKST()); // KST 기준 오늘
|
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 [showYearMonthPicker, setShowYearMonthPicker] = useState(false);
|
||||||
const [viewMode, setViewMode] = useState('yearMonth');
|
const [viewMode, setViewMode] = useState('yearMonth');
|
||||||
const [slideDirection, setSlideDirection] = useState(0);
|
const [slideDirection, setSlideDirection] = useState(0);
|
||||||
const pickerRef = useRef(null);
|
const pickerRef = useRef(null);
|
||||||
|
|
||||||
|
|
||||||
// 데이터 상태
|
|
||||||
const [selectedCategories, setSelectedCategories] = useState([]);
|
|
||||||
|
|
||||||
// 카테고리 데이터 로드 (useQuery)
|
// 카테고리 데이터 로드 (useQuery)
|
||||||
const { data: categories = [] } = useQuery({
|
const { data: categories = [] } = useQuery({
|
||||||
queryKey: ['scheduleCategories'],
|
queryKey: ['scheduleCategories'],
|
||||||
|
|
@ -49,12 +64,9 @@ function Schedule() {
|
||||||
const categoryRef = useRef(null);
|
const categoryRef = useRef(null);
|
||||||
const scrollContainerRef = useRef(null); // 일정 목록 스크롤 컨테이너
|
const scrollContainerRef = useRef(null); // 일정 목록 스크롤 컨테이너
|
||||||
const searchContainerRef = useRef(null); // 검색 컨테이너 (외부 클릭 감지용)
|
const searchContainerRef = useRef(null); // 검색 컨테이너 (외부 클릭 감지용)
|
||||||
|
|
||||||
// 검색 상태
|
// 검색 관련 로컬 상태 (store에서 관리하지 않는 것들)
|
||||||
const [isSearchMode, setIsSearchMode] = useState(false);
|
|
||||||
const [searchInput, setSearchInput] = useState(''); // 입력창에 표시되는 값
|
|
||||||
const [originalSearchQuery, setOriginalSearchQuery] = useState(''); // 사용자가 직접 입력한 원본 값 (필터링용)
|
const [originalSearchQuery, setOriginalSearchQuery] = useState(''); // 사용자가 직접 입력한 원본 값 (필터링용)
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
|
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
|
||||||
const [suggestions, setSuggestions] = useState([]); // 추천 검색어 목록
|
const [suggestions, setSuggestions] = useState([]); // 추천 검색어 목록
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,26 @@ const formatTime = (timeStr) => {
|
||||||
return timeStr.slice(0, 5);
|
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 }) {
|
function VideoInfo({ schedule, isShorts }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -242,17 +262,13 @@ function XSection({ schedule }) {
|
||||||
<span className="text-sm text-gray-500">@{username}</span>
|
<span className="text-sm text-gray-500">@{username}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* 본문 */}
|
{/* 본문 */}
|
||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
<p className="text-gray-900 text-[17px] leading-relaxed whitespace-pre-wrap">
|
<p className="text-gray-900 text-[17px] leading-relaxed whitespace-pre-wrap">
|
||||||
{decodeHtmlEntities(schedule.title)}
|
{decodeHtmlEntities(schedule.description || schedule.title)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -269,11 +285,9 @@ function XSection({ schedule }) {
|
||||||
|
|
||||||
{/* 날짜/시간 */}
|
{/* 날짜/시간 */}
|
||||||
<div className="px-5 py-4 border-t border-gray-100">
|
<div className="px-5 py-4 border-t border-gray-100">
|
||||||
<div className="flex items-center gap-2 text-gray-500 text-[15px]">
|
<span className="text-gray-500 text-[15px]">
|
||||||
<span>{formatTime(schedule.time)}</span>
|
{formatXDateTime(schedule.date, schedule.time)}
|
||||||
{schedule.time && <span>·</span>}
|
</span>
|
||||||
<span>{formatFullDate(schedule.date)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* X에서 보기 버튼 */}
|
{/* X에서 보기 버튼 */}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ const useScheduleStore = create((set) => ({
|
||||||
|
|
||||||
// 필터 및 선택
|
// 필터 및 선택
|
||||||
selectedCategories: [],
|
selectedCategories: [],
|
||||||
selectedDate: null, // null이면 전체보기, undefined이면 getTodayKST() 사용
|
selectedDate: undefined, // undefined면 오늘 날짜 사용, null이면 전체보기
|
||||||
currentDate: new Date(),
|
currentDate: new Date(),
|
||||||
|
|
||||||
// 스크롤 위치
|
// 스크롤 위치
|
||||||
|
|
@ -32,7 +32,7 @@ const useScheduleStore = create((set) => ({
|
||||||
searchTerm: "",
|
searchTerm: "",
|
||||||
isSearchMode: false,
|
isSearchMode: false,
|
||||||
selectedCategories: [],
|
selectedCategories: [],
|
||||||
selectedDate: null,
|
selectedDate: undefined,
|
||||||
currentDate: new Date(),
|
currentDate: new Date(),
|
||||||
scrollPosition: 0,
|
scrollPosition: 0,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue