Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
.env.local

# Editor directories and files
.vscode/*
Expand Down
52 changes: 52 additions & 0 deletions src/api/feeds/getMyFeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { apiClient } from '../index';
import type { PostData } from '@/types/post';

// API 응답 데이터 타입
export interface MyFeedData {
feedList: PostData[];
nextCursor: string;
isLast: boolean;
}

// API 응답 타입
export interface MyFeedResponse {
success: boolean;
code: number;
message: string;
data: MyFeedData;
}

// 요청 파라미터 타입
export interface GetMyFeedParams {
cursor?: string; // 첫 페이지는 null 또는 없음, 다음 페이지부터는 nextCursor 값 사용
}

// 내 피드 조회 API 함수
export const getMyFeeds = async (params?: GetMyFeedParams): Promise<MyFeedResponse> => {
const queryParams = new URLSearchParams();

// cursor가 있을 때만 쿼리 파라미터에 추가
if (params?.cursor) {
queryParams.append('cursor', params.cursor);
}

const url = `/feeds/mine${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;

const response = await apiClient.get<MyFeedResponse>(url);
return response.data;
};
Comment on lines +25 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

코드 중복 가능성을 확인해보세요.

getTotalFeed.ts와 거의 동일한 구조를 가지고 있습니다. 공통 유틸리티 함수나 제네릭 함수로 중복을 제거할 수 있을지 검토해보세요.

다음 스크립트로 두 API 함수 간의 유사성을 확인할 수 있습니다:


🏁 Script executed:

#!/bin/bash
# 두 feed API 파일의 구조적 유사성 확인
echo "=== getMyFeed.ts ==="
cat src/api/feeds/getMyFeed.ts | head -40

echo -e "\n=== getTotalFeed.ts ==="
cat src/api/feeds/getTotalFeed.ts | head -40

echo -e "\n=== 차이점 비교 ==="
diff src/api/feeds/getMyFeed.ts src/api/feeds/getTotalFeed.ts

Length of output: 3173


중복 피드 API 함수 리팩토링 제안

getMyFeeds(/feeds/mine)와 getTotalFeeds(/feeds)는 URLSearchParams 처리, 쿼리 문자열 생성, apiClient.get 호출 등 거의 동일한 로직을 갖고 있습니다. 아래 파일들을 점검하여 공통 유틸 함수 또는 제네릭 헬퍼로 추출해 중복을 제거해 보세요.

수정 대상:

  • src/api/feeds/getMyFeed.ts
  • src/api/feeds/getTotalFeed.ts

리팩토링 예시:

// src/api/feeds/fetchFeeds.ts
import { apiClient } from '../index';

export async function fetchFeeds<Params extends { cursor?: string }, Response>(
  endpoint: string,
  params?: Params,
): Promise<Response> {
  const qp = new URLSearchParams();
  if (params?.cursor) qp.append('cursor', params.cursor);
  const url = `${endpoint}${qp.toString() ? `?${qp}` : ''}`;
  const res = await apiClient.get<Response>(url);
  return res.data;
}

// 기존 함수들은 이렇게 변경
export const getMyFeeds = (params?: GetMyFeedParams) =>
  fetchFeeds<GetMyFeedParams, MyFeedResponse>('/feeds/mine', params);

export const getTotalFeeds = (params?: GetTotalFeedParams) =>
  fetchFeeds<GetTotalFeedParams, TotalFeedResponse>('/feeds', params);
🤖 Prompt for AI Agents
In src/api/feeds/getMyFeed.ts lines 25 to 37, refactor the getMyFeeds function
to remove duplication with getTotalFeeds by extracting the shared logic of
building query parameters, constructing the URL, and calling apiClient.get into
a generic utility function (e.g., fetchFeeds) in a new file like
src/api/feeds/fetchFeeds.ts. Then update getMyFeeds and getTotalFeeds to call
this utility with their specific endpoints and types, thereby centralizing the
common code and reducing redundancy.


/*
// 첫 페이지 조회
const firstPage = await getMyFeeds();

// 다음 페이지 조회 (nextCursor 사용)
const nextPage = await getMyFeeds({
cursor: firstPage.data.nextCursor
});

// 마지막 페이지인지 확인
if (firstPage.data.isLast) {
console.log('더 이상 불러올 데이터가 없습니다.');
}
*/
53 changes: 53 additions & 0 deletions src/api/feeds/getTotalFeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { apiClient } from '../index';
import type { PostData } from '@/types/post';

// API 응답 데이터 타입
export interface TotalFeedData {
feedList: PostData[];
nextCursor: string;
isLast: boolean;
}

// API 응답 타입
export interface TotalFeedResponse {
success: boolean;
code: number;
message: string;
data: TotalFeedData;
}

// 요청 파라미터 타입
export interface GetTotalFeedParams {
cursor?: string; // 첫 페이지는 null 또는 없음, 다음 페이지부터는 nextCursor 값 사용
}

export const getTotalFeeds = async (params?: GetTotalFeedParams): Promise<TotalFeedResponse> => {
const queryParams = new URLSearchParams();

// cursor가 있을 때만 쿼리 파라미터에 추가
if (params?.cursor) {
queryParams.append('cursor', params.cursor);
}

const url = `/feeds${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;

const response = await apiClient.get<TotalFeedResponse>(url);
return response.data;
};
Comment on lines +24 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

코드 중복을 개선할 수 있습니다.

getMyFeed.ts와 구조가 거의 동일합니다. 다음과 같은 제네릭 함수로 리팩터링을 고려해보세요:

// api/feeds/common.ts
interface FeedApiParams {
  cursor?: string;
}

interface FeedResponse<T> {
  success: boolean;
  code: number;
  message: string;
  data: T;
}

const createFeedApi = <T>(endpoint: string) => {
  return async (params?: FeedApiParams): Promise<FeedResponse<T>> => {
    const queryParams = new URLSearchParams();
    if (params?.cursor) {
      queryParams.append('cursor', params.cursor);
    }
    const url = `${endpoint}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
    const response = await apiClient.get<FeedResponse<T>>(url);
    return response.data;
  };
};
🤖 Prompt for AI Agents
In src/api/feeds/getTotalFeed.ts around lines 24 to 36, the getTotalFeeds
function duplicates code similar to getMyFeed.ts. Refactor by creating a generic
function in a new file like api/feeds/common.ts that accepts an endpoint string
and returns an async function handling the cursor parameter and API call. Then
replace getTotalFeeds with a call to this generic function, passing the
appropriate endpoint, to eliminate duplication and improve maintainability.


/*
사용 방법:

// 첫 페이지 조회
const firstPage = await getTotalFeeds();

// 다음 페이지 조회 (nextCursor 사용)
const nextPage = await getTotalFeeds({
cursor: firstPage.data.nextCursor
});

// 마지막 페이지인지 확인
if (firstPage.data.isLast) {
console.log('더 이상 불러올 데이터가 없습니다.');
}
*/
87 changes: 87 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import axios, { type AxiosResponse, type AxiosError } from 'axios';

// 하드코딩된 액세스 토큰
const ACCESS_TOKEN =
'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDIwMTY4OCwiZXhwIjoxNzU2NzkzNjg4fQ.oOyJ7JI_t2-Xq1-gfAv4ZaYNrbyplvqdxhCk76-Txe4';
Comment on lines +3 to +5
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

개발용 하드코딩된 토큰의 보안 위험성을 주의하세요.

하드코딩된 액세스 토큰이 소스코드에 포함되어 있습니다. 이는 보안상 위험할 수 있습니다.

프로덕션 배포 전에 다음과 같이 개선하는 것을 권장합니다:

-// 하드코딩된 액세스 토큰
-const ACCESS_TOKEN =
-  'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDIwMTY4OCwiZXhwIjoxNzU2NzkzNjg4fQ.oOyJ7JI_t2-Xq1-gfAv4ZaYNrbyplvqdxhCk76-Txe4';
+// 개발용 기본 토큰 (환경변수로 관리)
+const ACCESS_TOKEN = import.meta.env.VITE_DEV_ACCESS_TOKEN || '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 하드코딩된 액세스 토큰
const ACCESS_TOKEN =
'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDIwMTY4OCwiZXhwIjoxNzU2NzkzNjg4fQ.oOyJ7JI_t2-Xq1-gfAv4ZaYNrbyplvqdxhCk76-Txe4';
// 개발용 기본 토큰 (환경변수로 관리)
const ACCESS_TOKEN = import.meta.env.VITE_DEV_ACCESS_TOKEN || '';
🤖 Prompt for AI Agents
In src/api/index.ts around lines 3 to 5, the access token is hardcoded, posing a
security risk. Remove the hardcoded token and instead load the access token from
a secure environment variable or configuration file. Ensure the token is not
committed to source control and is injected securely during runtime or
deployment.


// 토큰 관리 유틸리티
export const TokenManager = {
setAccessToken: (token: string) => localStorage.setItem('accessToken', token),
getAccessToken: (): string | null => localStorage.getItem('accessToken'),
// setRefreshToken: (token: string) => localStorage.setItem('refreshToken', token),
// getRefreshToken: (): string | null => localStorage.getItem('refreshToken'),
clearTokens: () => {
localStorage.removeItem('accessToken');
// localStorage.removeItem('refreshToken');
},
hasValidToken: (): boolean => !!localStorage.getItem('accessToken'),
};

// API 기본 설정
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;

// 환경변수 확인용
console.log('API_BASE_URL:', API_BASE_URL);

// axios 인스턴스 생성
export const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});

// 요청 인터셉터
apiClient.interceptors.request.use(
config => {
// 로컬스토리지에서 토큰 먼저 확인
const token = TokenManager.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
} else {
// 토큰이 없으면 하드코딩된 토큰 사용 (개발용)
config.headers.Authorization = ACCESS_TOKEN;
}
return config;
},
error => Promise.reject(error),
);

// 응답 인터셉터 - 토큰 만료 처리 및 에러 처리
apiClient.interceptors.response.use(
(response: AxiosResponse) => response,
(error: AxiosError) => {
const { status } = error.response || {};

// 에러 로깅
console.error('API Error:', status, error.message);

// 토큰 만료 또는 인증 실패 시 로그인 페이지로 리다이렉트
if (status === 401) {
// alert('토큰이 만료되었거나 유효하지 않습니다. 로그인 페이지로 이동합니다.');

// 현재 페이지가 로그인 페이지가 아닌 경우에만 리다이렉트
if (window.location.pathname !== '/') {
// alert('로그인이 필요합니다. 로그인 페이지로 이동합니다.');
window.location.href = '/';
}
}

// 권한 없음 (403) 에러 처리
if (status === 403) {
console.warn('접근 권한이 없습니다.');
alert('접근 권한이 없습니다.');
}

// 서버 에러 (500번대) 처리
if (status && status >= 500) {
console.error('서버 오류가 발생했습니다.');
alert('서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.');
}

return Promise.reject(error);
},
);

export default apiClient;
10 changes: 5 additions & 5 deletions src/components/common/Post/PostBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ const PostBody = ({
bookTitle,
isbn,
bookAuthor,
postContent,
contentBody,
feedId,
images = [],
contentsUrl = [],
}: PostBodyProps) => {
const navigate = useNavigate();
const hasImage = images.length > 0;
const hasImage = contentsUrl.length > 0;

const handlePostClick = (feedId: number) => {
// if (!isClickable) return;
Expand All @@ -65,10 +65,10 @@ const PostBody = ({
<Container>
<BookInfoCard bookTitle={bookTitle} bookAuthor={bookAuthor} isbn={isbn} />
<PostContent hasImage={hasImage} onClick={() => handlePostClick(feedId)}>
<div className="content">{postContent}</div>
<div className="content">{contentBody}</div>
{hasImage && (
<div className="imgContainer">
{images.map((src, i) => (
{contentsUrl.map((src: string, i: number) => (
<img key={i} src={src} />
))}
</div>
Expand Down
14 changes: 9 additions & 5 deletions src/components/common/Post/PostFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,31 @@ const Container = styled.div<{ isDetail: boolean }>`
`;

interface PostFooterProps {
initialLikeCount: number;
likeCount: number;
commentCount: number;
feedId: number;
isMyFeed: boolean;
isSaved?: boolean;
isLiked?: boolean;
isPublic?: boolean;
isDetail?: boolean;
}

const PostFooter = ({
initialLikeCount,
likeCount: initialLikeCount,
commentCount,
feedId,
isMyFeed,
isPublic,
isSaved = false,
isLiked = false,
isPublic = true,
isDetail = false,
}: PostFooterProps) => {
const navigate = useNavigate();

const [liked, setLiked] = useState(false);
const [liked, setLiked] = useState(isLiked);
const [likeCount, setLikeCount] = useState<number>(initialLikeCount);
const [saved, setSaved] = useState(false);
const [saved, setSaved] = useState(isSaved);

const handleLike = () => {
setLiked(!liked);
Expand Down
37 changes: 20 additions & 17 deletions src/components/common/Post/PostHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,44 @@
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
interface PostHeaderProps {
profileImgUrl: string;
userName: string;
userTitle: string;
titleColor: string;
createdAt: string;
userId: number;
creatorProfileImageUrl?: string;
creatorNickname?: string;
alias?: string;
titleColor?: string;
postDate: string;
creatorId?: number;
type?: 'post' | 'reply';
}

const PostHeader = ({
profileImgUrl,
userName,
userTitle,
titleColor,
createdAt,
userId,
creatorProfileImageUrl,
creatorNickname,
alias,
titleColor = '#FFFFFF', // 기본값 설정
postDate,
creatorId,
type = 'post',
}: PostHeaderProps) => {
const navigate = useNavigate();

const handleClick = () => {
navigate(`/otherfeed/${userId}`);
if (creatorId) {
navigate(`/otherfeed/${creatorId}`);
}
};

return (
<Container type={type} onClick={handleClick}>
<div className="headerInfo">
<img src={profileImgUrl} alt="칭호 이미지" />
<img src={creatorProfileImageUrl} alt="칭호 이미지" />
<div className="infoBox">
<div className="username">{userName}</div>
<div className="username">{creatorNickname}</div>
<div className="usertitle" style={{ color: titleColor }}>
{userTitle}
{alias}
</div>
</div>
</div>
<div className="timestamp">{createdAt}</div>
<div className="timestamp">{postDate}</div>
</Container>
);
};
Expand Down
10 changes: 5 additions & 5 deletions src/components/common/Post/Reply.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ const Reply = ({
return (
<Container>
<PostHeader
profileImgUrl={imageUrl}
userName={nickName}
userTitle={userTitle}
creatorProfileImageUrl={imageUrl}
creatorNickname={nickName}
alias={userTitle}
titleColor={titleColor}
createdAt={postDate}
userId={userId}
postDate={postDate}
creatorId={userId}
type="reply"
/>
<ReplySection>
Expand Down
10 changes: 5 additions & 5 deletions src/components/common/Post/SubReply.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ const SubReply = ({
</ReplyIcon>
<Content>
<PostHeader
profileImgUrl={replyCommentimgUrl}
userName={replyCommentUserName}
userTitle={replyCommentUserTitle}
creatorProfileImageUrl={replyCommentimgUrl}
creatorNickname={replyCommentUserName}
alias={replyCommentUserTitle}
titleColor={titleColor}
createdAt={postDate}
userId={replyCommentUserId}
postDate={postDate}
creatorId={replyCommentUserId}
type="reply"
/>
<ReplySection>
Expand Down
2 changes: 1 addition & 1 deletion src/components/feed/BookInfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const BookContainer = styled.div`
interface BookInfoCardProps {
bookTitle: string;
bookAuthor: string;
isbn: number;
isbn: string;
}

const BookInfoCard = ({ bookTitle, bookAuthor, isbn }: BookInfoCardProps) => {
Expand Down
Loading