Skip to content

✨ feat: 프로필(/profile) 페이지 구현 #68

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

Merged
merged 18 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0cf9c37
✨ feat: profile type, constant 추가
junyeokk Feb 19, 2025
5c27958
✨ feat: profile header 컴포넌트 추가
junyeokk Feb 19, 2025
4a087b0
✨ feat: profile sidebar 컴포넌트 추가
junyeokk Feb 19, 2025
a5e77f8
✨ feat: profile section 컴포넌트 추가
junyeokk Feb 19, 2025
93ef094
✨ feat: profile 페이지, 라우팅 추가
junyeokk Feb 19, 2025
02fc23d
♻️ refactor: 프로필 클릭 시 맨 위로 스크롤 되지 않는 현상 해결
junyeokk Feb 19, 2025
377d2e5
✨ feat: activity 컴포넌트 관련 타입 추가
junyeokk Feb 19, 2025
c4374c8
✨ feat: activity 컴포넌트 Mock 수정
junyeokk Feb 19, 2025
3a61af6
✨ feat: activity 컴포넌트 추가
junyeokk Feb 19, 2025
570b31b
📦 chore: lodash 의존성 추가
junyeokk Feb 20, 2025
f1c9bfa
✨ feat: pipe로 데이터 변환 로직 개선 및 그리드 날짜 정렬
junyeokk Feb 20, 2025
894f81f
♻️ refactor: activity mock data 생성 방식 수정
junyeokk Feb 20, 2025
dc138bd
♻️ refactor: activity 순수 함수, pipeline, 컴포넌트 분리
junyeokk Feb 20, 2025
d18f7b5
Merge branch 'main' of https://github.com/boostcampwm-2024/refactor-w…
junyeokk Feb 21, 2025
410f45e
♻️ refactor: dayCell tooltip hover 시 duration 200ms로 변경 및 UX 향상
junyeokk Feb 21, 2025
d35359a
Merge branch 'main' into feat/profile-page
junyeokk Feb 21, 2025
884739a
♻️ refactor: profile 컴포넌트 import 상대 경로 였던 부분들 모두 절대 경로로 변경
junyeokk Feb 21, 2025
6bfbd8f
Merge branch 'feat/profile-page' of https://github.com/boostcampwm-20…
junyeokk Feb 21, 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
10 changes: 8 additions & 2 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/lodash": "^4.17.15",
"@types/node": "^22.9.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
Expand Down
19 changes: 14 additions & 5 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import { lazy, Suspense, useEffect } from "react";
import { Routes, Route, useLocation } from "react-router-dom";

import PostDetail from "@/components/common/Card/PostDetail";
import { Toaster } from "@/components/ui/toaster.tsx";
import { Toaster } from "@/components/ui/toaster";

import Loading from "@/pages/Loading.tsx";
import Loading from "@/pages/Loading";
import PostDetailPage from "@/pages/PostDetailPage";
import SignIn from "@/pages/SignIn.tsx";
import SignUp from "@/pages/SignUp.tsx";
import Profile from "@/pages/Profile";
import SignIn from "@/pages/SignIn";
import SignUp from "@/pages/SignUp";

import { useMediaQuery } from "@/hooks/common/useMediaQuery";

import { denamuAscii } from "@/constants/denamuAscii.ts";
import { denamuAscii } from "@/constants/denamuAscii";

import { useMediaStore } from "@/store/useMediaStore";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
Expand Down Expand Up @@ -89,6 +90,14 @@ export default function App() {
</Suspense>
}
/>
<Route
path="/profile"
element={
<Suspense fallback={<Loading />}>
<Profile />
</Suspense>
}
/>
<Route
path="/:id"
element={
Expand Down
16 changes: 16 additions & 0 deletions client/src/components/profile/common/Section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Card, CardContent } from "@/components/ui/card.tsx";

interface ProfileTempSectionProps {
title: string;
}

export const Section = ({ title }: ProfileTempSectionProps) => {
return (
<Card className="mb-8 overflow-hidden">
<CardContent className="p-6 h-96">
<h3 className="text-lg font-semibold mb-4">{title}</h3>
<p className="text-gray-400">서비스가 현재 개발 중입니다. 곧 만나요!</p>
</CardContent>
</Card>
);
};
30 changes: 30 additions & 0 deletions client/src/components/profile/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Card, CardContent } from "@/components/ui/card.tsx";

import { Avatar, Banner, Info, Stats } from "./index";
import { ActivityGraph } from "./ui/ActivityGraph/ActivityGraph.tsx";
import { User } from "@/types/profile.ts";

interface ProfileHeaderProps {
user: User;
}

export const Header = ({ user }: ProfileHeaderProps) => {
return (
<Card className="mb-8 overflow-hidden">
{user.rssRegistered && <Banner lastPosted={user.lastPosted} />}

<CardContent className="p-6">
<div className="flex items-start justify-between">
<div className="flex space-x-6">
<Avatar user={user} />
<Info user={user} />
</div>
</div>
<Stats totalPosts={user.totalPosts} totalViews={user.totalViews} topicsCount={user.topics.length} />
<div className="mt-6">
<ActivityGraph dailyActivities={user.dailyActivities} />
</div>
</CardContent>
</Card>
);
};
4 changes: 4 additions & 0 deletions client/src/components/profile/header/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./ui/Avatar";
export * from "./ui/Banner";
export * from "./ui/Info";
export * from "./ui/Stats";
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { TooltipProvider } from "@/components/ui/tooltip.tsx";

import { processActivityData } from "@/utils/activity.ts";

import { DayLabels } from "./DayLabels.tsx";
import { Legend } from "./Legend.tsx";
import { MonthLabels } from "./MonthLabels.tsx";
import { Week } from "./Week.tsx";
import { DailyActivity } from "@/types/profile.ts";

interface ActivityGraphProps {
dailyActivities: DailyActivity[];
}

export const ActivityGraph = ({ dailyActivities }: ActivityGraphProps) => {
const today = new Date();
const { weeks } = processActivityData(dailyActivities, today);

return (
<div className="p-4 bg-white rounded-lg">
<h3 className="text-lg font-semibold mb-4">Activity</h3>
<TooltipProvider>
<div className="flex flex-col">
<MonthLabels weeks={weeks} />
<div className="flex">
<DayLabels />
<div className="flex gap-0.5">
{weeks.map((weekInfo) => (
<Week key={weekInfo.weekNumber} weekInfo={weekInfo} />
))}
</div>
</div>
</div>
</TooltipProvider>
<Legend />
</div>
);
};
16 changes: 16 additions & 0 deletions client/src/components/profile/header/ui/ActivityGraph/DayCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip.tsx";

import { getColorClass } from "@/utils/color.ts";

import { DayInfo } from "@/types/activity.ts";

export const DayCell = ({ dayInfo }: { dayInfo: DayInfo }) => (
<Tooltip>
<TooltipTrigger>
<div className={`w-2.5 h-2.5 rounded-sm ${getColorClass(dayInfo.count)}`} />
</TooltipTrigger>
<TooltipContent>
<p>{`${dayInfo.dateStr}: ${dayInfo.count} views`}</p>
</TooltipContent>
</Tooltip>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const DayLabels = () => (
<div className="flex flex-col justify-between text-xs text-gray-400 pr-2">
<span className="translate-y-2">Mon</span>
<span>Wed</span>
<span className="-translate-y-2">Fri</span>
</div>
);
13 changes: 13 additions & 0 deletions client/src/components/profile/header/ui/ActivityGraph/Legend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const Legend = () => (
<div className="mt-4 flex items-center text-xs text-gray-500 space-x-2">
<span>Less</span>
<div className="flex space-x-0.5">
<div className="w-3 h-3 bg-gray-100 rounded-sm" />
<div className="w-3 h-3 bg-green-200 rounded-sm" />
<div className="w-3 h-3 bg-green-300 rounded-sm" />
<div className="w-3 h-3 bg-green-400 rounded-sm" />
<div className="w-3 h-3 bg-green-500 rounded-sm" />
</div>
<span>More</span>
</div>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { WeekInfo } from "@/types/activity.ts";

export const MonthLabels = ({ weeks }: { weeks: WeekInfo[] }) => {
const monthPositions = weeks.reduce(
(acc, week, index) => {
const firstDayOfWeek = week.days[0];
const month = firstDayOfWeek.date.toLocaleString("en-US", { month: "short" });

if (index === 0 || month !== weeks[index - 1].days[0].date.toLocaleString("en-US", { month: "short" })) {
acc.push({
month,
position: `${index * 0.75}rem`,
});
}
return acc;
},
[] as Array<{ month: string; position: string }>
);

return (
<div className="flex mb-5 pl-6">
<div className="relative flex">
{monthPositions.map(({ month, position }, index) => (
<div key={`${month}-${index}`} className="absolute text-xs text-gray-400" style={{ left: position }}>
{month}
</div>
))}
</div>
</div>
);
};
10 changes: 10 additions & 0 deletions client/src/components/profile/header/ui/ActivityGraph/Week.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DayCell } from "./DayCell.tsx";
import { WeekInfo } from "@/types/activity.ts";

export const Week = ({ weekInfo }: { weekInfo: WeekInfo }) => (
<div className="grid grid-rows-7 gap-0.5">
{weekInfo.days.map((day) => (
<DayCell key={day.dateStr} dayInfo={day} />
))}
</div>
);
25 changes: 25 additions & 0 deletions client/src/components/profile/header/ui/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CheckCircle2 } from "lucide-react";

import { Avatar as AvatarUI, AvatarFallback, AvatarImage } from "@/components/ui/avatar.tsx";

import { User } from "@/types/profile.ts";

interface ProfileHeaderAvatarProps {
user: User;
}

export const Avatar = ({ user }: ProfileHeaderAvatarProps) => {
return (
<div className="relative w-24 h-24">
<AvatarUI className="w-24 h-24 border-4 border-white">
<AvatarImage src={user.avatar} />
<AvatarFallback>KD</AvatarFallback>
</AvatarUI>
{user.rssRegistered && (
<div className="absolute -bottom-2 -right-2 bg-white rounded-full p-1 shadow-md">
<CheckCircle2 className="w-5 h-5 text-blue-500" />
</div>
)}
</div>
);
};
17 changes: 17 additions & 0 deletions client/src/components/profile/header/ui/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CheckCircle2 } from "lucide-react";

interface ProfileHeaderBannerProps {
lastPosted: string;
}

export const Banner = ({ lastPosted }: ProfileHeaderBannerProps) => {
return (
<div className="bg-blue-500 px-6 py-2 text-white flex items-center justify-between">
<div className="flex items-center space-x-2">
<CheckCircle2 className="w-5 h-5" />
<span>인증된 RSS 블로거</span>
</div>
<span className="text-sm">마지막 포스팅: {lastPosted}</span>
</div>
);
};
38 changes: 38 additions & 0 deletions client/src/components/profile/header/ui/Info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Edit } from "lucide-react";

import { Badge } from "@/components/ui/badge.tsx";
import { Button } from "@/components/ui/button.tsx";

import { User } from "@/types/profile.ts";

interface ProfileHeaderInfoProps {
user: User;
}

export const Info = ({ user }: ProfileHeaderInfoProps) => {
return (
<div>
<div className="flex items-center space-x-4">
<h1 className="text-2xl font-bold">{user.name}</h1>
<Button variant="outline" size="sm" className="flex items-center">
<Edit className="w-4 h-4 mr-2" />
프로필 수정
</Button>
</div>
<p className="text-gray-600 mt-1">{user.email}</p>
{user.blogUrl && (
<a href={user.blogUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 mt-2 hover:underline">
{user.blogUrl}
</a>
)}
<p className="text-gray-800 mt-4">{user.bio}</p>
<div className="flex flex-wrap gap-2 mt-4">
{user.topics.map((topic) => (
<Badge key={topic} variant="secondary">
{topic}
</Badge>
))}
</div>
</div>
);
};
24 changes: 24 additions & 0 deletions client/src/components/profile/header/ui/Stats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
interface ProfileStatsProps {
totalPosts: number;
totalViews: number;
topicsCount: number;
}

export const Stats = ({ totalPosts, totalViews, topicsCount }: ProfileStatsProps) => {
return (
<div className="grid grid-cols-3 gap-4 mt-8 p-4 bg-gray-50 rounded-lg">
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">{totalPosts}</p>
<p className="text-sm text-gray-600">총 포스팅</p>
</div>
<div className="text-center border-l border-r border-gray-200">
<p className="text-2xl font-bold text-blue-600">{totalViews.toLocaleString()}</p>
<p className="text-sm text-gray-600">월간 조회수</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">{topicsCount}</p>
<p className="text-sm text-gray-600">관심 토픽</p>
</div>
</div>
);
};
9 changes: 9 additions & 0 deletions client/src/components/profile/sections/LikedPosts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Section } from "../common/Section.tsx";

export const LikedPosts = () => {
return (
<section id="liked-posts">
<Section title="좋아요한 목록" />
</section>
);
};
Loading