Skip to content

Commit 48cd84b

Browse files
committed
♻️ refactor : Optimizing the navbar Component SSR
- Optimize navbar component to SSR and split CSR functionality into container component - Optimize SSR and CSR to match user information - Modify interface due to API response value changes - Create a hook to avoid a cascade of API requests Related issue: YJU-OKURA#128
1 parent 6bedc95 commit 48cd84b

File tree

9 files changed

+264
-244
lines changed

9 files changed

+264
-244
lines changed

src/app/classes/layout.tsx

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import {Navbar} from '@/src/components/navbar';
22

3-
export default function ClassesLayout({
4-
children, // will be a page or nested layout
5-
}: {
6-
children: React.ReactNode;
7-
}) {
3+
const ClassesLayout = ({children}: {children: React.ReactNode}) => {
84
return (
95
<section className="flex w-full h-screen">
106
<Navbar />
117
<div className="w-full overflow-x-auto overflow-y-auto">{children}</div>
128
</section>
139
);
14-
}
10+
};
11+
12+
export default ClassesLayout;

src/components/navbar/Navbar.tsx

+31-88
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,53 @@
1-
'use client';
2-
import {useState} from 'react';
31
import Image from 'next/image';
42
import Link from 'next/link';
5-
import {useParams} from 'next/navigation';
6-
import {MaterialContainer, MaterialForm} from './material';
73
import Profile from './profile';
4+
import {MaterialContainer} from './material';
85
import icons from '@/public/svgs/navbar';
96
import '@/src/styles/variable.css';
10-
import classUserState from '@/src/recoil/atoms/classUserState';
11-
import {useRecoilValue} from 'recoil';
12-
import ROLES from '@/src/constants/roles';
137

148
const Navbar = () => {
15-
const [isOpen, setIsOpen] = useState<boolean>(false);
16-
const classUser = useRecoilValue(classUserState);
17-
189
const pages = [
19-
{name: '클래스', icon: icons.group},
20-
{name: '마이페이지', icon: icons.mypage},
21-
{name: '문제은행', icon: icons.mypage},
10+
{name: '클래스', icon: icons.group, url: '/classes'},
11+
{name: '내정보', icon: icons.myPage, url: '/info'},
12+
{name: '문제은행', icon: icons.myPage, url: '/bank'},
2213
/* Billing Page - 保留 */
2314
];
2415

25-
const params = useParams<{className: string; materialName: string}>();
26-
// const searchParams = useSearchParams();
27-
// const search = searchParams.get('id');
28-
const search = '4';
29-
3016
return (
3117
<div className="w-72 h-full bg-gray-50">
32-
<div className="relative w-72 px-6 pt-5 navbar flex flex-col">
18+
<div className="relative w-72 p-6 h-full flex flex-col">
3319
{/* Profile */}
34-
<Profile params={params} cId={search} />
35-
<div className="pt-2 border-b border-gray-300"></div>
20+
<Profile />
3621
<div className="h-8"></div>
37-
3822
{/* Pages */}
3923
<div className="w-full">
40-
<div className="text-zinc-400 mb-4">페이지</div>
41-
<ul className="w-full">
42-
{pages.map((page, index) => {
43-
return (
44-
<li className="w-full mb-3 py-1" key={index}>
45-
<Link href="/" className=" flex">
46-
<Image
47-
src={page.icon}
48-
alt="icon"
49-
width={30}
50-
height={30}
51-
className="w-8 h-8 mr-3"
52-
></Image>
53-
{page.name}
54-
</Link>
55-
</li>
56-
);
57-
})}
58-
</ul>
24+
<div>
25+
<p className="text-zinc-400 mb-4">페이지</p>
26+
<ul className="w-full">
27+
{pages.map((page, index) => {
28+
return (
29+
<li className="w-full mb-2 py-1" key={index}>
30+
<Link href={page.url} className="flex">
31+
<Image
32+
src={page.icon}
33+
alt="icon"
34+
width={30}
35+
height={30}
36+
className="w-8 h-8 mr-3"
37+
></Image>
38+
{page.name}
39+
</Link>
40+
</li>
41+
);
42+
})}
43+
</ul>
44+
</div>
5945
</div>
6046
<div className="h-8"></div>
61-
62-
{/* Subject*/}
63-
{search ? (
64-
<>
65-
<div className="w-full flex-1">
66-
<div className="flex justify-between items-center mb-4">
67-
<div className="text-zinc-400">자료</div>
68-
{/* <MaterialForm /> */}
69-
{classUser && ROLES[classUser?.role_id] === 'ADMIN' ? (
70-
<div
71-
onClick={() => setIsOpen(true)}
72-
className="bg-blue-500 w-6 h-6 flex justify-center items-center rounded-lg "
73-
>
74-
<Image
75-
src={icons.plus}
76-
width={0}
77-
height={0}
78-
alt="plus"
79-
className="m-auto w-auto h-auto max-w-5 max-h-5"
80-
/>
81-
</div>
82-
) : null}
83-
{isOpen && search ? (
84-
<MaterialForm setIsOpen={setIsOpen} cId={search} />
85-
) : null}
86-
</div>
87-
<MaterialContainer params={params} cId={search} />
88-
</div>
89-
<div className="h-16"></div>
90-
91-
{/* class exit */}
92-
<div className="flex py-2">
93-
<Image
94-
src={icons.door}
95-
alt="icon"
96-
width={30}
97-
height={30}
98-
className="w-6 h-6 mr-2"
99-
></Image>
100-
{params.materialName ? (
101-
<Link href={`/classes/${search}`}>프롬프트창 떠나기</Link>
102-
) : (
103-
<Link href="/classes">클래스 떠나기</Link>
104-
)}
105-
</div>
106-
</>
107-
) : null}
47+
{/* Material*/}
48+
<div className="flex-grow">
49+
<MaterialContainer />
50+
</div>
10851
</div>
10952
</div>
11053
);

src/components/navbar/material/MaterialContainer.tsx

+85-57
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
1-
import {ChangeEvent, useState, KeyboardEvent} from 'react';
1+
'use client';
2+
import {ChangeEvent, useState, useEffect} from 'react';
23
import InfiniteScroll from 'react-infinite-scroller';
4+
import Link from 'next/link';
35
import Image from 'next/image';
6+
import {useParams} from 'next/navigation';
7+
import MaterialForm from './MaterialForm';
48
import MaterialList from './MaterialList';
59
import getMaterial from '@/src/api/material/getMaterial';
610
import searchMaterial from '@/src/api/material/searchMaterial';
7-
import {Material, ParamsProps} from '@/src/interfaces/navbar';
11+
import useDebounce from '@/src/hooks/useDebounce';
12+
import {Material} from '@/src/interfaces/navbar';
813
import icons from '@/public/svgs/navbar';
914

10-
const MaterialContainer = ({
11-
params,
12-
cId,
13-
}: {
14-
params: ParamsProps;
15-
cId: string | null;
16-
}) => {
15+
const MaterialContainer = () => {
1716
const [materials, setMaterials] = useState<Material[]>([]);
1817
const [searchMaterials, setSearchMaterials] = useState<Material[]>([]);
1918
const [keyWord, setKeyWord] = useState<string>('');
2019
const [boardPage, setBoardPage] = useState<number>(1);
2120
const [hasMore, setHasMore] = useState<boolean>(true);
21+
const [isOpen, setIsOpen] = useState<boolean>(false);
22+
const param = useParams<{cId: string; mId: string}>();
23+
24+
const debounceVal = useDebounce(keyWord, 500);
25+
26+
useEffect(() => {
27+
if (debounceVal) {
28+
searchMaterial(parseInt(param.cId), debounceVal, 1, 5).then(res => {
29+
setSearchMaterials(res);
30+
});
31+
}
32+
}, [debounceVal]);
2233

2334
const onLoadMore = () => {
2435
setHasMore(false);
25-
getMaterial(4, boardPage, 8).then(res => {
36+
getMaterial(parseInt(param.cId), boardPage, 8).then(res => {
2637
if (res.length === 0) {
2738
setHasMore(false);
2839
} else {
@@ -46,56 +57,73 @@ const MaterialContainer = ({
4657
}
4758
};
4859

49-
const handleKeyEnter = (e: KeyboardEvent<HTMLInputElement>) => {
50-
if (e.key === 'Enter')
51-
searchMaterial(1, keyWord, 1, 5).then(res => {
52-
console.log(res);
53-
setSearchMaterials(res);
54-
});
55-
};
56-
5760
return (
58-
<div className="">
59-
{/* prompt - search */}
60-
<div className="w-full flex bg-white items-center mb-3 px-1">
61-
<Image
62-
src={icons.search}
63-
alt="icon"
64-
width={20}
65-
height={20}
66-
className="w-5 h-5 opacity-50"
67-
/>
68-
<input
69-
type="text"
70-
className="w-full p-1 border-0 outline-none"
71-
placeholder="검색"
72-
onChange={handleInputText}
73-
onKeyDown={handleKeyEnter}
74-
/>
75-
</div>
76-
{/* Prompt - list */}
77-
<div className="h-[350px] overflow-auto">
78-
<InfiniteScroll
79-
pageStart={0}
80-
loadMore={onLoadMore}
81-
hasMore={hasMore}
82-
loader={<div key="unique"> loading...</div>}
83-
useWindow={false}
84-
threshold={20}
85-
>
86-
{materials ? (
87-
keyWord ? (
88-
<MaterialList
89-
materials={searchMaterials}
90-
params={params}
91-
cId={cId}
61+
<div className="h-full flex flex-col">
62+
{param.cId && (
63+
<>
64+
<div className="w-full flex-1">
65+
<div className="w-full flex justify-between items-center mb-4">
66+
<p className="text-zinc-400">자료</p>
67+
<div
68+
className="text-end bg-blue-500 w-6 h-6 flex justify-center items-center rounded-lg"
69+
onClick={() => setIsOpen(true)}
70+
>
71+
<Image src={icons.plus} width={30} height={30} alt="plus" />
72+
</div>
73+
</div>
74+
{isOpen && <MaterialForm setIsOpen={setIsOpen} cId={param.cId} />}
75+
{/* prompt - search */}
76+
<div className="w-full flex bg-white items-center mb-3 px-1">
77+
<Image
78+
src={icons.search}
79+
alt="icon"
80+
width={20}
81+
height={20}
82+
className="w-5 h-5 opacity-50"
83+
/>
84+
<input
85+
type="text"
86+
className="w-full p-1 border-0 outline-none"
87+
placeholder="Search"
88+
onChange={handleInputText}
9289
/>
90+
</div>
91+
{/* Prompt - list */}
92+
<div className="h-[calc(100%-85px)] overflow-auto">
93+
<InfiniteScroll
94+
pageStart={0}
95+
loadMore={onLoadMore}
96+
hasMore={hasMore}
97+
loader={<div key="unique"> loading...</div>}
98+
useWindow={false}
99+
threshold={20}
100+
>
101+
{materials && keyWord ? (
102+
<MaterialList materials={searchMaterials} cId={param.cId} />
103+
) : (
104+
<MaterialList materials={materials} cId={param.cId} />
105+
)}
106+
</InfiniteScroll>
107+
</div>
108+
</div>
109+
<div className="flex-none h-16"></div>
110+
{/* Exit */}
111+
<div className="flex flex-none h-[50px]">
112+
<Image
113+
src={icons.door}
114+
alt="icon"
115+
width={30}
116+
height={30}
117+
className="w-6 h-6 mr-2"
118+
></Image>
119+
{param.mId ? (
120+
<Link href={`/classes/${param.cId}`}>프롬프트 떠나기</Link>
93121
) : (
94-
<MaterialList materials={materials} params={params} cId={cId} />
95-
)
96-
) : null}
97-
</InfiniteScroll>
98-
</div>
122+
<Link href="/classes">클래스 떠나기</Link>
123+
)}
124+
</div>
125+
</>
126+
)}
99127
</div>
100128
);
101129
};

0 commit comments

Comments
 (0)