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
2 changes: 1 addition & 1 deletion public/genres.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
{
"id": "science",
"title": "과학",
"title": "과학·IT",
"subTitle": "과학자",
"iconUrl": "/assets/genre/science.svg",
"color": "#C8A5FF"
Expand Down
12 changes: 9 additions & 3 deletions src/api/comments/getComments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ export interface CommentData {
content: string;
likeCount: number;
isLike: boolean;
isDeleted: boolean;
replyList: ReplyData[];
}

export interface ReplyData {
replyId: number;
parentCommentCreatorNickname: string;
commentId: number;
creatorId: number;
creatorProfileImageUrl: string | null;
creatorNickname: string;
Expand All @@ -25,6 +27,7 @@ export interface ReplyData {
content: string;
likeCount: number;
isLike: boolean;
isDeleted: boolean;
}

export interface GetCommentsResponse {
Expand All @@ -41,9 +44,10 @@ export interface GetCommentsResponse {
export interface GetCommentsParams {
size?: number;
cursor?: string | null;
postType: 'FEED' | 'RECORD' | 'VOTE';
}

export const getComments = async (postId: number, params?: GetCommentsParams) => {
export const getComments = async (postId: number, params: GetCommentsParams) => {
const searchParams = new URLSearchParams();

if (params?.size) {
Expand All @@ -54,8 +58,10 @@ export const getComments = async (postId: number, params?: GetCommentsParams) =>
searchParams.append('cursor', params.cursor);
}

searchParams.append('postType', params.postType);

const queryString = searchParams.toString();
const url = queryString ? `/comments/${postId}?${queryString}` : `/comments/${postId}`;
const url = `/comments/${postId}?${queryString}`;

const response = await apiClient.get<GetCommentsResponse>(url);
return response.data;
Expand Down
13 changes: 13 additions & 0 deletions src/api/feeds/deleteFeedPost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { apiClient } from '../index';

export interface DeleteFeedPostResponse {
isSuccess: boolean;
code: number;
message: string;
data: null;
}

export const deleteFeedPost = async (feedId: number) => {
const response = await apiClient.delete<DeleteFeedPostResponse>(`/feeds/${feedId}`);
return response.data;
};
22 changes: 22 additions & 0 deletions src/api/feeds/postFeedLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { apiClient } from '../index';

export interface PostFeedLikeRequest {
type: boolean;
}

export interface PostFeedLikeResponse {
isSuccess: boolean;
code: number;
message: string;
data: {
feedId: number;
isLiked: boolean;
};
}

export const postFeedLike = async (feedId: number, type: boolean) => {
const response = await apiClient.post<PostFeedLikeResponse>(`/feeds/${feedId}/likes`, {
type,
});
return response.data;
};
Comment on lines +1 to +22
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

API 래퍼 일관성 점검 권장

프로젝트 내 다른 API 래퍼들이 response.data 전체를 반환하는지, 혹은 data.data만 반환하는지 일관성을 확인해 주세요. 혼재 시 사용처에서 접근 경로가 달라져 버그 요인이 됩니다.

다음 스크립트로 API 래퍼들의 반환 패턴을 빠르게 점검할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# API 래퍼 반환 일관성 점검
echo "==== return response.data 사용 위치 ===="
rg -n "return\s+response\.data;"

echo
echo "==== return response\.data\?\.\w+ 또는 중첩 data 반환 패턴 ===="
rg -n "return\s+response\.data\.\w+;"

echo
echo "==== isSuccess / data 접근 패턴 사용처 ===="
rg -n "isSuccess|response\.data\?" src | head -n 200

Length of output: 3050


API 래퍼 반환 형태 통일 필요

현재 대부분의 API 래퍼는 response.data 전체를 반환하고 있으나, 오직 getMyProfile만 중첩된 response.data.data를 반환하고 있어 사용처에서 접근 경로가 달라질 수 있습니다. 아래 파일을 점검해 반환 형태를 일관되게 맞춰 주세요.

• 파일: src/api/users/getMyProfile.ts
위치: 16줄
변경 전:

return response.data.data;

변경 후 (다른 래퍼와 동일하게):

return response.data;

필요에 따라 모든 래퍼가 data.data만 반환하도록 통일하는 방안도 고려 가능합니다. 일관된 반환 구조를 선택해 전체 코드베이스에서 동일하게 적용해 주세요.

🤖 Prompt for AI Agents
In src/api/feeds/postFeedLike.ts (lines 1-22) and specifically fix the
inconsistent wrapper in src/api/users/getMyProfile.ts (around line 16):
postFeedLike currently returns response.data (which matches the majority of
wrappers) but getMyProfile returns response.data.data, so change getMyProfile to
return response.data instead of response.data.data and adjust its return type if
necessary so all API wrappers consistently return response.data across the
codebase; update any callsites/types that assumed the nested shape.

18 changes: 18 additions & 0 deletions src/api/users/getMyProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { apiClient } from '../index';

export interface GetMyProfileResponse {
code: number;
status: string;
message: string;
data: {
profileImageUrl: string;
nickname: string;
aliasName: string;
aliasColor: string;
};
}

export const getMyProfile = async () => {
const response = await apiClient.get<GetMyProfileResponse>('/users/my-page');
return response.data.data;
};
50 changes: 50 additions & 0 deletions src/api/users/patchProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { apiClient } from '../index';

export interface PatchProfileRequest {
nickname: string | null;
aliasName: string;
}

export interface PatchProfileResponse {
isSuccess?: boolean;
code: number;
message: string;
}

export const patchProfile = async (data: PatchProfileRequest): Promise<PatchProfileResponse> => {
try {
const response = await apiClient.patch<PatchProfileResponse>('/users', data);
return response.data;
} catch (error) {
let errorMessage = '프로필 편집 중 오류가 발생했어요.';
let errorCode = 0;

if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as { response?: { data?: { code?: number; message?: string } } };
if (axiosError.response?.data) {
const serverData = axiosError.response.data;
errorCode = serverData.code || 0;

switch (errorCode) {
case 70004:
errorMessage = '현재 닉네임과 같은 닉네임이에요.';
break;
case 70005:
errorMessage = '닉네임은 6개월에 한번 변경할 수 있어요.';
break;
case 70006:
errorMessage = '이미 사용중인 닉네임이에요.';
break;
default:
errorMessage = serverData.message || '프로필 편집에 실패했어요.';
}
}
}

return {
isSuccess: false,
code: errorCode,
message: errorMessage,
};
}
};
16 changes: 11 additions & 5 deletions src/components/common/Modal/MoreMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ const MoreMenu = ({ onEdit, onDelete, onClose }: MoreMenuProps) => {

const Overlay = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100vh;
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
min-width: 320px;
max-width: 767px;
margin: 0 auto;
background-color: rgba(18, 18, 18, 0.1);
backdrop-filter: blur(2.5px);
z-index: 1200;
`;

const Container = styled.div`
Expand All @@ -43,7 +49,7 @@ const Container = styled.div`
padding: 20px;
border-radius: 12px 12px 0px 0px;
background-color: ${colors.darkgrey.main};
z-index: 1001;
z-index: 1201;
`;

const Button = styled.div<{ variant: 'edit' | 'delete' }>`
Expand Down
6 changes: 1 addition & 5 deletions src/components/common/Modal/PopupContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@ const PopupContainer = () => {
);
}
case 'moremenu': {
return (
<Wrapper>
<MoreMenu {...(popupProps as MoreMenuProps)} />
</Wrapper>
);
return <MoreMenu {...(popupProps as MoreMenuProps)} />;
}
case 'reply-modal': {
return <ReplyModal {...(popupProps as ReplyModalProps)} />;
Expand Down
6 changes: 3 additions & 3 deletions src/components/common/Modal/ReplyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { deleteComment } from '@/api/comments/deleteComment';
import { usePopupActions } from '@/hooks/usePopupActions';
import type { ReplyModalProps } from '@/stores/usePopupStore';

const ReplyModal = ({ isOpen, userId, replyId, position, onClose }: ReplyModalProps) => {
const ReplyModal = ({ isOpen, userId, commentId, position, onClose }: ReplyModalProps) => {
const [currentUserId, setCurrentUserId] = useState<number | null>(null);
const { openConfirm, openSnackbar, closePopup } = usePopupActions();

Expand Down Expand Up @@ -33,8 +33,8 @@ const ReplyModal = ({ isOpen, userId, replyId, position, onClose }: ReplyModalPr
disc: '삭제 후에는 되돌릴 수 없어요.',
onConfirm: async () => {
try {
await deleteComment(replyId);
console.log('댓글 삭제 성공:', replyId);
await deleteComment(commentId);
console.log('댓글 삭제 성공:', commentId);

openSnackbar({
message: '댓글 삭제를 완료했어요.',
Expand Down
8 changes: 6 additions & 2 deletions src/components/common/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useNavigate, useLocation } from 'react-router-dom';
import styled from '@emotion/styled';
import Fab from './Fab';
import type { FabProps } from '../../types/fab';
import FeedIcon from '../../assets/navbar/feed.svg';
import GroupIcon from '../../assets/navbar/group.svg';
import SearchIcon from '../../assets/navbar/search.svg';
Expand Down Expand Up @@ -72,7 +71,12 @@ const items: RouteItem[] = [
{ path: '/mypage', label: '내 정보', icon: MyIcon, activeIcon: MyIconActive },
];

const NavBar = ({ src, path }: FabProps) => {
interface NavBarProps {
src?: string;
path?: string;
}

const NavBar = ({ src, path }: NavBarProps) => {
Comment on lines +74 to +79
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

Fab 렌더링 가드 및 Prop 타입 강화 제안

path만 있고 src가 없는 경우 Fab에 undefined가 전달될 수 있습니다. 최소한 렌더링 가드로 방지하고, 가능하면 타입을 더 엄격하게 해주세요.

렌더링 가드(권장, 간단):

-        {path && <Fab src={src} path={path} />}
+        {path && src && <Fab src={src} path={path} />}

타입을 엄격하게(선택):

-interface NavBarProps {
-  src?: string;
-  path?: string;
-}
+type NavBarProps = { src: string; path: string } | { src?: undefined; path?: undefined };

Also applies to: 97-97

🤖 Prompt for AI Agents
In src/components/common/NavBar.tsx around lines 74-79 (and also at line 97),
the NavBar currently accepts optional src and path which can pass undefined into
the Fab component; add a rendering guard so Fab is only rendered when src is
defined (e.g., src && <Fab ...>), and strengthen the prop types by making src
required when path is present or by using a discriminated union so the component
signature prevents path-without-src (e.g., define props as either { src: string;
path?: string } | { src?: undefined; path?: undefined }). Ensure you update
usages and adjust any conditional rendering at line 97 accordingly.

const navigate = useNavigate();
const { pathname } = useLocation();

Expand Down
19 changes: 16 additions & 3 deletions src/components/common/Post/PostFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import save from '../../../assets/feed/save.svg';
import activeSave from '../../../assets/feed/activeSave.svg';
import lockIcon from '../../../assets/feed/lockIcon.svg';
import { postSaveFeed } from '@/api/feeds/postSave';
import { postFeedLike } from '@/api/feeds/postFeedLike';

const Container = styled.div<{ isDetail: boolean }>`
width: 100%;
Expand Down Expand Up @@ -70,9 +71,21 @@ const PostFooter = ({
const [likeCount, setLikeCount] = useState<number>(initialLikeCount);
const [saved, setSaved] = useState(isSaved);

const handleLike = () => {
setLiked(!liked);
setLikeCount(prev => (liked ? prev - 1 : prev + 1));
const handleLike = async () => {
try {
const response = await postFeedLike(feedId, !liked);

if (response.isSuccess) {
// 성공 시 상태 업데이트
setLiked(response.data.isLiked);
setLikeCount(prev => (response.data.isLiked ? prev + 1 : prev - 1));
console.log('좋아요 상태 변경 성공:', response.data.isLiked);
} else {
console.error('좋아요 상태 변경 실패:', response.message);
}
} catch (error) {
console.error('좋아요 API 호출 실패:', error);
}
Comment on lines +74 to +88
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

⚠️ Potential issue

좋아요 토글 동시 클릭(중복 요청) 시 레이스/역순 응답으로 카운트가 틀어질 수 있습니다

  • 여러 번 빠르게 클릭 시 요청이 겹치고, 응답이 역순으로 도착하면 likeCount가 실제 서버 상태와 달라질 수 있습니다.
  • 또한 서버가 동일 상태를 반환해도 현재 로직은 isLiked가 true면 무조건 +1, false면 -1 하므로 “상태 변화가 없는 경우”에도 카운트가 변합니다.
  • 음수 카운트 방어도 필요합니다.

다음과 같이 in-flight 가드와 안전한 델타 계산을 적용해 주세요.

적용 diff(해당 범위 내 수정):

-  const handleLike = async () => {
-    try {
-      const response = await postFeedLike(feedId, !liked);
-
-      if (response.isSuccess) {
-        // 성공 시 상태 업데이트
-        setLiked(response.data.isLiked);
-        setLikeCount(prev => (response.data.isLiked ? prev + 1 : prev - 1));
-        console.log('좋아요 상태 변경 성공:', response.data.isLiked);
-      } else {
-        console.error('좋아요 상태 변경 실패:', response.message);
-      }
-    } catch (error) {
-      console.error('좋아요 API 호출 실패:', error);
-    }
-  };
+  const handleLike = async () => {
+    if (liking) return; // 중복 클릭 방지
+    setLiking(true);
+    const prevLiked = liked;
+    try {
+      const response = await postFeedLike(feedId, !prevLiked);
+
+      if (response.isSuccess) {
+        const nextLiked = response.data?.isLiked ?? !prevLiked;
+        // 상태 변화가 있는 경우에만 카운트 변경
+        setLiked(nextLiked);
+        setLikeCount(prev => {
+          if (nextLiked === prevLiked) return prev;
+          const next = nextLiked ? prev + 1 : prev - 1;
+          return Math.max(next, 0); // 음수 방어
+        });
+        console.log('좋아요 상태 변경 성공:', nextLiked);
+      } else {
+        console.error('좋아요 상태 변경 실패:', response.message);
+      }
+    } catch (error) {
+      console.error('좋아요 API 호출 실패:', error);
+    } finally {
+      setLiking(false);
+    }
+  };

범위 밖 추가(상단 state 정의에 추가):

// 추가: 중복 요청 방지용 상태
const [liking, setLiking] = useState(false);
🤖 Prompt for AI Agents
In src/components/common/Post/PostFooter.tsx around lines 74 to 88, add a guard
to prevent duplicate in-flight requests and compute a safe delta based on
previous state and the server-returned isLiked: define a top-level state const
[liking, setLiking] = useState(false); at the component state area, then in
handleLike return early if liking is true, setLiking(true) before the API call
and setLiking(false) in finally; after a successful response only update liked
to response.data.isLiked and update likeCount using the previous value and
comparing prevLiked (use a functional setState) to response.data.isLiked so you
only apply +1 when it changed from false→true or -1 for true→false, and ensure
the new likeCount is clamped to a minimum of 0; also avoid changing count if
server reports no state change and log errors as currently done.

};

const handleSave = async () => {
Expand Down
Loading