Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0", viewport-fit=cover" />
<title>stepbookstep-fe</title>
</head>
<body>
Expand Down
2 changes: 1 addition & 1 deletion src/assets/icons/clock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/icons/home.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/icons/search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/icons/user.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions src/components/BottomBar/BottomBar.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export const styles = {
// 전체 컨테이너
container: `
fixed bottom-0

left-1/2
-translate-x-1/2
w-full
max-w-[375px]
bg-gray-50
border-t border-gray-100

flex justify-around items-center
z-50

pt-[4px]

pb-[calc(4px+env(safe-area-inset-bottom))]

min-h-[62px]
`,

button: `
flex flex-col items-center justify-center
w-full
h-[54px]
cursor-pointer
transition-colors duration-200 ease-in-out
`,

icon: `w-6 h-6 mb-[2px]`,

label: `
text-[10px]
font-sb
leading-[16px]
`,
};
60 changes: 60 additions & 0 deletions src/components/BottomBar/BottomBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useState } from 'react';
import { styles } from './BottomBar.styles';
import type { BottomBarProps, NavItem, TabId } from './BottomBar.types';

import {
HomeIcon,
SearchIcon,
ClockIcon,
UserIcon
} from '../../assets/icons';

const NAV_ITEMS: NavItem[] = [
{ id: 'home', label: '홈', icon: HomeIcon },
{ id: 'search', label: '탐색', icon: SearchIcon },
{ id: 'routine', label: '루틴', icon: ClockIcon },
{ id: 'mypage', label: '마이페이지', icon: UserIcon },
];

const BottomBar = ({ defaultTab = 'home', onTabSelect }: BottomBarProps) => {
const [activeTab, setActiveTab] = useState<TabId>(defaultTab);

const handleTabClick = (id: TabId) => {
setActiveTab(id);
if (onTabSelect) {
onTabSelect(id);
}
};

return (
<nav className={styles.container}>
{NAV_ITEMS.map((item) => {
const isActive = activeTab === item.id;

// 활성 상태면 purple-500, 아니면 gray-400 (비활성 색상)
const activeColor = isActive ? 'text-purple-500' : 'text-gray-500';

// 아이콘 컴포넌트
const IconComponent = item.icon;

return (
<button
key={item.id}
type="button"
onClick={() => handleTabClick(item.id)}
className={`${styles.button} ${activeColor}`}
>

<IconComponent className={`${styles.icon} fill-current`} />

<span className={styles.label}>
{item.label}
</span>
</button>
);
})}
</nav>
);
};

export default BottomBar;
14 changes: 14 additions & 0 deletions src/components/BottomBar/BottomBar.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { FunctionComponent, SVGProps } from 'react';

export type TabId = 'home' | 'search' | 'routine' | 'mypage';

export interface NavItem {
id: TabId;
label: string;
icon: FunctionComponent<SVGProps<SVGSVGElement>>;
}

export interface BottomBarProps {
defaultTab?: TabId;
onTabSelect?: (id: TabId) => void;
}
2 changes: 1 addition & 1 deletion src/components/TextField/TextField.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export interface TextFieldProps extends Omit<React.InputHTMLAttributes<HTMLInput
icon?: boolean;
onIconClick?: () => void;
state?: TextFieldState;
}
}
181 changes: 46 additions & 135 deletions src/pages/PlayGround.tsx
Original file line number Diff line number Diff line change
@@ -1,143 +1,54 @@
import { useState } from "react";
import { TextField } from "@/components/TextField/TextField";
import type { TextFieldState } from "@/components/TextField/TextField.types"; // 타입 import 추가
import { useState } from 'react';
import BottomBar from '../components/BottomBar/BottomBar';
import type { TabId } from '../components/BottomBar/BottomBar.types';

export default function PlayGround() {
// TextField용 state
const [searchValue, setSearchValue] = useState("");
const [filledValue, setFilledValue] = useState("Sample Text");

// Success 및 Error 상태 테스트를 위한 State
const [successValue, setSuccessValue] = useState("Correct Input");
const [errorValue, setErrorValue] = useState("Wrong Input");

// [중요] 7번 인터랙티브 테스트를 위한 State와 로직이 여기 있어야 합니다!
const [emailValue, setEmailValue] = useState("");

// 이메일 유효성 검사 함수
const getEmailState = (value: string): TextFieldState => {
if (value.length === 0) return "default"; // 입력 없으면 기본
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 이메일 정규식
return emailRegex.test(value) ? "success" : "error";
const [currentPage, setCurrentPage] = useState<TabId>('home');

const renderPageContent = () => {
switch (currentPage) {
case 'home':
return (
<div className="flex flex-col items-center justify-center h-full space-y-4">
<h1 className="text-4xl font-bold">홈 페이지</h1>

</div>
);
case 'search':
return (
<div className="flex flex-col items-center justify-center h-full space-y-4">
<h1 className="text-4xl font-bold">탐색 페이지</h1>
</div>
);
case 'routine':
return (
<div className="flex flex-col items-center justify-center h-full space-y-4">
<h1 className="text-4xl font-bold">루틴 페이지</h1>
</div>
);
case 'mypage':
return (
<div className="flex flex-col items-center justify-center h-full space-y-4">
<h1 className="text-4xl font-bold">마이페이지</h1>
</div>
);
default:
return <div>페이지를 찾을 수 없습니다.</div>;
}
};

// 현재 상태 계산
const currentEmailState = getEmailState(emailValue);

return (
<div className="p-8 space-y-8 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold text-gray-900">TextField Component Playground</h1>

{/* TextField 섹션 */}
<section className="space-y-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">TextField - 상태별 예시</h2>

{/* 1. Default State */}
<div className="space-y-2">
<p className="text-sm font-medium text-gray-600">1. Default State (빈 필드)</p>
<TextField
title="Title"
placeholder="Placeholder"
helpText="Help Text"
icon={true}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
</div>

{/* 2. Focus State */}
<div className="space-y-2">
<p className="text-sm font-medium text-gray-600">2. Focus State (클릭 시)</p>
<TextField
title="Title"
placeholder="Placeholder"
helpText="Help Text"
icon={true}
/>
</div>

{/* 3. Filled State */}
<div className="space-y-2">
<p className="text-sm font-medium text-gray-600">3. Filling State</p>
<TextField
title="Title"
placeholder="Placeholder"
helpText="Help Text"
icon={true}
value={filledValue}
onChange={(e) => setFilledValue(e.target.value)}
/>
</div>

{/* 4. Filled State (고정) */}
<div className="space-y-2">
<p className="text-sm font-medium text-gray-600">4. Filled State</p>
<TextField
title="Title"
placeholder="Placeholder"
helpText="Help Text"
icon={true}
/>
</div>

{/* 5. Success State */}
<div className="space-y-2">
<p className="text-sm font-medium text-gray-600">5. Success State</p>
<TextField
title="Title"
placeholder="Placeholder"
helpText="Success Text"
icon={true}
value={successValue}
onChange={(e) => setSuccessValue(e.target.value)}
state="success"
/>
</div>

{/* 6. Error State */}
<div className="space-y-2">
<p className="text-sm font-medium text-gray-600">6. Error State</p>
<TextField
title="Title"
placeholder="Placeholder"
helpText="Error Text"
icon={true}
value={errorValue}
onChange={(e) => setErrorValue(e.target.value)}
state="error"
/>
</div>

{/* 구분선 */}
<hr className="border-gray-200" />

{/* 7. 실제 동작 테스트 (여기가 6번 div 밖으로 나와야 합니다) */}
<div className="space-y-2">
<h2 className="text-lg font-bold text-gray-900">7. Interactive Test (실시간 유효성 검사)</h2>
<p className="text-sm text-gray-600 mb-4">
아래 입력창에 이메일을 입력해보세요. <br/>
- <b>입력 중</b>: 빨간색 (Error) <br/>
- <b>이메일 형식이 완성됨</b>: 파란색 (Success)으로 자동 변경됩니다.
</p>

<TextField
title="이메일 입력"
placeholder="[email protected]"
helpText={
currentEmailState === "error"
? "올바른 이메일 형식이 아닙니다."
: currentEmailState === "success"
? "사용 가능한 이메일입니다."
: "이메일을 입력해주세요."
}
icon={true}
value={emailValue}
onChange={(e) => setEmailValue(e.target.value)}
state={currentEmailState}
/>
</div>

</section>
<div className="min-h-screen">
<main className="w-full h-screen pb-[60px] p-4">
{renderPageContent()}
</main>
<BottomBar
defaultTab="home"
onTabSelect={(tabId) => {
console.log(`[PlayGround] 탭 변경됨: ${tabId}`); // 콘솔 확인용
setCurrentPage(tabId); // 화면 내용 변경
}}
/>
</div>
);
}