-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 저장한 책 및 피드 조회 API 연동 #163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e463191
9c0890a
9eb9b74
6f91636
ce8b05e
c2451e7
4789e6c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { apiClient } from '../index'; | ||
|
|
||
| // 개인적으로 저장한 책 정보 타입 | ||
| export interface SavedBookInMy { | ||
| bookId: number; | ||
| bookTitle: string; | ||
| authorName: string; | ||
| publisher: string; | ||
| bookImageUrl: string; | ||
| isbn: string; | ||
| isSaved: boolean; | ||
| } | ||
|
|
||
| // API 응답 데이터 타입 | ||
| export interface SavedBooksInMyData { | ||
| bookList: SavedBookInMy[]; | ||
| } | ||
|
|
||
| // API 응답 타입 | ||
| export interface SavedBooksInMyResponse { | ||
| isSuccess: boolean; | ||
| code: number; | ||
| message: string; | ||
| data: SavedBooksInMyData; | ||
| } | ||
|
|
||
| // 개인적으로 저장한 책 목록 조회 API 함수 | ||
| export const getSavedBooksInMy = async () => { | ||
| try { | ||
| const response = await apiClient.get<SavedBooksInMyResponse>('/books/saved'); | ||
| return response.data; | ||
| } catch (error) { | ||
| console.error('개인 저장 책 조회 API 오류:', error); | ||
| throw error; | ||
| } | ||
| }; | ||
|
|
||
| /* | ||
| // 사용 예시 | ||
| const savedBooksInMy = await getSavedBooksInMy(); | ||
| console.log(savedBooksInMy.data.bookList); // 개인 저장 책 목록 | ||
| console.log(savedBooksInMy.data.bookList[0].bookTitle); // 첫 번째 책 제목 | ||
| */ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import { apiClient } from '../index'; | ||
|
|
||
| // 개인적으로 저장한 피드 정보 타입 | ||
| export interface SavedFeedInMy { | ||
| feedId: number; | ||
| creatorId: number; | ||
| creatorNickname: string; | ||
| creatorProfileImageUrl: string; | ||
| aliasName: string; | ||
| aliasColor: string; | ||
| postDate: string; | ||
| isbn: string; | ||
| bookTitle: string; | ||
| bookAuthor: string; | ||
| contentBody: string; | ||
| contentUrls: string[]; | ||
| likeCount: number; | ||
| commentCount: number; | ||
| isSaved: boolean; | ||
| isLiked: boolean; | ||
| isWriter: boolean; | ||
| } | ||
|
|
||
| // API 응답 데이터 타입 (무한스크롤 지원) | ||
| export interface SavedFeedsInMyData { | ||
| feedList: SavedFeedInMy[]; | ||
| nextCursor: string; | ||
| isLast: boolean; | ||
| } | ||
|
|
||
| // API 응답 타입 | ||
| export interface SavedFeedsInMyResponse { | ||
| isSuccess: boolean; | ||
| code: number; | ||
| message: string; | ||
| data: SavedFeedsInMyData; | ||
| } | ||
|
|
||
| // 개인적으로 저장한 피드 목록 조회 API 함수 (무한스크롤) | ||
| export const getSavedFeedsInMy = async (cursor: string | null = null) => { | ||
| try { | ||
| const params: { cursor?: string | null } = {}; | ||
| if (cursor !== null) { | ||
| params.cursor = cursor; | ||
| } | ||
|
|
||
| const response = await apiClient.get<SavedFeedsInMyResponse>('/feeds/saved', { | ||
| params, | ||
| }); | ||
| return response.data; | ||
| } catch (error) { | ||
| console.error('개인 저장 피드 조회 API 오류:', error); | ||
| throw error; | ||
| } | ||
| }; | ||
|
|
||
| /* | ||
| // 사용 예시 (무한스크롤) | ||
| const savedFeedsInMy = await getSavedFeedsInMy(); | ||
| console.log(savedFeedsInMy.data.feedList); // 저장된 피드 목록 | ||
| console.log(savedFeedsInMy.data.nextCursor); // 다음 페이지 커서 | ||
| console.log(savedFeedsInMy.data.isLast); // 마지막 페이지 여부 | ||
|
|
||
| // 다음 페이지 로드 | ||
| if (!savedFeedsInMy.data.isLast) { | ||
| const nextPage = await getSavedFeedsInMy(savedFeedsInMy.data.nextCursor); | ||
| } | ||
| */ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,12 +74,20 @@ const Mypage = () => { | |
| }; | ||
|
|
||
| const handleService = () => { | ||
| window.open('https://slashpage.com/thip/xjqy1g2vw7vejm6vd54z', '_blank'); | ||
| window.open('https://slashpage.com/thip/dk58wg2e6yy3zmnqevxz', '_blank'); | ||
| }; | ||
|
Comment on lines
76
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 보안: window.open('_blank') 사용 시 reverse tabnabbing 방어 추가 필요 외부 링크를 새 탭으로 열 때는 opener 참조 차단이 필요합니다. 현재 구현은 탭 하이재킹 위험이 있습니다. 아래처럼 교체하는 것을 권장합니다. - const handleService = () => {
- window.open('https://slashpage.com/thip/dk58wg2e6yy3zmnqevxz', '_blank');
- };
+ const handleService = () => {
+ openExternal('https://slashpage.com/thip/dk58wg2e6yy3zmnqevxz');
+ };
- const handleTerms = () => {
- window.open('https://slashpage.com/thip/7916x82r8y74n24kpyg3', '_blank');
- };
+ const handleTerms = () => {
+ openExternal('https://slashpage.com/thip/7916x82r8y74n24kpyg3');
+ };
- const handleVersion = () => {
- window.open('https://slashpage.com/thip/1q3vdn2p9w93pmxy49pr', '_blank');
- };
+ const handleVersion = () => {
+ openExternal('https://slashpage.com/thip/1q3vdn2p9w93pmxy49pr');
+ };헬퍼 예시(이 파일 상단 또는 공용 유틸에 추가): // utils/window.ts (예시)
export const openExternal = (url: string) => {
const w = window.open(url, '_blank', 'noopener,noreferrer');
if (w) w.opener = null;
};또한, 동일한 형태의 외부 링크가 늘어나는 추세라면 URL들을 상수로 중앙집중화 하는 것도 유지보수에 유리합니다. 예: Also applies to: 80-83, 84-86 🤖 Prompt for AI Agents |
||
|
|
||
| const handleTerms = () => { | ||
| window.open('https://slashpage.com/thip/7916x82r8y74n24kpyg3', '_blank'); | ||
| }; | ||
|
|
||
| const handleVersion = () => { | ||
| window.open('https://slashpage.com/thip/1q3vdn2p9w93pmxy49pr', '_blank'); | ||
| }; | ||
|
|
||
| return ( | ||
| <Wrapper> | ||
| <Header>마이페이지</Header> | ||
| <Header>내 정보</Header> | ||
| <Container> | ||
| <UserProfile> | ||
| <div className="userInfo"> | ||
|
|
@@ -106,15 +114,15 @@ const Mypage = () => { | |
| <MenuGrid> | ||
| <MenuButton src={alert} name="알림설정" isButton onClick={handleAlert} /> | ||
| <MenuButton src={service} name="고객센터" isButton onClick={handleService} /> | ||
| <MenuButton src={notice} name="공지사항" isButton /> | ||
| <MenuButton src={notice} name="공지사항" isButton onClick={handleNotice} /> | ||
| <MenuButton | ||
| src={terms} | ||
| name="개인정보처리방침 & 이용약관" | ||
| isButton | ||
| onClick={handleNotice} | ||
| onClick={handleTerms} | ||
| /> | ||
| <MenuButton src={guide} name="가이드" isButton onClick={handleGuide} /> | ||
| <MenuButton src={ver} name="버젼 1.0" isButton={false} /> | ||
| <MenuButton src={ver} name="버젼 1.0.0" isButton onClick={handleVersion} /> | ||
| </MenuGrid> | ||
| </Section> | ||
| <BottomMenu> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useNavigate } from 'react-router-dom'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState, useEffect } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState, useEffect, useRef, useCallback } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import TitleHeader from '../../components/common/TitleHeader'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import TabBar from '@/components/feed/TabBar'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import FeedPost from '@/components/feed/FeedPost'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -8,32 +8,129 @@ import save from '../../assets/feed/save.svg'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| import activeSave from '../../assets/feed/activeSave.svg'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import styled from '@emotion/styled'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { colors, typography } from '@/styles/global/global'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { mockBooks } from '@/data/bookData'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { mockPosts } from '@/data/postData'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getSavedBooksInMy, type SavedBookInMy } from '@/api/books/getSavedBooksInMy'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getSavedFeedsInMy, type SavedFeedInMy } from '@/api/feeds/getSavedFeedsInMy'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { postSaveBook } from '@/api/books/postSaveBook'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import LoadingSpinner from '@/components/common/LoadingSpinner'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tabs = ['피드', '책']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const SavePage = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hasPosts = mockPosts.length > 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hasBooks = mockBooks.length > 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const navigate = useNavigate(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [activeTab, setActiveTab] = useState(tabs[0]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [savedBooks, setSavedBooks] = useState<{ [isbn: string]: boolean }>({}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 피드 관련 상태 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [savedFeeds, setSavedFeeds] = useState<SavedFeedInMy[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [feedNextCursor, setFeedNextCursor] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [feedIsLast, setFeedIsLast] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [feedLoading, setFeedLoading] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 책 관련 상태 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [savedBooks, setSavedBooks] = useState<SavedBookInMy[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 초기 로딩 상태 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [initialLoading, setInitialLoading] = useState(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Intersection Observer ref | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const feedObserverRef = useRef<HTMLDivElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleBack = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| navigate('/mypage'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 저장된 피드 로드 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const loadSavedFeeds = useCallback(async (cursor: string | null = null) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFeedLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await getSavedFeedsInMy(cursor); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cursor === null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 첫 로드 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSavedFeeds(response.data.feedList); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 추가 로드 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSavedFeeds(prev => [...prev, ...response.data.feedList]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFeedNextCursor(response.data.nextCursor); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFeedIsLast(response.data.isLast); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('저장된 피드 로드 실패:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFeedLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion loadSavedFeeds 중복 요청 방지 및 안정성 개선 IntersectionObserver가 동일 커서로 여러 번 트리거될 여지가 있습니다. 함수 초반에 ref 기반 가드와 락 설정/해제를 추가하면 안정적입니다. 적용 제안: const loadSavedFeeds = useCallback(async (cursor: string | null = null) => {
try {
- setFeedLoading(true);
+ if (feedLoadingRef.current) return;
+ setFeedLoading(true);
+ feedLoadingRef.current = true;
const response = await getSavedFeedsInMy(cursor);
if (cursor === null) {
// 첫 로드
setSavedFeeds(response.data.feedList);
} else {
// 추가 로드
setSavedFeeds(prev => [...prev, ...response.data.feedList]);
}
setFeedNextCursor(response.data.nextCursor);
setFeedIsLast(response.data.isLast);
} catch (error) {
console.error('저장된 피드 로드 실패:', error);
} finally {
- setFeedLoading(false);
+ setFeedLoading(false);
+ feedLoadingRef.current = false;
}
}, []);
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 페이지 진입 시 모든 데이터 로드 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const loadAllData = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setInitialLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 두 API를 병렬로 호출 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [feedsResponse, booksResponse] = await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getSavedFeedsInMy(null), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getSavedBooksInMy(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 피드 데이터 설정 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSavedFeeds(feedsResponse.data.feedList); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFeedNextCursor(feedsResponse.data.nextCursor); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFeedIsLast(feedsResponse.data.isLast); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 책 데이터 설정 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSavedBooks(booksResponse.data.bookList); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+71
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 초기 데이터 병렬 로드에서도 반환 타입 오해로 인한 접근 버그 getSavedFeedsInMy/getSavedBooksInMy 모두 data 자체를 반환합니다. - const [feedsResponse, booksResponse] = await Promise.all([
+ const [feedsData, booksData] = await Promise.all([
getSavedFeedsInMy(null),
getSavedBooksInMy(),
]);
// 피드 데이터 설정
- setSavedFeeds(feedsResponse.data.feedList);
- setFeedNextCursor(feedsResponse.data.nextCursor);
- setFeedIsLast(feedsResponse.data.isLast);
+ setSavedFeeds(feedsData.feedList);
+ setFeedNextCursor(feedsData.nextCursor);
+ setFeedIsLast(feedsData.isLast);
// 책 데이터 설정
- setSavedBooks(booksResponse.data.bookList);
+ setSavedBooks(booksData.bookList);추가 제안: 한쪽 API 실패 시 다른 데이터는 보여주고 싶다면 Promise.all 대신 allSettled로 부분 성공을 허용하는 패턴을 고려하세요. 필요하시면 패치 제안 드리겠습니다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('초기 데이터 로드 실패:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setInitialLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadAllData(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); // 한 번만 실행 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Intersection Observer 설정 (피드) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const observer = new IntersectionObserver( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entries => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entries.forEach(entry => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entry.isIntersecting && !feedIsLast && !feedLoading && feedNextCursor) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadSavedFeeds(feedNextCursor); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { threshold: 0.1 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (feedObserverRef.current) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| observer.observe(feedObserverRef.current); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => observer.disconnect(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [feedIsLast, feedLoading, feedNextCursor, loadSavedFeeds]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.scrollTo(0, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [activeTab]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleSaveToggle = (isbn: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSavedBooks(prev => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...prev, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [isbn]: !prev[isbn], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleSaveToggle = async (isbn: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const currentBook = savedBooks.find(book => book.isbn === isbn); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!currentBook) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newSaveState = !currentBook.isSaved; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await postSaveBook(isbn, newSaveState); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 로컬 상태 업데이트 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSavedBooks(prev => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev.map(book => (book.isbn === isbn ? { ...book, isSaved: newSaveState } : book)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('저장 토글:', isbn, newSaveState); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('저장 토글 실패:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -44,13 +141,27 @@ const SavePage = () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| title="저장" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <TabBar tabs={tabs} activeTab={activeTab} onTabClick={setActiveTab} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {activeTab === '피드' ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {initialLoading ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <LoadingSpinner fullHeight={true} size="large" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : activeTab === '피드' ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {hasPosts ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {savedFeeds.length > 0 ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <FeedContainer> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {mockPosts.map(post => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <FeedPost key={post.feedId} showHeader={true} isMyFeed={false} {...post} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {savedFeeds.map((feed, index) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <FeedPost | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key={feed.feedId} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| showHeader={true} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isMyFeed={false} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isLast={index === savedFeeds.length - 1} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {...feed} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* 무한스크롤을 위한 observer 요소 */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {!feedIsLast && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div ref={feedObserverRef} style={{ height: '20px' }}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {feedLoading && <LoadingSpinner fullHeight={false} size="small" />} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </FeedContainer> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <EmptyState> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -59,24 +170,24 @@ const SavePage = () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| </EmptyState> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : hasBooks ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : savedBooks.length > 0 ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <BookList> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {mockBooks.map(book => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <BookItem key={book.isbn}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {savedBooks.map(book => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <BookItem key={book.bookId}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <LeftSection> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Cover src={book.coverUrl} alt={`${book.title} 커버`} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Cover src={book.bookImageUrl} alt={`${book.bookTitle} 커버`} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <BookInfo> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Title>{book.title}</Title> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Title>{book.bookTitle}</Title> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Subtitle> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {book.author} 저 · {book.publisher} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {book.authorName} 저 · {book.publisher} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Subtitle> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </BookInfo> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </LeftSection> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SaveIcon onClick={() => handleSaveToggle(book.isbn)}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src={savedBooks[book.isbn] ? activeSave : save} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alt={savedBooks[book.isbn] ? '저장됨' : '저장'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src={book.isSaved ? activeSave : save} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alt={book.isSaved ? '저장됨' : '저장'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </SaveIcon> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </BookItem> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Props 타입을 좁혀 불필요한 prop 전달을 방지하세요
공개 API에서
isMyFeed를 제거했지만, 현재 시그니처가 여전히FeedListProps를 사용하고 있어 호출부가isMyFeed를 넘겨도 TS에서 잡히지 않고 컴포넌트 내부에서 무시됩니다. 혼란 방지를 위해 타입을 좁히는 것을 권장합니다.호출부에 남아있는
isMyFeed전달을 점검하려면 아래 스크립트를 실행해 주세요.🏁 Script executed:
Length of output: 80
아래 스크립트로
isMyFeedprop 전달 여부를 다시 확인해 주세요.🏁 Script executed:
Length of output: 330
Props 타입 좁히기 및 호출부
isMyFeed제거MyFeed컴포넌트에서 더 이상 사용하지 않는isMyFeedprop을 타입에서 제외하고, 호출부에서도 관련 전달을 제거하세요.문제 요약
isMyFeed를 삭제했음에도 여전히FeedListProps에 포함되어 있어, 호출부에서 넘겨도 TS가 잡아내지 않고 무시됩니다.수정할 위치
제안하는 변경사항
MyFeed 시그니처 수정 (타입에서
isMyFeed제외)호출부에서
isMyFeedprop 제거📝 Committable suggestion
🤖 Prompt for AI Agents