-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e06e8cb
commit d8845c6
Showing
12 changed files
with
474 additions
and
380 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { useTheme } from "../contexts/ThemeContext"; | ||
import { memo } from "../hocs"; | ||
import { Item } from "../types"; | ||
import { ComplexForm } from "./ComplexForm"; | ||
import { Header } from "./Header"; | ||
import { ItemList } from "./ItemList"; | ||
import { NotificationSystem } from "./NotificationSystem"; | ||
|
||
interface AppContentProps { | ||
items: Item[]; | ||
onAddItems: () => void; | ||
} | ||
|
||
export const AppContent: React.FC<AppContentProps> = memo( | ||
({ items, onAddItems }) => { | ||
const { theme } = useTheme(); | ||
|
||
return ( | ||
<div | ||
className={`min-h-screen ${theme === "light" ? "bg-gray-100" : "bg-gray-900 text-white"}`} | ||
> | ||
<Header /> | ||
<div className="container mx-auto px-4 py-8"> | ||
<div className="flex flex-col md:flex-row"> | ||
<div className="w-full md:w-1/2 md:pr-4"> | ||
<ItemList items={items} onAddItemsClick={onAddItems} /> | ||
</div> | ||
<div className="w-full md:w-1/2 md:pl-4"> | ||
<ComplexForm /> | ||
</div> | ||
</div> | ||
</div> | ||
<NotificationSystem /> | ||
</div> | ||
); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { useState } from "react"; | ||
import { renderLog } from "../../utils"; | ||
import { useNotification } from "../contexts/NotificationContext"; | ||
import { memo } from "../hocs"; | ||
import { useCallback } from "../hooks"; | ||
import { PREFERENCES } from "../constants"; | ||
|
||
export const ComplexForm: React.FC = memo(() => { | ||
renderLog("ComplexForm rendered"); | ||
const { addNotification } = useNotification(); | ||
const [formData, setFormData] = useState({ | ||
name: "", | ||
email: "", | ||
age: 0, | ||
preferences: [] as string[], | ||
}); | ||
|
||
const handleSubmit = useCallback( | ||
(e: React.FormEvent) => { | ||
e.preventDefault(); | ||
addNotification("폼이 성공적으로 제출되었습니다", "success"); | ||
}, | ||
[addNotification], | ||
); | ||
|
||
const handleInputChange = useCallback( | ||
(e: React.ChangeEvent<HTMLInputElement>) => { | ||
const { name, value } = e.target; | ||
setFormData((prev) => ({ | ||
...prev, | ||
[name]: name === "age" ? parseInt(value) || 0 : value, | ||
})); | ||
}, | ||
[], | ||
); | ||
|
||
const handlePreferenceChange = useCallback((preference: string) => { | ||
setFormData((prev) => ({ | ||
...prev, | ||
preferences: prev.preferences.includes(preference) | ||
? prev.preferences.filter((p) => p !== preference) | ||
: [...prev.preferences, preference], | ||
})); | ||
}, []); | ||
|
||
return ( | ||
<div className="mt-8"> | ||
<h2 className="text-2xl font-bold mb-4">복잡한 폼</h2> | ||
<form onSubmit={handleSubmit} className="space-y-4"> | ||
<input | ||
type="text" | ||
name="name" | ||
value={formData.name} | ||
onChange={handleInputChange} | ||
placeholder="이름" | ||
className="w-full p-2 border border-gray-300 rounded text-black" | ||
/> | ||
<input | ||
type="email" | ||
name="email" | ||
value={formData.email} | ||
onChange={handleInputChange} | ||
placeholder="이메일" | ||
className="w-full p-2 border border-gray-300 rounded text-black" | ||
/> | ||
<input | ||
type="number" | ||
name="age" | ||
value={formData.age} | ||
onChange={handleInputChange} | ||
placeholder="나이" | ||
className="w-full p-2 border border-gray-300 rounded text-black" | ||
/> | ||
<div className="space-x-4"> | ||
{PREFERENCES.map((pref) => ( | ||
<label key={pref} className="inline-flex items-center"> | ||
<input | ||
type="checkbox" | ||
checked={formData.preferences.includes(pref)} | ||
onChange={() => handlePreferenceChange(pref)} | ||
className="form-checkbox h-5 w-5 text-blue-600" | ||
/> | ||
<span className="ml-2">{pref}</span> | ||
</label> | ||
))} | ||
</div> | ||
<button | ||
type="submit" | ||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" | ||
> | ||
제출 | ||
</button> | ||
</form> | ||
</div> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { renderLog } from "../../utils"; | ||
import { useTheme } from "../contexts/ThemeContext"; | ||
import { useUser } from "../contexts/UserContext"; | ||
import { memo } from "../hocs"; | ||
import { useCallback } from "../hooks"; | ||
|
||
export const Header: React.FC = memo(() => { | ||
renderLog("Header rendered"); | ||
const { theme, toggleTheme } = useTheme(); | ||
const { user, login, logout } = useUser(); | ||
|
||
const handleLogin = useCallback(() => { | ||
// 실제 애플리케이션에서는 사용자 입력을 받아야 합니다. | ||
login("[email protected]", "password"); | ||
}, []); | ||
|
||
return ( | ||
<header className="bg-gray-800 text-white p-4"> | ||
<div className="container mx-auto flex justify-between items-center"> | ||
<h1 className="text-2xl font-bold">샘플 애플리케이션</h1> | ||
<div className="flex items-center"> | ||
<button | ||
onClick={toggleTheme} | ||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-2" | ||
> | ||
{theme === "light" ? "다크 모드" : "라이트 모드"} | ||
</button> | ||
{user ? ( | ||
<div className="flex items-center"> | ||
<span className="mr-2">{user.name}님 환영합니다!</span> | ||
<button | ||
onClick={logout} | ||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded" | ||
> | ||
로그아웃 | ||
</button> | ||
</div> | ||
) : ( | ||
<button | ||
onClick={handleLogin} | ||
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded" | ||
> | ||
로그인 | ||
</button> | ||
)} | ||
</div> | ||
</div> | ||
</header> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { useState } from "react"; | ||
import { renderLog } from "../../utils"; | ||
import { memo } from "../hocs"; | ||
import { Item } from "../types"; | ||
import { useTheme } from "../contexts/ThemeContext"; | ||
import { useMemo } from "../hooks"; | ||
|
||
export const ItemList = memo<{ | ||
items: Item[]; | ||
onAddItemsClick: () => void; | ||
}>(({ items, onAddItemsClick }) => { | ||
renderLog("ItemList rendered"); | ||
const [filter, setFilter] = useState(""); | ||
|
||
const { theme } = useTheme(); | ||
|
||
const filteredItems = useMemo( | ||
() => | ||
items.filter( | ||
(item) => | ||
item.name.toLowerCase().includes(filter.toLowerCase()) || | ||
item.category.toLowerCase().includes(filter.toLowerCase()), | ||
), | ||
[items, filter], | ||
); | ||
|
||
const totalPrice = filteredItems.reduce((sum, item) => sum + item.price, 0); | ||
|
||
const averagePrice = Math.round(totalPrice / filteredItems.length) || 0; | ||
|
||
return ( | ||
<div className="mt-8"> | ||
<div className="flex justify-between items-center mb-4"> | ||
<h2 className="text-2xl font-bold">상품 목록</h2> | ||
<div> | ||
<button | ||
type="button" | ||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-xs" | ||
onClick={onAddItemsClick} | ||
> | ||
대량추가 | ||
</button> | ||
</div> | ||
</div> | ||
<input | ||
type="text" | ||
placeholder="상품 검색..." | ||
value={filter} | ||
onChange={(e) => setFilter(e.target.value)} | ||
className="w-full p-2 mb-4 border border-gray-300 rounded text-black" | ||
/> | ||
<ul className="mb-4 mx-4 flex gap-3 text-sm justify-end"> | ||
<li>검색결과: {filteredItems.length.toLocaleString()}개</li> | ||
<li>전체가격: {totalPrice.toLocaleString()}원</li> | ||
<li>평균가격: {averagePrice.toLocaleString()}원</li> | ||
</ul> | ||
<ul className="space-y-2"> | ||
{filteredItems.map((item, index) => ( | ||
<li | ||
key={index} | ||
className={`p-2 rounded shadow ${theme === "light" ? "bg-white text-black" : "bg-gray-700 text-white"}`} | ||
> | ||
{item.name} - {item.category} - {item.price.toLocaleString()}원 | ||
</li> | ||
))} | ||
</ul> | ||
</div> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { renderLog } from "../../utils"; | ||
import { useNotification } from "../contexts/NotificationContext"; | ||
import { memo } from "../hocs"; | ||
|
||
// NotificationSystem 컴포넌트 | ||
export const NotificationSystem: React.FC = memo(() => { | ||
renderLog("NotificationSystem rendered"); | ||
const { notifications, removeNotification } = useNotification(); | ||
|
||
return ( | ||
<div className="fixed bottom-4 right-4 space-y-2"> | ||
{notifications.map((notification) => ( | ||
<div | ||
key={notification.id} | ||
className={`p-4 rounded shadow-lg ${ | ||
notification.type === "success" | ||
? "bg-green-500" | ||
: notification.type === "error" | ||
? "bg-red-500" | ||
: notification.type === "warning" | ||
? "bg-yellow-500" | ||
: "bg-blue-500" | ||
} text-white`} | ||
> | ||
{notification.message} | ||
<button | ||
onClick={() => removeNotification(notification.id)} | ||
className="ml-4 text-white hover:text-gray-200" | ||
> | ||
닫기 | ||
</button> | ||
</div> | ||
))} | ||
</div> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const PREFERENCES = ["독서", "운동", "음악", "여행"] as const; | ||
export const INITIAL_ITEMS_COUNT = 1000; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// contexts/NotificationContext.tsx | ||
import { createContext, useContext, useState } from "react"; | ||
import { Notification } from "../types"; | ||
import { useMemo } from "../hooks"; | ||
|
||
const NotificationsContext = createContext<Notification[] | undefined>( | ||
undefined, | ||
); | ||
|
||
interface NotificationActionsType { | ||
addNotification: (message: string, type: Notification["type"]) => void; | ||
removeNotification: (id: number) => void; | ||
} | ||
|
||
const NotificationActionsContext = createContext< | ||
NotificationActionsType | undefined | ||
>(undefined); | ||
|
||
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ | ||
children, | ||
}) => { | ||
const [notifications, setNotifications] = useState<Notification[]>([]); | ||
|
||
const actions = useMemo( | ||
() => ({ | ||
addNotification: (message: string, type: Notification["type"]) => { | ||
const newNotification: Notification = { | ||
id: Date.now(), | ||
message, | ||
type, | ||
}; | ||
setNotifications((prev) => [...prev, newNotification]); | ||
}, | ||
removeNotification: (id: number) => { | ||
setNotifications((prev) => | ||
prev.filter((notification) => notification.id !== id), | ||
); | ||
}, | ||
}), | ||
[], | ||
); | ||
|
||
return ( | ||
<NotificationActionsContext.Provider value={actions}> | ||
<NotificationsContext.Provider value={notifications}> | ||
{children} | ||
</NotificationsContext.Provider> | ||
</NotificationActionsContext.Provider> | ||
); | ||
}; | ||
|
||
export const useNotifications = () => { | ||
const context = useContext(NotificationsContext); | ||
if (!context) | ||
throw new Error("NotificationProvider 내에서 사용되어야 합니다."); | ||
return context; | ||
}; | ||
|
||
export const useNotificationActions = () => { | ||
const context = useContext(NotificationActionsContext); | ||
if (!context) | ||
throw new Error("NotificationProvider 내에서 사용되어야 합니다."); | ||
return context; | ||
}; | ||
|
||
export const useNotification = () => { | ||
return { | ||
notifications: useNotifications(), | ||
...useNotificationActions(), | ||
}; | ||
}; |
Oops, something went wrong.