Skip to content

Commit feda477

Browse files
authored
[시설물 정보] 백엔드 API 응답값 추가에 따른 코드 변경 (#1240)
1 parent 702c988 commit feda477

4 files changed

Lines changed: 51 additions & 213 deletions

File tree

src/api/coopshop/entity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ export interface CoopShopDetailResponse extends APIResponse {
2424
phone: string | null;
2525
location: string;
2626
remarks: string | null;
27+
icon_url: string | null;
2728
updated_at: string; // yyyy-MM-dd
2829
}

src/components/CampusInfo/CampusInfo.module.scss

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,34 @@
8686
}
8787

8888
.icon-wrapper {
89+
align-items: center;
8990
border-radius: 8px;
9091
background: #f1f8ff;
92+
display: flex;
93+
flex-shrink: 0;
94+
justify-content: center;
9195
padding: 12px;
9296
width: 32px;
9397
height: 32px;
9498
margin: 0;
9599
}
96100

101+
.icon-image {
102+
display: block;
103+
height: 100%;
104+
object-fit: contain;
105+
width: 100%;
106+
}
107+
108+
.icon-fallback {
109+
color: #072552;
110+
font-family: Pretendard, sans-serif;
111+
font-size: 18px;
112+
font-style: normal;
113+
font-weight: 700;
114+
line-height: 1;
115+
}
116+
97117
.info-title-container {
98118
display: flex;
99119
gap: 16px;

src/components/CampusInfo/index.tsx

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,13 @@
11
import { cn } from '@bcsdlab/utils';
22
import { useSuspenseQuery } from '@tanstack/react-query';
33
import { coopshopQueries } from 'api/coopshop/queries';
4-
import Book from './svg/book.svg';
5-
import Cafe from './svg/cafe.svg';
6-
import Cut from './svg/cut.svg';
7-
import Flatware from './svg/flatware.svg';
8-
import Glasses from './svg/glasses.svg';
9-
import Laundry from './svg/laundry.svg';
10-
import PostOffice from './svg/post-office.svg';
11-
import Print from './svg/print.svg';
124
import styles from './CampusInfo.module.scss';
135

146
const CAFETERIA_HEAD_TABLE = {
157
row: ['평일', '주말'],
168
col: ['아침', '점심', '저녁'],
179
};
1810

19-
const SHOP_ICON = {
20-
서점: <Book />,
21-
대즐: <Cafe />,
22-
미용실: <Cut />,
23-
세탁소: <Laundry />,
24-
우체국: <PostOffice />,
25-
'복지관 참빛관 편의점': <Cafe />,
26-
복사실: <Print />,
27-
학생식당: <Flatware />,
28-
복지관식당: <Flatware />,
29-
오락실: <Cafe />,
30-
안경원: <Glasses />,
31-
우편취급국: <PostOffice />,
32-
};
33-
3411
const formatDateRange = (fromDate: string, toDate: string) => {
3512
const from = new Date(fromDate);
3613
const to = new Date(toDate);
@@ -56,6 +33,27 @@ const formatDateRange = (fromDate: string, toDate: string) => {
5633
return `기간 : ${fromFormatted} - ${toFormatted}`;
5734
};
5835

36+
type ShopIconProps = {
37+
iconUrl: string | null | undefined;
38+
name: string;
39+
};
40+
41+
function ShopIcon({ iconUrl, name }: ShopIconProps) {
42+
return (
43+
<div className={styles['icon-wrapper']}>
44+
{iconUrl ? (
45+
// NOTE: 백엔드가 내려주는 소형 반복 아이콘은 호스트가 고정되지 않을 수 있어 <img>를 유지합니다.
46+
// eslint-disable-next-line @next/next/no-img-element
47+
<img className={styles['icon-image']} src={iconUrl} alt={name} decoding="async" />
48+
) : (
49+
<span className={styles['icon-fallback']} aria-hidden="true">
50+
{name.slice(0, 1)}
51+
</span>
52+
)}
53+
</div>
54+
);
55+
}
56+
5957
function CampusInfo() {
6058
const { data: campusInfo } = useSuspenseQuery(coopshopQueries.allShopInfo());
6159

@@ -87,10 +85,8 @@ function CampusInfo() {
8785
<div className={styles['info-block']}>
8886
<div className={styles['info-cafeteria']}>
8987
<div className={styles['info-title-container']}>
90-
<div className={styles['icon-wrapper']}>
91-
<Flatware />
92-
</div>
93-
<div className={styles['info-title']}>학생식당</div>
88+
<ShopIcon iconUrl={cafeteriaInfo?.icon_url} name={cafeteriaInfo?.name ?? '학생식당'} />
89+
<div className={styles['info-title']}>{cafeteriaInfo?.name ?? '학생식당'}</div>
9490
</div>
9591
<table className={styles.table}>
9692
<thead>
@@ -125,9 +121,9 @@ function CampusInfo() {
125121
</div>
126122
</div>
127123

128-
{filteredCampusInfo.slice(0, 2).map(({ id, name, opens }) => (
124+
{filteredCampusInfo.slice(0, 2).map(({ id, name, opens, icon_url }) => (
129125
<div className={styles['info-block']} key={id}>
130-
<div className={styles['icon-wrapper']}>{SHOP_ICON[name as keyof typeof SHOP_ICON]}</div>
126+
<ShopIcon iconUrl={icon_url} name={name} />
131127
<div className={styles['info-description-container']}>
132128
<div className={styles['info-title']}>{name}</div>
133129
{opens.map(({ day_of_week, open_time, close_time }) => (
@@ -141,9 +137,9 @@ function CampusInfo() {
141137
))}
142138
</div>
143139
<div className={styles['info-column']}>
144-
{filteredCampusInfo.slice(2, 6).map(({ id, name, opens }) => (
140+
{filteredCampusInfo.slice(2, 6).map(({ id, name, opens, icon_url }) => (
145141
<div className={styles['info-block']} key={id}>
146-
<div className={styles['icon-wrapper']}>{SHOP_ICON[name as keyof typeof SHOP_ICON]}</div>
142+
<ShopIcon iconUrl={icon_url} name={name} />
147143
<div className={styles['info-description-container']}>
148144
<div className={styles['info-title']}>{name}</div>
149145
{opens.map(({ day_of_week, open_time, close_time }) => (
@@ -157,9 +153,9 @@ function CampusInfo() {
157153
))}
158154
</div>
159155
<div className={styles['info-column']}>
160-
{filteredCampusInfo.slice(6).map(({ id, name, opens }) => (
156+
{filteredCampusInfo.slice(6).map(({ id, name, opens, icon_url }) => (
161157
<div className={styles['info-block']} key={id}>
162-
<div className={styles['icon-wrapper']}>{SHOP_ICON[name as keyof typeof SHOP_ICON]}</div>
158+
<ShopIcon iconUrl={icon_url} name={name} />
163159
<div className={styles['info-description-container']}>
164160
<div className={styles['info-title']}>{name}</div>
165161
{opens.map(({ day_of_week, open_time, close_time }) => (

src/pages/campusinfo/index.tsx

Lines changed: 1 addition & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -1,180 +1 @@
1-
import { cn } from '@bcsdlab/utils';
2-
import { useSuspenseQuery } from '@tanstack/react-query';
3-
import { coopshopQueries } from 'api/coopshop/queries';
4-
import Book from 'components/CampusInfo/svg/book.svg';
5-
import Cafe from 'components/CampusInfo/svg/cafe.svg';
6-
import Cut from 'components/CampusInfo/svg/cut.svg';
7-
import Flatware from 'components/CampusInfo/svg/flatware.svg';
8-
import Glasses from 'components/CampusInfo/svg/glasses.svg';
9-
import Laundry from 'components/CampusInfo/svg/laundry.svg';
10-
import PostOffice from 'components/CampusInfo/svg/post-office.svg';
11-
import Print from 'components/CampusInfo/svg/print.svg';
12-
import styles from './CampusInfo.module.scss';
13-
14-
const CAFETERIA_HEAD_TABLE = {
15-
row: ['평일', '주말'],
16-
col: ['아침', '점심', '저녁'],
17-
};
18-
19-
const SHOP_ICON = {
20-
서점: <Book />,
21-
대즐: <Cafe />,
22-
미용실: <Cut />,
23-
세탁소: <Laundry />,
24-
우체국: <PostOffice />,
25-
'복지관 참빛관 편의점': <Cafe />,
26-
복사실: <Print />,
27-
학생식당: <Flatware />,
28-
복지관식당: <Flatware />,
29-
오락실: <Cafe />,
30-
안경원: <Glasses />,
31-
우편취급국: <PostOffice />,
32-
};
33-
34-
const formatDateRange = (fromDate: string, toDate: string) => {
35-
const from = new Date(fromDate);
36-
const to = new Date(toDate);
37-
38-
const options: Intl.DateTimeFormatOptions = {
39-
year: 'numeric',
40-
month: 'long',
41-
day: 'numeric',
42-
};
43-
44-
const fromFormatted = from.toLocaleDateString('ko-KR', options);
45-
let toFormatted = to.toLocaleDateString('ko-KR', options);
46-
47-
if (from.getFullYear() === to.getFullYear()) {
48-
const toOptions: Intl.DateTimeFormatOptions = {
49-
month: 'long',
50-
day: 'numeric',
51-
};
52-
53-
toFormatted = to.toLocaleDateString('ko-KR', toOptions);
54-
}
55-
56-
return `기간 : ${fromFormatted} - ${toFormatted}`;
57-
};
58-
59-
function CampusInfo() {
60-
const { data: campusInfo } = useSuspenseQuery(coopshopQueries.allShopInfo());
61-
62-
const cafeteriaInfo = campusInfo?.coop_shops.find((shop) => shop.name === '학생식당');
63-
const filteredCampusInfo = campusInfo?.coop_shops.filter((shop) => shop.name !== '학생식당');
64-
65-
const getFormattedShopTime = (open: string, close: string) => {
66-
if (open === close) {
67-
return open;
68-
}
69-
70-
return `${open} - ${close}`;
71-
};
72-
73-
const getTimeToTypeAndDay = (type: string, day: string) => {
74-
const target = cafeteriaInfo?.opens?.find((open) => open.day_of_week === day && open.type === type);
75-
76-
return target ? getFormattedShopTime(target.open_time, target.close_time) : '미운영';
77-
};
78-
79-
return (
80-
<div className={styles.container}>
81-
<div className={styles['title-container']}>
82-
<h2 className={styles.title}>{`${campusInfo.semester} 시설물 운영 시간`}</h2>
83-
<div className={styles.subtitle}>{formatDateRange(campusInfo.from_date, campusInfo.to_date)}</div>
84-
</div>
85-
<section className={styles['info-container']}>
86-
<div className={styles['info-column']}>
87-
<div className={styles['info-block']}>
88-
<div className={styles['info-cafeteria']}>
89-
<div className={styles['info-title-container']}>
90-
<div className={styles['icon-wrapper']}>
91-
<Flatware />
92-
</div>
93-
<div className={styles['info-title']}>학생식당</div>
94-
</div>
95-
<table className={styles.table}>
96-
<thead>
97-
<tr className={styles['table-head__tr']}>
98-
<th>시간</th>
99-
{CAFETERIA_HEAD_TABLE.row.map((type) => (
100-
<th className={styles['table-head__th']} key={type}>
101-
{type}
102-
</th>
103-
))}
104-
</tr>
105-
</thead>
106-
<tbody>
107-
{CAFETERIA_HEAD_TABLE.col.map((type) => (
108-
<tr className={styles['table-body__tr']} key={type}>
109-
<td className={styles['table-body__td']}>{type}</td>
110-
{CAFETERIA_HEAD_TABLE.row.map((day) => (
111-
<td
112-
className={cn({
113-
[styles['table-body__td']]: true,
114-
[styles.closed]: getTimeToTypeAndDay(type, day) === '미운영',
115-
})}
116-
key={`${type}-${day}`}
117-
>
118-
{getTimeToTypeAndDay(type, day)}
119-
</td>
120-
))}
121-
</tr>
122-
))}
123-
</tbody>
124-
</table>
125-
</div>
126-
</div>
127-
128-
{filteredCampusInfo.slice(0, 2).map(({ id, name, opens }) => (
129-
<div className={styles['info-block']} key={id}>
130-
<div className={styles['icon-wrapper']}>{SHOP_ICON[name as keyof typeof SHOP_ICON]}</div>
131-
<div className={styles['info-description-container']}>
132-
<div className={styles['info-title']}>{name}</div>
133-
{opens.map(({ day_of_week, open_time, close_time }) => (
134-
<div
135-
className={styles['info-description']}
136-
key={`${id}-${day_of_week}`}
137-
>{`${day_of_week}: ${getFormattedShopTime(open_time, close_time)}`}</div>
138-
))}
139-
</div>
140-
</div>
141-
))}
142-
</div>
143-
<div className={styles['info-column']}>
144-
{filteredCampusInfo.slice(2, 6).map(({ id, name, opens }) => (
145-
<div className={styles['info-block']} key={id}>
146-
<div className={styles['icon-wrapper']}>{SHOP_ICON[name as keyof typeof SHOP_ICON]}</div>
147-
<div className={styles['info-description-container']}>
148-
<div className={styles['info-title']}>{name}</div>
149-
{opens.map(({ day_of_week, open_time, close_time }) => (
150-
<div
151-
className={styles['info-description']}
152-
key={`${id}-${day_of_week}`}
153-
>{`${day_of_week}: ${getFormattedShopTime(open_time, close_time)}`}</div>
154-
))}
155-
</div>
156-
</div>
157-
))}
158-
</div>
159-
<div className={styles['info-column']}>
160-
{filteredCampusInfo.slice(6).map(({ id, name, opens }) => (
161-
<div className={styles['info-block']} key={id}>
162-
<div className={styles['icon-wrapper']}>{SHOP_ICON[name as keyof typeof SHOP_ICON]}</div>
163-
<div className={styles['info-description-container']}>
164-
<div className={styles['info-title']}>{name}</div>
165-
{opens.map(({ day_of_week, open_time, close_time }) => (
166-
<div
167-
className={styles['info-description']}
168-
key={`${id}-${day_of_week}`}
169-
>{`${day_of_week}: ${getFormattedShopTime(open_time, close_time)}`}</div>
170-
))}
171-
</div>
172-
</div>
173-
))}
174-
</div>
175-
</section>
176-
</div>
177-
);
178-
}
179-
180-
export default CampusInfo;
1+
export { default } from 'components/CampusInfo';

0 commit comments

Comments
 (0)