Skip to content
Merged
20 changes: 1 addition & 19 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,8 @@
import { Outlet } from "react-router-dom";
import GlobalLayout from "./components/layout/GlobalLayout";
import { OnboardingPage } from "./pages/onboarding/OnboardingPage";

function App() {
const path = window.location.pathname;

let content;
if (path === '/onboarding') {
content = <OnboardingPage />;
} else {
content = (
<>
<p className="text-brand-primary">brand-primary 색상 테스트</p>
<p className="bg-brand-surface text-brand-text">brand 색상 테스트</p>
<hr className="my-8 border-gray-600" />
<a href="/onboarding" className="text-lg font-bold text-lime-400 hover:underline"> 온보딩 페이지 테스트 </a>
</>
);
}

return (
<GlobalLayout variant="black">
<GlobalLayout>
<Outlet />
</GlobalLayout>
);
Expand Down
17 changes: 10 additions & 7 deletions src/components/common/AppTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,28 @@ interface Tab {
label: string;
}

interface AppTabsProps {
tabs: readonly Tab[];
value: string;
onValueChange: (value: string) => void;
interface AppTabsProps<T extends string> {
tabs: readonly (Tab & { value: T })[];
value: T;
onValueChange: (value: T) => void;
listClassName?: string;
triggerClassName?: string;
className?: string;
}

export const AppTabs = ({
export const AppTabs = <T extends string>({
tabs,
value,
onValueChange,
listClassName,
triggerClassName,
className,
}: AppTabsProps) => {
}: AppTabsProps<T>) => {
return (
<Tabs value={value} onValueChange={onValueChange} className={cn(className)}>
<Tabs
value={value}
onValueChange={(value: string) => onValueChange(value as T)}
className={cn(className)}>
<TabsList
className={cn(
"grid w-full bg-brand-background rounded-none p-0 border-b-2 border-brand-surface",
Expand Down
71 changes: 71 additions & 0 deletions src/components/profile/ProfileSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Camera } from "lucide-react";

interface ProfileSectionProps {
isEditable?: boolean;
}

export const ProfileSection = ({ isEditable = false }: ProfileSectionProps) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);

const handleImageClick = () => {
if (!isEditable) return;
fileInputRef.current?.click();
};

const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();

reader.onloadend = () => {
if (typeof reader.result === "string") {
setImagePreview(reader.result);
}
};
reader.readAsDataURL(file);
}
};

return (
<div className="flex flex-col items-center mt-8">
<div className="relative">
<div
className={`w-32 h-32 rounded-full bg-brand-primary flex items-center justify-center overflow-hidden ${
isEditable ? "cursor-pointer" : ""
}`}
onClick={handleImageClick}>
{imagePreview ? (
<img
src={imagePreview}
alt="Profile preview"
className="w-full h-full object-cover"
/>
) : (
isEditable && <Camera className="size-12 text-brand-background" />
)}
</div>
{isEditable && (
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept="image/*"
/>
)}
</div>
<h2 className="text-2xl font-bold mt-4 text-white">이름</h2>
<p className="text-brand-text mt-1">나이 · 포지션</p>
{isEditable && (
<Button
variant="outline"
className="mt-4 bg-transparent border-brand-surface text-white hover:bg-brand-surface/50 hover:text-white">
프로필 설정
</Button>
)}
</div>
);
};
8 changes: 8 additions & 0 deletions src/constants/my.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const MENU_ITEMS = [
{ id: "posts", label: "작성한 글" },
{ id: "history", label: "활동 이력" },
{ id: "likes", label: "좋아요 한 글" },
{ id: "bookmarks", label: "북마크" },
] as const;

export type MenuItemId = (typeof MENU_ITEMS)[number]["id"];
12 changes: 12 additions & 0 deletions src/constants/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const DEVELOPER_TABS = [
{
value: "profile",
label: "프로필",
},
{
value: "posts",
label: "작성한 글 ",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

label 값 끝에 불필요한 공백이 포함되어 있습니다. UI에 의도치 않은 영향을 줄 수 있으므로 제거하는 것이 좋습니다.

Suggested change
label: "작성한 글 ",
label: "작성한 글",

},
] as const;

export type DeveloperTabValue = (typeof DEVELOPER_TABS)[number]["value"];
10 changes: 10 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import "./index.css";
import App from "./App.tsx";
import { LoginPage } from "./pages/auth/LoginPage.tsx";
import { BoardPage } from "./pages/home/index.tsx";
import { MyPage } from "./pages/my/index.tsx";
import { DeveloperPage } from "./pages/developer/index.tsx";
import { ExplorePage } from "./pages/explore/index.tsx";

async function enableMocking() {
Expand All @@ -31,6 +33,14 @@ const router = createBrowserRouter([
path: "explore",
element: <ExplorePage />,
},
{
path: "my",
element: <MyPage />,
},
{
path: "developer/:id",
element: <DeveloperPage />,
},
],
},
{
Expand Down
5 changes: 5 additions & 0 deletions src/pages/developer/_components/Posts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Posts = () => {
return <h1>작성글</h1>;
};

export default Posts;
5 changes: 5 additions & 0 deletions src/pages/developer/_components/Profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Profile = () => {
return <h1>프로필</h1>;
};

export default Profile;
34 changes: 34 additions & 0 deletions src/pages/developer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useState } from "react";
import { AppTabs } from "@/components/common/AppTabs";
import { ProfileSection } from "@/components/profile/ProfileSection.tsx";
import type { DeveloperTabValue } from "@/constants/profile";
import { DEVELOPER_TABS } from "@/constants/profile";
import Posts from "./_components/Posts.tsx";
import Profile from "./_components/Profile.tsx";

export const DeveloperPage = () => {
const [activeTab, setActiveTab] = useState<DeveloperTabValue>("profile");

const handleTabChange = (value: DeveloperTabValue) => {
setActiveTab(value);
};

return (
<div className="dark text-foreground flex flex-col -mx-4">
<div className="px-4">
<ProfileSection />
</div>
<AppTabs
tabs={DEVELOPER_TABS}
value={activeTab}
onValueChange={handleTabChange}
listClassName="grid-cols-2"
className="w-full mt-8"
/>
<div className="w-full mt-4 text-white px-4">
{activeTab === "profile" && <Profile />}
{activeTab === "posts" && <Posts />}
</div>
</div>
);
};
28 changes: 28 additions & 0 deletions src/pages/my/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ProfileSection } from "@/components/profile/ProfileSection";
import { ChevronRight } from "lucide-react";
import { MENU_ITEMS, type MenuItemId } from "@/constants/my";

export const MyPage = () => {
const handleMenuItemClick = (menuId: MenuItemId) => {
console.log(`선택된 메뉴: ${menuId}`);
};

return (
<div className="dark text-foreground flex flex-col items-center">
<ProfileSection isEditable />
<div className="w-full mt-8 border-t border-brand-surface">
<ul className="text-white">
{MENU_ITEMS.map((item) => (
<li
key={item.id}
onClick={() => handleMenuItemClick(item.id)}
className="flex justify-between items-center py-4 border-b border-brand-surface cursor-pointer hover:bg-brand-surface/50 transition-colors">
<span>{item.label}</span>
<ChevronRight className="size-5 text-brand-text" />
</li>
))}
</ul>
</div>
</div>
);
};