Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
54adcd3
feat: 투표하기 API 연동 및 실시간 투표 기능 구현
ljh130334 Aug 18, 2025
a265d79
fix: 투표 API 400 에러 수정 및 디자인 변경 취소
ljh130334 Aug 18, 2025
f39b5cb
Merge pull request #156 from THIP-TextHip/main
heeeeyong Aug 18, 2025
0d79f81
fix: 피드 게시글에서 수정하기로 이동 navigation 추가
heeeeyong Aug 18, 2025
5831632
Merge pull request #157 from THIP-TextHip/feat/api-rooms-poll
ljh130334 Aug 18, 2025
b83b9a9
Merge pull request #158 from THIP-TextHip/chore/minor-updates
heeeeyong Aug 18, 2025
e463191
fix: 마이페이지 버튼 이동경로 수정
heeeeyong Aug 18, 2025
9c0890a
fix: 내 피드 Footer 수정
heeeeyong Aug 18, 2025
9eb9b74
fix: 마이페이지 버젼 버튼 추가
heeeeyong Aug 18, 2025
6f91636
fix: 마이페이지 수정
heeeeyong Aug 18, 2025
647255f
feat: GroupSearch 무한 스크롤 로직 추가, 검색 로직 변경
ho0010 Aug 18, 2025
af21ab6
feat: GroupSearch handleBackButton 로직 추가
ho0010 Aug 18, 2025
f9b7e4d
feat: GroupSearch 각 groupcard 클릭 이벤트
ho0010 Aug 18, 2025
5561111
fix: GroupDetail 하단 버튼 클릭시 상태 반영
ho0010 Aug 18, 2025
acdaab1
design: GroupCard 요구사항에 맞게 수정
ho0010 Aug 18, 2025
51d2f4c
feat: MyGroupModal GroupCard 클릭 이벤트 추가
ho0010 Aug 18, 2025
4a387ad
Merge pull request #159 from THIP-TextHip/feat/api-rooms
ho0010 Aug 18, 2025
eee53b6
feat: 기록을 피드에 핀하기 기능 구현
ljh130334 Aug 18, 2025
4178f81
feat: getFeedsByIsbn API
ho0010 Aug 18, 2025
b3f24bd
feat: SearchBook getFeedsByIsbn API 연동에 따른 변경
ho0010 Aug 18, 2025
81dd112
chore: 자잘한 오류 및 버그 수정
ljh130334 Aug 18, 2025
4e3b679
Merge pull request #160 from THIP-TextHip/feat/api-rooms-pin
ljh130334 Aug 18, 2025
3cbf785
feat: BookSearchBottomSheet 실제 검색 API 연동
ljh130334 Aug 18, 2025
5824164
feat: GroupDetail API PasswordModal 연동
ho0010 Aug 18, 2025
346e6d6
design: 소개 말줄임 적용
ho0010 Aug 18, 2025
0f074bf
fix: 모집 데이터 형식 수정
ho0010 Aug 18, 2025
3b20008
Merge pull request #161 from THIP-TextHip/feat/search-hotfix
ljh130334 Aug 18, 2025
77c3170
design: 비공개 방 locked 이미지 추가
ho0010 Aug 18, 2025
8ba3c3d
design: min-height 추가
ho0010 Aug 18, 2025
a787da9
fix: import 경로
ho0010 Aug 18, 2025
f741c33
Merge pull request #162 from THIP-TextHip/feat/api-rooms
ho0010 Aug 18, 2025
ce8b05e
Merge branch 'develop' of https://github.com/THIP-TextHip/THIP-Web in…
heeeeyong Aug 18, 2025
c2451e7
feat: 마이페이지 > 저장 API 연동 완료
heeeeyong Aug 18, 2025
4789e6c
fix: 탭별 지연 로딩 제거를 위한 API 병렬 호출
heeeeyong Aug 18, 2025
62c2187
Merge pull request #163 from THIP-TextHip/chore/minor-updates
heeeeyong Aug 18, 2025
b4ec363
fix: 최근검색어 삭제 API 수정
heeeeyong Aug 18, 2025
df13a48
fix: 책 검색 무한스크롤 로직 수정
heeeeyong Aug 18, 2025
f281813
fix: 최근검색어 API userId 삭제
heeeeyong Aug 18, 2025
a2aa143
fix:중복 key 에러 해결
heeeeyong Aug 18, 2025
c76e5aa
feat: 댓글 모달 UI 및 기능 구현
ljh130334 Aug 19, 2025
8ab41c7
fix: TypeScript 타입 오류 수정
ljh130334 Aug 19, 2025
edddf87
fix: modal -> bottomsheet
ljh130334 Aug 19, 2025
d23673a
feat: 전역 댓글 바텀시트 시스템 구현
ljh130334 Aug 19, 2025
e583630
refactor: 전역이 아닌 페이지별 관리로 수정
ljh130334 Aug 19, 2025
1321bc2
fix: 댓글 바텀시트 입력 지연 문제 해결
ljh130334 Aug 19, 2025
c51a04d
style: 일부 스타일링 수정
ljh130334 Aug 19, 2025
0e73085
Merge pull request #165 from THIP-TextHip/feat/comment-hotfix
ljh130334 Aug 19, 2025
569952c
fix: 책 검색 모달 탭 유지 및 무한스크롤 기능 추가
ljh130334 Aug 19, 2025
4e6b360
Merge pull request #166 from THIP-TextHip/feat/search-hotfix
ljh130334 Aug 19, 2025
327fbb8
Merge pull request #164 from THIP-TextHip/chore/minor-updates
heeeeyong Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?

# Claude Code documentation
CLAUDE.md
43 changes: 43 additions & 0 deletions src/api/books/getSavedBooksInMy.ts
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); // 첫 번째 책 제목
*/
4 changes: 2 additions & 2 deletions src/api/books/getSearchBooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ export const getSearchBooks = async (
}
};

export const convertToSearchedBooks = (apiBooks: BookSearchItem[]): SearchedBook[] => {
export const convertToSearchedBooks = (apiBooks: BookSearchItem[], startIndex: number = 0): SearchedBook[] => {
return apiBooks.map((book, index) => ({
id: index + 1,
id: startIndex + index + 1,
title: book.title,
author: book.authorName,
publisher: book.publisher,
Expand Down
48 changes: 48 additions & 0 deletions src/api/feeds/getFeedsByIsbn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { apiClient } from '@/api/index';

export type FeedSort = 'like' | 'latest';

export interface FeedItem {
feedId: number;
creatorId: number;
isWriter: boolean;
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;
}

export interface GetFeedsByIsbnResponse {
isSuccess: boolean;
code: number;
message: string;
data: {
feeds: FeedItem[];
nextCursor: string | null;
isLast: boolean;
};
}

export const getFeedsByIsbn = async (
isbn: string,
sort: FeedSort = 'like',
cursor?: string | null,
): Promise<GetFeedsByIsbnResponse> => {
const params = new URLSearchParams();
params.set('sort', sort);
if (cursor != null) params.set('cursor', cursor);

const url = `/feeds/related-books/${encodeURIComponent(isbn)}?` + params.toString();
const res = await apiClient.get<GetFeedsByIsbnResponse>(url);
return res.data;
};
68 changes: 68 additions & 0 deletions src/api/feeds/getSavedFeedsInMy.ts
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);
}
*/
3 changes: 1 addition & 2 deletions src/api/recentsearch/deleteRecentSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ export interface DeleteRecentSearchResponse {

export const deleteRecentSearch = async (
recentSearchId: number,
userId: number,
): Promise<DeleteRecentSearchResponse> => {
try {
const response = await apiClient.delete<DeleteRecentSearchResponse>(
`/recent-searches/${recentSearchId}?userId=${userId}`,
`/recent-searches/${recentSearchId}`,
);
return response.data;
} catch (error) {
Expand Down
6 changes: 6 additions & 0 deletions src/api/record/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './createRecord';
export * from './createVote';
export * from './deleteRecord';
export * from './deleteVote';
export * from './postVote';
export * from './pinRecordToFeed';
41 changes: 41 additions & 0 deletions src/api/record/pinRecordToFeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { apiClient } from '../index';
import type { ApiResponse } from '@/types/record';

// 피드 핀 응답 데이터 타입
export interface PinRecordData {
bookTitle: string;
authorName: string;
bookImageUrl: string;
isbn: string;
}

// API 응답 타입
export type PinRecordResponse = ApiResponse<PinRecordData>;

// 기록을 피드에 핀하기 API 함수
export const pinRecordToFeed = async (roomId: number, recordId: number) => {
const response = await apiClient.get<PinRecordResponse>(
`/rooms/${roomId}/records/${recordId}/pin`
);
return response.data;
};

/*
사용 예시:
try {
const result = await pinRecordToFeed(1, 123);
if (result.isSuccess) {
console.log("책 제목:", result.data.bookTitle);
console.log("저자명:", result.data.authorName);
console.log("책 이미지:", result.data.bookImageUrl);
console.log("ISBN:", result.data.isbn);
// 성공 처리 로직 - 피드 작성 화면으로 이동
} else {
console.error("핀하기 실패:", result.message);
// 실패 처리 로직
}
} catch (error) {
console.error("API 호출 오류:", error);
// 에러 처리 로직
}
*/
38 changes: 38 additions & 0 deletions src/api/record/postVote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { apiClient } from '../index';
import type { VoteRequest, VoteData, ApiResponse } from '@/types/record';

// API 응답 타입
export type VoteResponse = ApiResponse<VoteData>;

// 투표하기 API 함수
export const postVote = async (roomId: number, voteId: number, voteData: VoteRequest) => {
const response = await apiClient.post<VoteResponse>(`/rooms/${roomId}/vote/${voteId}`, voteData);
return response.data;
};

/*
사용 예시:
const voteData: VoteRequest = {
voteItemId: 1,
type: true // true: 투표하기, false: 투표취소
};

try {
const result = await postVote(1, 1, voteData);
if (result.isSuccess) {
console.log("투표 성공:", result.data);
console.log("포스트 ID:", result.data.postId);
console.log("방 ID:", result.data.roomId);
console.log("투표 결과:", result.data.voteItems);
// 성공 처리 로직
} else {
console.error("투표 실패:", result.message);
// 실패 처리 로직 (에러 코드별 분기 처리 가능)
// 120001: 이미 투표한 투표항목입니다.
// 120002: 투표하지 않은 투표항목은 취소할 수 없습니다.
}
} catch (error) {
console.error("API 호출 오류:", error);
// 에러 처리 로직
}
*/
12 changes: 12 additions & 0 deletions src/assets/books/lockedBook.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/feed/pin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 54 additions & 3 deletions src/components/common/BookSearchBottomSheet/BookList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useEffect, useRef, useCallback } from 'react';
import {
BookList as StyledBookList,
BookItem,
BookCover,
BookInfo,
BookTitle,
LoadingContainer,
LoadingText,
} from './BookSearchBottomSheet.styled';

export interface Book {
Expand All @@ -17,17 +20,58 @@ export interface Book {
interface BookListProps {
books: Book[];
onBookSelect: (book: Book) => void;
onLoadMore?: () => Promise<void>;
hasNextPage?: boolean;
isLoadingMore?: boolean;
isSearchMode?: boolean;
}

const BookList = ({ books, onBookSelect }: BookListProps) => {
const BookList = ({
books,
onBookSelect,
onLoadMore,
hasNextPage = false,
isLoadingMore = false,
isSearchMode = false
}: BookListProps) => {
const observerRef = useRef<IntersectionObserver | null>(null);
const loadingRef = useRef<HTMLDivElement | null>(null);

const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>) => {
e.currentTarget.style.display = 'none';
};

// 무한스크롤을 위한 Intersection Observer 설정
const lastBookElementRef = useCallback((node: HTMLDivElement | null) => {
if (isLoadingMore) return;

if (observerRef.current) observerRef.current.disconnect();

observerRef.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasNextPage && onLoadMore && isSearchMode) {
onLoadMore();
}
});

if (node) observerRef.current.observe(node);
}, [isLoadingMore, hasNextPage, onLoadMore, isSearchMode]);

useEffect(() => {
return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, []);

return (
<StyledBookList>
{books.map(book => (
<BookItem key={book.id} onClick={() => onBookSelect(book)}>
{books.map((book, index) => (
<BookItem
key={`${book.id}-${book.isbn}`}
onClick={() => onBookSelect(book)}
ref={index === books.length - 1 ? lastBookElementRef : null}
>
<BookCover>
<img src={book.cover} alt={book.title} onError={handleImageError} />
</BookCover>
Expand All @@ -36,6 +80,13 @@ const BookList = ({ books, onBookSelect }: BookListProps) => {
</BookInfo>
</BookItem>
))}

{/* 검색 모드에서 더 많은 데이터가 있고 로딩 중일 때 로딩 표시 */}
{isSearchMode && isLoadingMore && (
<LoadingContainer ref={loadingRef}>
<LoadingText>더 많은 책을 불러오는 중...</LoadingText>
</LoadingContainer>
)}
</StyledBookList>
);
};
Expand Down
Loading