Skip to content

Commit 0906bfd

Browse files
committed
Merge remote-tracking branch 'origin/feature/sponsor'
2 parents 0dfc9b5 + 20fbd31 commit 0906bfd

File tree

8 files changed

+240
-17
lines changed

8 files changed

+240
-17
lines changed

.prettierrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"semi": true,
33
"singleQuote": false,
44
"tabWidth": 2,
5-
"printWidth": 80,
5+
"printWidth": 120,
66
"trailingComma": "es5",
77
"arrowParens": "always",
88
"endOfLine": "auto"
9-
}
9+
}

apps/pyconkr/src/App.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,39 @@
11
import * as Common from "@frontend/common";
22
import React from "react";
3-
import { BrowserRouter, Route, Routes } from "react-router-dom";
3+
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";
4+
import { SponsorProvider } from "./contexts/SponsorContext";
45

56
import MainLayout from "./components/layout";
67
import { Test } from "./components/pages/test";
78
import { IS_DEBUG_ENV } from "./consts/index.ts";
89

9-
export const App: React.FC = () => {
10+
// 스폰서를 표시할 페이지 경로 설정
11+
const SPONSOR_VISIBLE_PATHS = ["/"];
12+
13+
const AppContent = () => {
14+
const location = useLocation();
15+
const shouldShowSponsor = SPONSOR_VISIBLE_PATHS.includes(location.pathname);
16+
1017
return (
11-
<BrowserRouter>
18+
<SponsorProvider initialVisibility={shouldShowSponsor}>
1219
<Routes>
1320
<Route element={<MainLayout />}>
1421
{IS_DEBUG_ENV && <Route path="/debug" element={<Test />} />}
15-
<Route path="/pages/:id" element={<Common.Components.PageIdParamRenderer />} />
22+
<Route
23+
path="/pages/:id"
24+
element={<Common.Components.PageIdParamRenderer />}
25+
/>
1626
<Route path="*" element={<Common.Components.RouteRenderer />} />
1727
</Route>
1828
</Routes>
29+
</SponsorProvider>
30+
);
31+
};
32+
33+
export const App: React.FC = () => {
34+
return (
35+
<BrowserRouter>
36+
<AppContent />
1937
</BrowserRouter>
2038
);
2139
};

apps/pyconkr/src/assets/sponsorExample.svg

Lines changed: 9 additions & 0 deletions
Loading

apps/pyconkr/src/components/layout/Nav/index.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,15 @@ const menus = [
6060
];
6161

6262
export default function Nav() {
63-
const { hoveredMenu, focusedMenu, menuRefs, setHoveredMenu, setFocusedMenu, handleKeyDown, handleBlur } = useMenu();
63+
const {
64+
hoveredMenu,
65+
focusedMenu,
66+
menuRefs,
67+
setHoveredMenu,
68+
setFocusedMenu,
69+
handleKeyDown,
70+
handleBlur,
71+
} = useMenu();
6472

6573
const [isSubMenuHovered, setIsSubMenuHovered] = useState(false);
6674
const [hoveredSubItem, setHoveredSubItem] = useState<string | null>(null);
@@ -73,12 +81,17 @@ export default function Nav() {
7381
}, [hoveredMenu, focusedMenu]);
7482

7583
const showSubmenu = !!hoveredMenu || !!focusedMenu || isSubMenuHovered;
76-
const activeMenu = hoveredMenu || focusedMenu || (isSubMenuHovered ? lastActiveMenuRef.current : null);
84+
const activeMenu =
85+
hoveredMenu ||
86+
focusedMenu ||
87+
(isSubMenuHovered ? lastActiveMenuRef.current : null);
7788
const currentMenu = menus.find((menu) => menu.text === activeMenu);
7889

7990
const hasActiveThirdLevel = useMemo(() => {
8091
if (!hoveredSubItem || !currentMenu) return false;
81-
const activeSubItem = currentMenu.subMenu.find((item) => item.text === hoveredSubItem);
92+
const activeSubItem = currentMenu.subMenu.find(
93+
(item) => item.text === hoveredSubItem
94+
);
8295
return activeSubItem?.subMenu && activeSubItem.subMenu.length > 0;
8396
}, [currentMenu, hoveredSubItem]);
8497

@@ -123,7 +136,9 @@ export default function Nav() {
123136
<SecondLevelItem
124137
key={subItem.text}
125138
onMouseEnter={() => setHoveredSubItem(subItem.text)}
126-
className={hoveredSubItem === subItem.text ? "active" : ""}
139+
className={
140+
hoveredSubItem === subItem.text ? "active" : ""
141+
}
127142
>
128143
<a href={subItem.href} tabIndex={0}>
129144
{subItem.text}
@@ -146,7 +161,8 @@ export default function Nav() {
146161
}}
147162
>
148163
{currentMenu.subMenu.map((subItem) => {
149-
const hasThirdLevel = subItem.subMenu && subItem.subMenu.length > 0;
164+
const hasThirdLevel =
165+
subItem.subMenu && subItem.subMenu.length > 0;
150166
const isActive = hoveredSubItem === subItem.text;
151167

152168
if (!hasThirdLevel || !isActive) return null;
@@ -186,14 +202,16 @@ const NavSubContainer = styled.div`
186202
height: auto;
187203
min-height: 150px;
188204
background-color: rgba(255, 255, 255, 0.7);
189-
background-image: linear-gradient(rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.45));
205+
background-image: linear-gradient(
206+
rgba(255, 255, 255, 0.7),
207+
rgba(255, 255, 255, 0.45)
208+
);
190209
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
191210
position: fixed;
192211
left: 0;
193212
top: 60px;
194213
z-index: 1500;
195214
display: flex;
196-
justify-content: center;
197215
padding-top: 34px;
198216
padding-bottom: 34px;
199217
overflow-y: auto;
@@ -205,7 +223,7 @@ const SubMenuWrapper = styled.div`
205223
max-width: 1200px;
206224
width: 100%;
207225
height: auto;
208-
padding-left: 118px;
226+
padding-left: 114px;
209227
box-sizing: border-box;
210228
`;
211229

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import styled from "@emotion/styled";
2+
import { useEffect, useState } from "react";
3+
import SponsorExample from "../../../assets/sponsorExample.svg?react";
4+
import { useSponsor } from "../../../contexts/SponsorContext";
5+
6+
interface Sponsor {
7+
id: number;
8+
name: string;
9+
Logo: React.ComponentType;
10+
}
11+
12+
export default function Sponsor() {
13+
const [sponsors, setSponsors] = useState<Sponsor[]>([]);
14+
const { isVisible } = useSponsor();
15+
16+
// 16개의 임시 스폰서 데이터 생성
17+
useEffect(() => {
18+
const fetchSponsors = () => {
19+
const dummySponsors = Array(16)
20+
.fill(null)
21+
.map((_, index) => ({
22+
id: index + 1,
23+
name: `후원사 ${index + 1}`,
24+
Logo: SponsorExample,
25+
}));
26+
27+
setSponsors(dummySponsors);
28+
};
29+
30+
fetchSponsors();
31+
}, []);
32+
33+
if (!isVisible) return null;
34+
35+
return (
36+
<SponsorSection aria-label="후원사 섹션">
37+
<SponsorTitle as="h4" role="heading" aria-level={4}>
38+
후원사 목록
39+
</SponsorTitle>
40+
<SponsorGrid role="list" aria-label="후원사 목록 그리드">
41+
{sponsors.map((sponsor) => (
42+
<SponsorItem key={sponsor.id} role="listitem">
43+
<SponsorButton
44+
type="button"
45+
aria-label={`${sponsor.name} 상세 정보 보기`}
46+
>
47+
<span className="sr-only">{sponsor.name}</span>
48+
<sponsor.Logo aria-hidden="true" />
49+
</SponsorButton>
50+
</SponsorItem>
51+
))}
52+
</SponsorGrid>
53+
</SponsorSection>
54+
);
55+
}
56+
57+
const SponsorSection = styled.section`
58+
width: 1067px;
59+
margin: 0 auto;
60+
margin-bottom: 140px;
61+
`;
62+
63+
const SponsorTitle = styled.h4`
64+
font-weight: 600;
65+
font-size: 37px;
66+
text-align: center;
67+
margin: 0;
68+
`;
69+
70+
const SponsorGrid = styled.div`
71+
margin-top: 101px;
72+
display: grid;
73+
grid-template-columns: repeat(4, 1fr);
74+
column-gap: 35px;
75+
row-gap: 75px;
76+
justify-items: center;
77+
`;
78+
79+
const SponsorItem = styled.div`
80+
width: 240px;
81+
height: 75px;
82+
`;
83+
84+
const SponsorButton = styled.button`
85+
width: 100%;
86+
height: 100%;
87+
display: flex;
88+
align-items: center;
89+
justify-content: center;
90+
background: none;
91+
border: none;
92+
padding: 0;
93+
cursor: pointer;
94+
transition: transform 0.2s ease;
95+
96+
&:focus {
97+
outline: 2px solid #007aff;
98+
outline-offset: 4px;
99+
border-radius: 4px;
100+
}
101+
102+
&:focus:not(:focus-visible) {
103+
outline: none;
104+
}
105+
106+
&:focus-visible {
107+
outline: 2px solid #007aff;
108+
outline-offset: 4px;
109+
border-radius: 4px;
110+
}
111+
112+
&:hover {
113+
// transform: scale(1.05);
114+
}
115+
116+
.sr-only {
117+
position: absolute;
118+
width: 1px;
119+
height: 1px;
120+
padding: 0;
121+
margin: -1px;
122+
overflow: hidden;
123+
clip: rect(0, 0, 0, 0);
124+
white-space: nowrap;
125+
border: 0;
126+
}
127+
`;

apps/pyconkr/src/components/layout/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Outlet } from "react-router-dom";
33

44
import Footer from "./Footer";
55
import Header from "./Header";
6+
import Sponsor from "./Sponsor";
67

78
export default function MainLayout() {
89
return (
@@ -11,6 +12,7 @@ export default function MainLayout() {
1112
<MainContent>
1213
<Outlet />
1314
</MainContent>
15+
<Sponsor />
1416
<Footer />
1517
</LayoutContainer>
1618
);

apps/pyconkr/src/components/pages/test.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,32 @@ const TabList: { [key in SelectedTabType]: React.ReactNode } = {
2727
};
2828

2929
export const Test: React.FC = () => {
30-
const [selectedTab, setSelectedTab] = React.useState<SelectedTabType>(getTabFromLocalStorage());
31-
const selectTab = (tab: SelectedTabType) => setSelectedTab(setTabToLocalStorage(tab));
30+
const [selectedTab, setSelectedTab] = React.useState<SelectedTabType>(
31+
getTabFromLocalStorage()
32+
);
33+
const selectTab = (tab: SelectedTabType) =>
34+
setSelectedTab(setTabToLocalStorage(tab));
3235
const TabButton: React.FC<{ tab: SelectedTabType }> = ({ tab }) => (
33-
<Button variant={selectedTab === tab ? "contained" : "outlined"} onClick={() => selectTab(tab)}>
36+
<Button
37+
variant={selectedTab === tab ? "contained" : "outlined"}
38+
onClick={() => selectTab(tab)}
39+
>
3440
{tab} Test
3541
</Button>
3642
);
3743

3844
return (
45+
<<<<<<< HEAD
3946
<Stack sx={{ width: "100%", height: "100%", minHeight: "100%", flexGrow: 1, py: 2 }} spacing={2}>
4047
<Stack direction="row" spacing={2} sx={{ width: "100%", justifyContent: "center" }}>
48+
=======
49+
<Stack>
50+
<Stack
51+
direction="row"
52+
spacing={2}
53+
sx={{ width: "100%", justifyContent: "center" }}
54+
>
55+
>>>>>>> origin/feature/sponsor
4156
{Object.keys(TabList).map((tab) => (
4257
<TabButton key={tab} tab={tab as SelectedTabType} />
4358
))}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { createContext, useContext, useState, ReactNode } from "react";
2+
3+
interface SponsorContextType {
4+
isVisible: boolean;
5+
setIsVisible: (value: boolean) => void;
6+
}
7+
8+
const SponsorContext = createContext<SponsorContextType | undefined>(undefined);
9+
10+
interface SponsorProviderProps {
11+
children: ReactNode;
12+
initialVisibility?: boolean;
13+
}
14+
15+
export function SponsorProvider({
16+
children,
17+
initialVisibility = false,
18+
}: SponsorProviderProps) {
19+
const [isVisible, setIsVisible] = useState(initialVisibility);
20+
21+
return (
22+
<SponsorContext.Provider value={{ isVisible, setIsVisible }}>
23+
{children}
24+
</SponsorContext.Provider>
25+
);
26+
}
27+
28+
export function useSponsor() {
29+
const context = useContext(SponsorContext);
30+
if (context === undefined) {
31+
throw new Error("useSponsor must be used within a SponsorProvider");
32+
}
33+
return context;
34+
}

0 commit comments

Comments
 (0)