Style: UI 수정#11
Conversation
- 페이지 별 레이아웃 관리를 app.tsx -> globalLayout에서 담당하도록 변경 - 기존에 globalLayout에서 처리하던 header를 컴포넌트로 분리, 페이지에서 import 하여 사용하도록 변경 - header에서 bookmark, like 등 일부 기능을 처리해야 해서 컴포넌트 분리 후 페이지에서 import 하는 방식 선택 - bottomnavigation 아이콘 클릭 시 해당 페이지로 이동
- RecentSearchList 및 SearchHeader 컴포넌트를 _components 폴더로 이동 - RecentSearchList 삭제 아이콘 p-0 적용하여 정렬 스타일 개선 - SearchHeader 컴포넌트 스타일 수정
- 페이지 별 헤더 및 헤더 아이콘 적용 - header을 페이지에서 관리함에 따라 GlobalLayout에서 p 적용 시 header에도 패딩 적용됨 - GlobalLayout p-4 속성 삭제 후, 각 페이지 콘텐트 div에서 패딩 적용
- 버튼 컴포넌트의 스타일을 통일성 있게 수정 - 입력 필드 및 선택기 스타일 개선 - 관심 분야 선택 제한 로직 추가
- PostDetailPage에서 댓글 및 답글 좋아요 기능을 추가 - CommentItem 및 CommentSection 컴포넌트에서 답글 좋아요 기능 통합 - PostContent 및 CommentSection의 UI 개선 - ReplyItem 컴포넌트 새로 추가하여 답글 표시 및 좋아요 기능 구현
- 세미콜론 추가로 코드 일관성 유지 - 불필요한 공백 제거
- ListItem 컴포넌트에 클릭 시 해당 포스트의 커뮤니티 경로로 이동하는 기능 추가 - useNavigate 훅을 사용하여 포스트 ID에 기반한 경로 설정
- Reply 인터페이스를 추가하여 답글 구조 정의 - Comment 인터페이스에 isLiked 및 replyCount, replies 필드 추가
- 커뮤니티 게시글 상세 조회, 댓글 목록 조회, 댓글 추가, 좋아요 토글 기능 구현 - 게시글 및 댓글 데이터 구조 정의 및 초기 데이터 추가 - 답글 추가 및 좋아요 기능을 위한 API 핸들러 추가
- 게시글 및 댓글 데이터 가져오는 로직을 간소화하여 가독성 향상 - 댓글 좋아요 기능을 위한 toggleCommentLike 훅 추가 - 답글 좋아요 기능을 위한 toggleReplyLike 훅 추가 - 에러 메시지 문자열을 일관된 쌍따옴표로 변경
There was a problem hiding this comment.
Summary of Changes
Hello @Monixc, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
This pull request delivers a UI/UX overhaul, focusing on improving the application's overall look and feel, enhancing user interaction, and streamlining the global layout management. It introduces a more modular header system, activates the bottom navigation, and refines key user flows like community post viewing and onboarding, all while ensuring a consistent visual design.
Highlights
- Global Layout Refactor: The
GlobalLayoutcomponent no longer manages header visibility, allowing pages to import and configure their ownHeadercomponents. Bottom navigation visibility is now dynamically controlled withinGlobalLayoutbased on the current route. Global padding has also been removed, giving individual pages more control over their spacing. - Customizable Header Component: A new
Headercomponent acceptsleftIcon,rightIcon,onLeftClick, andonRightClickprops, enabling different pages to display specific icons and actions in their headers, such as back buttons, search, or notifications. - Functional Bottom Navigation: The bottom navigation bar is now fully functional, with icons linked to their respective routes (
/board,/explore,/chat,/my), and the active tab is correctly highlighted based on the current URL. - Community Post Detail Page Overhaul: The UI for the community post detail page has been revamped, including a new overall layout, changing the comment input field from a
textareato aninput, and visually distinguishing comments/replies with indentation. Functionality for liking comments and replies has also been added. - Onboarding & Search Page UI Improvements: The onboarding flow's UI has been updated to use the
brand-color system. The search page's input field and recent search list have received styling improvements. - MSW Mock API Expansion: The mock service worker (MSW) handlers have been extended to support the new community post detail features, including mock endpoints for fetching individual posts, their comments and replies, and handling like toggles for posts, comments, and replies.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments or fill out our survey to provide feedback.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Code Review
전반적으로 UI 개선 및 리팩토링이 잘 이루어졌습니다. 특히 GlobalLayout과 Header 컴포넌트의 리팩토링으로 코드 구조가 더 명확하고 재사용성이 높아졌습니다. 몇 가지 개선점을 제안드렸으며, 특히 usePost 훅의 쿼리 무효화 로직과 PostContent의 좋아요 버튼 처리 부분을 확인해주시면 좋겠습니다.
또한, 스타일 가이드에 따라 모든 코드에 대한 자동화 테스트가 필요합니다.[^1] 이번 PR에 테스트 코드 변경사항이 포함되지 않은 것으로 보이는데, 추가된 기능과 로직에 대한 테스트 코드를 보강해주시면 코드의 안정성을 더욱 높일 수 있을 것입니다.
| export function useToggleCommentLike() { | ||
| const queryClient = useQueryClient(); | ||
| return useMutation({ | ||
| mutationFn: toggleCommentLike, | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey: ["post"] }); | ||
| }, | ||
| }); | ||
| } No newline at end of file | ||
| } |
There was a problem hiding this comment.
onSuccess에서 ['post'] 키로 쿼리를 무효화하면, 현재 보고 있지 않은 다른 게시물들의 캐시까지 함께 무효화되어 불필요한 데이터 리페칭이 발생할 수 있습니다. postId를 훅의 인자로 받아 ['post', postId]와 같이 더 구체적인 키를 사용해 무효화하는 것이 성능상 유리합니다. 이 변경은 PostDetailPage.tsx에서 훅을 호출하는 부분의 수정도 필요합니다.
| export function useToggleCommentLike() { | |
| const queryClient = useQueryClient(); | |
| return useMutation({ | |
| mutationFn: toggleCommentLike, | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: ["post"] }); | |
| }, | |
| }); | |
| } | |
| \ No newline at end of file | |
| } | |
| export function useToggleCommentLike(postId: string) { | |
| const queryClient = useQueryClient(); | |
| return useMutation({ | |
| mutationFn: toggleCommentLike, | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: ["post", postId] }); | |
| }, | |
| }); | |
| } |
| export function useToggleReplyLike() { | ||
| const queryClient = useQueryClient(); | ||
| return useMutation({ | ||
| mutationFn: toggleReplyLike, | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey: ["post"] }); | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
useToggleCommentLike와 동일한 문제입니다. postId를 훅의 인자로 받아 ['post', postId] 키를 무효화하여 불필요한 리페칭을 방지하는 것이 좋습니다. 이 변경은 PostDetailPage.tsx에서 훅을 호출하는 부분의 수정도 필요합니다.
| export function useToggleReplyLike() { | |
| const queryClient = useQueryClient(); | |
| return useMutation({ | |
| mutationFn: toggleReplyLike, | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: ["post"] }); | |
| }, | |
| }); | |
| } | |
| export function useToggleReplyLike(postId: string) { | |
| const queryClient = useQueryClient(); | |
| return useMutation({ | |
| mutationFn: toggleReplyLike, | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: ["post", postId] }); | |
| }, | |
| }); | |
| } |
| export function PostContent({ post, onLikeClick }: PostContentProps) { | ||
| return ( | ||
| <main className="flex flex-col gap-y-4 text-white"> | ||
| <Badge | ||
| variant="outline" | ||
| className="w-fit border-brand-primary text-brand-primary" | ||
| > | ||
| {post.category} | ||
| </Badge> | ||
| <h2 className="text-2xl font-bold">{post.title}</h2> | ||
| <div className="flex items-center gap-x-2 text-sm text-gray-400"> | ||
| <span>{post.author.name}</span> | ||
| <Separator orientation="vertical" className="h-3" /> | ||
| <span>{post.createdAt}</span> | ||
| <main className="w-full flex flex-col text-white"> | ||
| <div className="p-4 flex flex-col gap-y-4"> | ||
| <Badge | ||
| variant="outline" | ||
| className="w-fit border-brand-primary text-brand-primary"> | ||
| {post.category} | ||
| </Badge> | ||
| <h2 className="text-xl font-bold">{post.title}</h2> | ||
| <div className="flex items-center gap-x-2 text-xs text-brand-text"> | ||
| <span>{post.author.name}</span> | ||
| <span>•</span> | ||
| <span>{post.createdAt}</span> | ||
| </div> | ||
| </div> | ||
| <Separator className="my-2 bg-gray-700" /> | ||
| <div className="min-h-[200px] py-4 text-base whitespace-pre-wrap"> | ||
| {post.content} | ||
| </div> | ||
| <div className="flex items-center gap-x-2 py-2"> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| className="h-8 w-8 hover:bg-white/10 rounded-full" | ||
| onClick={onLikeClick} | ||
| disabled={isLikePending} | ||
| > | ||
| <Heart | ||
| className="h-6 w-6" | ||
| fill={post.isLiked ? "#FF4A4A" : "none"} | ||
| stroke={post.isLiked ? "#FF4A4A" : "currentColor"} | ||
| /> | ||
| </Button> | ||
| <span className="text-sm font-semibold">{post.likeCount}</span> | ||
|
|
||
| <Separator className="w-full bg-brand-surface " /> | ||
|
|
||
| <div className="p-4 flex flex-col "> | ||
| <div className="min-h-[200px] text-sm leading-relaxed whitespace-pre-wrap break-words"> | ||
| {post.content} | ||
| </div> | ||
| <div className="flex items-center"> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| className="h-12 w-12 hover:bg-white/10 rounded-full -ml-2" | ||
| onClick={onLikeClick}> | ||
| <Heart | ||
| className="!h-6 !w-6" | ||
| fill={post.isLiked ? "#FF4A4A" : "none"} | ||
| stroke={post.isLiked ? "#FF4A4A" : "currentColor"} | ||
| /> | ||
| </Button> | ||
|
|
||
| <span className="text-xs font-semibold flex items-center -mt-0.5"> | ||
| {post.likeCount} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| <Separator className="bg-gray-700 h-1" /> | ||
|
|
||
| <Separator className="bg-brand-surface !h-1" /> | ||
| </main> | ||
| ); | ||
| } |
There was a problem hiding this comment.
PostContentProps에는 isLikePending prop이 정의되어 있지만, 컴포넌트에서는 사용되지 않고 있습니다. 이로 인해 '좋아요' 버튼이 로딩 중에도 계속 클릭 가능한 상태로 남아있어 중복 요청이 발생할 수 있습니다. isLikePending prop을 다시 사용하고 버튼의 disabled 속성에 바인딩하는 것을 권장합니다.
export function PostContent({ post, onLikeClick, isLikePending }: PostContentProps) {
return (
<main className="w-full flex flex-col text-white">
<div className="p-4 flex flex-col gap-y-4">
<Badge
variant="outline"
className="w-fit border-brand-primary text-brand-primary">
{post.category}
</Badge>
<h2 className="text-xl font-bold">{post.title}</h2>
<div className="flex items-center gap-x-2 text-xs text-brand-text">
<span>{post.author.name}</span>
<span>•</span>
<span>{post.createdAt}</span>
</div>
</div>
<Separator className="w-full bg-brand-surface " />
<div className="p-4 flex flex-col ">
<div className="min-h-[200px] text-sm leading-relaxed whitespace-pre-wrap break-words">
{post.content}
</div>
<div className="flex items-center">
<Button
variant="ghost"
size="icon"
className="h-12 w-12 hover:bg-white/10 rounded-full -ml-2"
onClick={onLikeClick}
disabled={isLikePending}>
<Heart
className="!h-6 !w-6"
fill={post.isLiked ? "#FF4A4A" : "none"}
stroke={post.isLiked ? "#FF4A4A" : "currentColor"}
/>
</Button>
<span className="text-xs font-semibold flex items-center -mt-0.5">
{post.likeCount}
</span>
</div>
</div>
<Separator className="bg-brand-surface !h-1" />
</main>
);
}| const shouldShowBottomNav = ["/board", "/explore", "/chat", "/my"].some( | ||
| (path) => location.pathname.startsWith(path) | ||
| ); |
There was a problem hiding this comment.
shouldShowBottomNav를 결정하는 경로가 하드코딩되어 있습니다. src/constants/navigation.ts의 NAV_ITEMS를 사용하도록 리팩토링하면 중복을 줄이고 내비게이션 경로 변경 시 더 유연하게 대처할 수 있습니다.
예를 들어, 다음과 같이 수정할 수 있습니다:
// import { NAV_ITEMS } from "@/constants/navigation";
const shouldShowBottomNav = NAV_ITEMS.some((item) =>
location.pathname.startsWith(item.path)
);- 공용 모달 래퍼로 레이아웃 변경 - 레이아웃 관련 코드는 공용 모달 컴포넌트에서 담당 - filteringModal에서는 내부 요소(슬라이더, 인풋 등)만 관리하도록 코드 분리
- AddPostModal 컴포넌트와 PostItem, RecruitItem 컴포넌트를 추가하여 게시글 작성 UI 구성 - useAddPostModal 훅을 통해 상태 관리 및 입력 처리 로직 구현 - 기존의 .gitkeep 파일 삭제
📌 PR 종류 (PR Type)
📝 요약 (Summary)
ex) 게시글 리스트 UI 개선 및 반응형 대응
글로벌 레이아웃 구조 개선 및 페이지 UI 개선
🔍 상세 내용 (Describe your changes)
ex)
글로벌 레이아웃 구조 개선
shouldShowBottomNav배열에 입력된 경로는 BottomNavigation 컴포넌트를 사용합니다. 기존 코드에서 페이지마다GlobalLayout을 최상위 래퍼로 호출하고,showBottomNav등으로 헤더/푸터 표시 여부를 설정하는 구조가 비효율적이라고 생각하여 GlobalLayout에서 일괄로 처리하도록 변경하였습니다.p-4를 삭제하였습니다. 페이지에서<div className=p-4>로 요소들을 감싸 사용합니다.헤더 컴포넌트 추가
바텀 네비게이션바 페이지 이동 기능 구현
온보딩 페이지 UI 개선
bg-gray-400등) -> brand- 컬러 사용하도록 변경하였습니다.게시글 상세 페이지 UI 개선
textarea->input으로 변경검색 페이지 UI 개선
📸 스크린샷 (필수)
✅ 확인 사항 (CheckList)
💬 기타 참고사항 (Optional)
ex) 이 부분 로직이 좀 복잡해서 한번 더 확인 부탁드립니다 🙏