Skip to content

Commit de67fa6

Browse files
jihun36661jiwoo27
andauthored
[Feat] 예적금 검색페이지 구현 (#31)
* feat: search page 구성 * feat: 폴더 및 파일이름 변경 * feat: 누락된 변경사항(index) 변경 * feat: 홈 페이지와 연결 * feat: css파일 수정 및 수정사항 반영 * feat: 수정요청 반영(에러처리, 로딩중 검색) * feat: 정렬 로직 수정, 기본값 상수화, 검색 로직 개선, 리스트 key 수정 * feat: error 처리 부분 수정 * feat: useEffect 정리 및 검색버튼 disabled 처리 추가 * feat: UI 코드 중복 제거 * feat: loading 관련부분 수정 * feat: 에러처리 추가 * feat: 에러처리 수정 * feat: 적금 로직 오류 수정 및 버튼 비활성화 누락 수정 * feat: savings 파일에 catch 추가 * feat: 로딩중 로직 추가 * feat: 버튼 텍스트 사이즈 수정 * chore: pointer 추가 * chore: 주석 삭제 * feat: bank list 분리 * feat: 금액 안 잘리도록 수정 * chore: 띄어쓰기 * feat: css 수정 * feat: 상품 정렬 기준 변경 * feat: 로딩 중일 떄 스피너 추가 * feat: zustand 패키지 설치 * feat: 검색 조건 유지되도록 수정 * fix: 옵셔널 체이닝 사용 --------- Co-authored-by: 1jiwoo27 <[email protected]>
1 parent c1b1410 commit de67fa6

File tree

23 files changed

+851
-23
lines changed

23 files changed

+851
-23
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"axios": "^1.12.2",
1919
"react": "^19.1.1",
2020
"react-dom": "^19.1.1",
21-
"react-router-dom": "^7.9.4"
21+
"react-router-dom": "^7.9.4",
22+
"zustand": "^5.0.9"
2223
},
2324
"devDependencies": {
2425
"@eslint/js": "^9.37.0",

pnpm-lock.yaml

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { vars } from '../../styles/theme.css';
2+
import { style, globalStyle } from '@vanilla-extract/css';
3+
4+
export const mainContainer = style({
5+
display: 'block',
6+
width: '70rem',
7+
minHeight: '100vh',
8+
marginLeft: 'auto',
9+
marginRight: 'auto',
10+
paddingBottom: '4rem',
11+
boxSizing: 'border-box',
12+
paddingTop: '10rem',
13+
});
14+
15+
export const section = style({
16+
display: 'flex',
17+
flexDirection: 'column',
18+
gap: '0.2rem',
19+
marginBottom: '3rem',
20+
});
21+
22+
export const sectionTitle = style({
23+
fontSize: vars.size.lg,
24+
fontWeight: vars.weight.semibold,
25+
color: vars.color.black,
26+
marginBottom: '1rem',
27+
});
28+
29+
export const bankContainer = style({
30+
display: 'flex',
31+
flexDirection: 'column',
32+
width: '70rem',
33+
gap: '1.5rem',
34+
borderRadius: '10px',
35+
});
36+
37+
export const bankGrid = style({
38+
display: 'grid',
39+
gridTemplateColumns: 'repeat(6, 1fr)',
40+
gap: '1rem',
41+
justifyContent: 'center',
42+
alignContent: 'center',
43+
});
44+
45+
export const bankButton = style({
46+
width: '100%',
47+
aspectRatio: '1 / 1',
48+
display: 'flex',
49+
flexDirection: 'column',
50+
alignItems: 'center',
51+
justifyContent: 'center',
52+
padding: '0.5rem',
53+
gap: '0.2rem',
54+
border: `1px solid ${vars.color.gray500}`,
55+
borderRadius: '5px',
56+
cursor: 'pointer',
57+
});
58+
59+
export const bankLogo = style({
60+
width: '5rem',
61+
height: '5rem',
62+
marginBottom: '0.5rem',
63+
display: 'flex',
64+
justifyContent: 'center',
65+
alignItems: 'center',
66+
});
67+
68+
export const bankName = style({
69+
fontSize: vars.size.xxs,
70+
fontWeight: vars.weight.medium,
71+
color: vars.color.gray800,
72+
});
73+
74+
export const bankButtonSelected = style({
75+
border: `2px solid ${vars.color.pink300}`,
76+
backgroundColor: vars.color.pink100,
77+
});
78+
79+
export const selectAllContainer = style({
80+
display: 'flex',
81+
alignItems: 'center',
82+
gap: '0.5rem',
83+
marginBottom: '1rem',
84+
justifyContent: 'flex-end',
85+
});
86+
87+
export const selectAllButton = style({
88+
width: '11rem',
89+
borderRadius: '5px',
90+
cursor: 'pointer',
91+
backgroundColor: vars.color.pink300,
92+
fontSize: vars.size.sm,
93+
fontWeight: vars.weight.semibold,
94+
padding: '0.6rem 1rem',
95+
border: 'none',
96+
});
97+
98+
export const termContainer = style({
99+
display: 'flex',
100+
gap: '1rem',
101+
width: '70rem',
102+
alignItems: 'center',
103+
justifyContent: 'center',
104+
});
105+
106+
export const searchContainer = style({
107+
display: 'flex',
108+
alignItems: 'center',
109+
justifyContent: 'flex-end',
110+
paddingBottom: '2rem',
111+
});
112+
113+
export const searchButton = style({
114+
backgroundColor: vars.color.pink300,
115+
color: vars.color.white,
116+
fontSize: vars.size.sm,
117+
fontWeight: vars.weight.semibold,
118+
padding: '0.6rem 1rem',
119+
border: 'none',
120+
borderRadius: '5px',
121+
cursor: 'pointer',
122+
width: '11rem',
123+
});
124+
125+
export const depositListContainer = style({
126+
display: 'flex',
127+
flexDirection: 'column',
128+
gap: '1rem',
129+
marginTop: '2rem',
130+
});
131+
132+
globalStyle(`${bankLogo} svg`, {
133+
width: '100%',
134+
height: '100%',
135+
});
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { useState, useEffect } from 'react';
2+
import { getDepositsList } from '../../shared/api/products';
3+
import { useDepositSearchStore } from '../../shared/stores/useDepositSearchStore';
4+
import { button } from '../../shared/components/button/button.css';
5+
import { BANK_LIST } from '../../shared/constants/bank-list';
6+
import Header from '../../shared/components/header/header';
7+
import DepositBasic from '../../shared/components/deposit-basic/deposit-basic';
8+
import DropDown from '../../shared/components/dropdown/dropdown';
9+
import Spinner from '../../shared/components/spinner/spinner';
10+
import * as styles from './deposit-search-page.css';
11+
12+
const DepositSearchPage = () => {
13+
const {
14+
selectedBanks,
15+
saveTerm,
16+
depositList,
17+
initialized,
18+
setSelectedBanks,
19+
setSaveTerm,
20+
setDepositList,
21+
setInitialized,
22+
} = useDepositSearchStore();
23+
const [loading, setIsLoading] = useState(true);
24+
const [isError, setIsError] = useState(false);
25+
26+
const fetchDepositList = async (banks: string[], term: number) => {
27+
setIsLoading(true);
28+
setIsError(false);
29+
try {
30+
const res = await getDepositsList(banks, term);
31+
const data = [...(res?.result ?? [])].sort((a, b) => {
32+
if (a.saveTerm !== b.saveTerm) return b.saveTerm - a.saveTerm;
33+
if (a.maxRate !== b.maxRate) return b.maxRate - a.maxRate;
34+
return b.baseRate - a.baseRate;
35+
});
36+
37+
setDepositList(data);
38+
} catch (error) {
39+
setDepositList([]);
40+
setIsError(true);
41+
} finally {
42+
setIsLoading(false);
43+
}
44+
};
45+
46+
useEffect(() => {
47+
if (!initialized) {
48+
const allBankIds = BANK_LIST.map((b) => b.id);
49+
setSelectedBanks(allBankIds);
50+
fetchDepositList(allBankIds, saveTerm);
51+
setInitialized(true);
52+
} else {
53+
setIsLoading(false);
54+
}
55+
}, []);
56+
57+
const handleBankToggle = (bankId: string) => {
58+
setSelectedBanks((prev) => {
59+
if (prev.includes(bankId)) {
60+
return prev.filter((id) => id !== bankId);
61+
} else {
62+
return [...prev, bankId];
63+
}
64+
});
65+
};
66+
67+
const handleTermChange = (term: number) => {
68+
setSaveTerm(term);
69+
};
70+
71+
const handleSearch = () => {
72+
if (loading) return;
73+
const targetBanks = selectedBanks.length > 0 ? selectedBanks : BANK_LIST.map((b) => b.id);
74+
if (selectedBanks.length === 0) {
75+
setSelectedBanks(targetBanks);
76+
}
77+
fetchDepositList(targetBanks, saveTerm);
78+
};
79+
80+
const handleSelectAll = () => {
81+
if (selectedBanks.length === BANK_LIST.length) {
82+
setSelectedBanks([]);
83+
} else {
84+
setSelectedBanks(BANK_LIST.map((b) => b.id));
85+
}
86+
};
87+
88+
return (
89+
<>
90+
<Header />
91+
<main className={styles.mainContainer}>
92+
{/*은행 선택*/}
93+
<section className={styles.section}>
94+
<h2 className={styles.sectionTitle}>은행</h2>
95+
<div className={styles.bankContainer}>
96+
<div className={styles.bankGrid}>
97+
{BANK_LIST.map((bank) => {
98+
const isSelected = selectedBanks.includes(bank.id);
99+
return (
100+
<button
101+
key={bank.id}
102+
className={`${styles.bankButton} ${isSelected ? styles.bankButtonSelected : ''}`}
103+
onClick={() => handleBankToggle(bank.id)}
104+
>
105+
<div className={styles.bankLogo}>
106+
<bank.logo />
107+
</div>
108+
<span className={styles.bankName}>{bank.name}</span>
109+
</button>
110+
);
111+
})}
112+
</div>
113+
<div className={styles.selectAllContainer}>
114+
<button
115+
className={`${styles.selectAllButton} ${button({ variant: 'pink' })}`}
116+
onClick={() => handleSelectAll()}
117+
>
118+
전체선택
119+
</button>
120+
</div>
121+
</div>
122+
</section>
123+
{/*기간 선택*/}
124+
<section className={styles.section}>
125+
<h2 className={styles.sectionTitle}>기간</h2>
126+
<div className={styles.termContainer}>
127+
<DropDown color='pink' value={saveTerm} onChange={handleTermChange} />
128+
</div>
129+
</section>
130+
{/*검색 버튼*/}
131+
<div className={styles.searchContainer}>
132+
<button
133+
className={`${styles.searchButton} ${button({ variant: 'pink' })}`}
134+
onClick={handleSearch}
135+
disabled={loading}
136+
>
137+
검색
138+
</button>
139+
</div>
140+
{/*상품 리스트*/}
141+
{loading ? (
142+
<div className={styles.depositListContainer}>
143+
<Spinner color='pink' />
144+
</div>
145+
) : !isError ? (
146+
<div className={styles.depositListContainer}>
147+
{depositList.length > 0 ? (
148+
depositList.map((item) => (
149+
<DepositBasic
150+
key={`${item.productId}-${item.optionId}`}
151+
productId={item.productId}
152+
optionId={item.optionId}
153+
bankName={item.bankName}
154+
productName={item.productName}
155+
saveTerm={item.saveTerm}
156+
baseRate={item.baseRate}
157+
maxRate={item.maxRate}
158+
/>
159+
))
160+
) : (
161+
<p>검색된 예금 상품이 없습니다.</p>
162+
)}
163+
</div>
164+
) : (
165+
<p>예금 상품을 불러오는 중 오류가 발생했습니다. 다시 시도해주세요.</p>
166+
)}
167+
</main>
168+
</>
169+
);
170+
};
171+
172+
export default DepositSearchPage;

0 commit comments

Comments
 (0)