Skip to content

Commit 6256854

Browse files
authored
chore: develop to main (#304)
* feat: 유실물 게시글 생성하기 (#290) (#291) * feat: 유실물 게시글 생성하기 (#290) * fix: test 관련 코드 주석 해제 (#290) * feat: 유실물 게시글 수정 (#281) (#292) * chore: D-n 규칙에 따라 자동으로 Label 을 업데이트하는 Github Actions (#293) (#294) * feat: 토큰 재발급 (#296) (#298) * feat: 커뮤니티 페이지 글 목록 조회 (#299) * feat: community page list api 연동 (#297) * feat: 인기글 목록 조회 추가 (#297) * feat: community detail page (#300) (#301) * feat: community detail page (#300) * chore: remove console (#300) * chore: 네이밍 수정 (#300) * chore: lostFound와 community의 image 관련 리스폰스 형식 다른 점 처리 (#300) * chore: date 포맷팅 관련 수정 (#300) * feat: 커뮤니티 게시글 생성 및 수정 (#302) (#303) * feat: 커뮤니티 게시글 생성 (#302) * feat: 커뮤니티 게시글 수정 (#302) * chore: resolve conflicts (#develop)
1 parent 3abdb26 commit 6256854

File tree

68 files changed

+1739
-135
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+1739
-135
lines changed

packages/utils/src/date.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { formatDistanceToNow } from 'date-fns';
1+
import { format, formatDistanceToNow } from 'date-fns';
22
import { ko } from 'date-fns/locale';
33

44
const TIME_UNITS = {
@@ -17,27 +17,19 @@ interface FormatOptions {
1717
* 분이 0인 경우 'MM.DD HH' 형식으로 반환합니다.
1818
*/
1919
function formatToShortDate(date: Date): string {
20-
const month = date.getMonth() + 1;
21-
const day = date.getDate();
22-
const hours = date.getHours();
2320
const minutes = date.getMinutes();
24-
25-
const formattedDate = `${String(month).padStart(2, '0')}.${String(day).padStart(2, '0')}`;
26-
const formattedTime =
27-
minutes === 0
28-
? String(hours).padStart(2, '0')
29-
: `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
30-
31-
return `${formattedDate} ${formattedTime}`;
21+
return minutes === 0
22+
? format(date, 'MM월 dd일 a h시', { locale: ko })
23+
: format(date, 'MM월 dd일 a h시 mm분', { locale: ko });
3224
}
3325

3426
/**
3527
* 날짜를 상황에 맞는 형식으로 변환합니다.
3628
* - 1분 이내: '방금 전'
3729
* - 24시간 이내: 'n시간 전'
3830
* - 24시간 이후:
39-
* - format이 'short'인 경우 'MM.DD HH:mm'
40-
* - format이 'relative'인 경우 'n일 전'
31+
* - format이 'short'인 경우 'MM월 dd일 a h시 mm분'
32+
* - format이 'relative'인 경우 '약 n일 전 / 약 n개월 전 / 약 n년 전'
4133
*/
4234
export function formatDateTime(dateString: string, options: FormatOptions = {}): string {
4335
const { format = 'relative' } = options;

services/ahhachul.com/src/apis/fetcher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import axios, {
99
import { AUTH_ALERT_MSG } from '@/constants';
1010
import { authService } from '@/contexts';
1111
import type { ValueOf } from '@/types';
12-
import { TokenRefreshService, type ErrorResponse } from '@/utils';
12+
import { TokenRefreshService } from '@/utils';
1313

1414
import { BASE_URL } from './baseUrl';
1515
import { API_PREFIX } from './endpointPrefix';
@@ -90,7 +90,7 @@ class ApiClient {
9090
const errorMessage = response.data?.message;
9191

9292
if (errorMessage === AUTH_ALERT_MSG.INVALID_ACCESS_TOKEN) {
93-
return this.tokenService.handleTokenRefresh(error as AxiosError<ErrorResponse>);
93+
return this.tokenService.handleTokenRefresh(error);
9494
}
9595

9696
if (errorMessage === AUTH_ALERT_MSG.DUPLICATE_SIGNIN_DETECTED) {
Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,83 @@
1-
export {};
1+
import axiosInstance from '@/apis/fetcher';
2+
import {
3+
type ApiResponse,
4+
type PaginatedList,
5+
type CommunityListParams,
6+
type CommunityPost,
7+
CommunityType,
8+
CommunityDetail,
9+
CommentList,
10+
CommunityForm,
11+
WithPostId,
12+
CommunityEditForm,
13+
} from '@/types';
14+
import { appendFilesToFormData, createJsonBlob, extractFormData } from '@/utils';
15+
16+
export const fetchCommunityList = async (req: CommunityListParams) => {
17+
const endpoint =
18+
req.categoryType === CommunityType.HOT ? '/community-hot-posts' : '/community-posts';
19+
20+
const { data } = await axiosInstance.get<ApiResponse<PaginatedList<CommunityPost>>>(endpoint, {
21+
params: {
22+
...req,
23+
...(req.categoryType !== CommunityType.HOT && { categoryType: req.categoryType }),
24+
pageSize: 10,
25+
sort: 'createdAt,desc',
26+
},
27+
});
28+
return data;
29+
};
30+
31+
export const createCommunity = async (req: CommunityForm) => {
32+
const formData = new FormData();
33+
const formDataWithoutImages = extractFormData(req, 'images');
34+
const jsonBlob = createJsonBlob(formDataWithoutImages);
35+
36+
formData.append('content', jsonBlob);
37+
38+
if (req.images?.length) {
39+
appendFilesToFormData(formData, req.images, 'imageFiles');
40+
}
41+
42+
const { data } = await axiosInstance.post<ApiResponse<WithPostId>>('/community-posts', formData, {
43+
headers: {
44+
'Content-Type': 'multipart/form-data',
45+
},
46+
});
47+
48+
return data;
49+
};
50+
51+
export const fetchCommunityDetail = (id: number) =>
52+
axiosInstance.get<ApiResponse<CommunityDetail>>(`/community-posts/${id}`);
53+
54+
export const fetchCommunityCommentList = (id: number) =>
55+
axiosInstance.get<ApiResponse<CommentList>>(`/community-posts/${id}/comments`);
56+
57+
export const editCommunity = async (id: number, req: CommunityEditForm) => {
58+
const formData = new FormData();
59+
const formDataWithoutImages = extractFormData(req, 'images');
60+
const jsonBlob = createJsonBlob(formDataWithoutImages);
61+
62+
formData.append('content', jsonBlob);
63+
64+
if (req.images?.length) {
65+
appendFilesToFormData(
66+
formData,
67+
req.images.flatMap(image => (image.data !== null ? [image.data] : [])),
68+
'imageFiles',
69+
);
70+
}
71+
72+
const { data } = await axiosInstance.post<ApiResponse<WithPostId>>(
73+
`/community-posts/${id}`,
74+
formData,
75+
{
76+
headers: {
77+
'Content-Type': 'multipart/form-data',
78+
},
79+
},
80+
);
81+
82+
return data;
83+
};

services/ahhachul.com/src/apis/request/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './auth';
22
export * from './user';
33
export * from './token';
44
export * from './lostFound';
5+
export * from './community';

services/ahhachul.com/src/apis/request/lostFound.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,17 @@ export const createLostFound = async (req: LostFoundForm) => {
3636
appendFilesToFormData(formData, req.images);
3737
}
3838

39-
const response = await axiosInstance.post<ApiResponse<WithPostId>>('/lost-posts', formData, {
39+
const { data } = await axiosInstance.post<ApiResponse<WithPostId>>('/lost-posts', formData, {
4040
headers: {
4141
'Content-Type': 'multipart/form-data',
4242
},
4343
});
4444

45-
return response.data;
45+
return data;
4646
};
4747

4848
export const fetchLostFoundDetail = (id: number) =>
49-
axiosInstance.get<ApiResponse<LostFoundPostDetail>>(`lost-posts/${id}`);
49+
axiosInstance.get<ApiResponse<LostFoundPostDetail>>(`/lost-posts/${id}`);
5050

5151
export const fetchLostFoundCommentList = (id: number) =>
5252
axiosInstance.get<ApiResponse<CommentList>>(`/lost-posts/${id}/comments`);
@@ -59,14 +59,13 @@ export const editLostFound = async (id: number, req: LostFoundEditForm) => {
5959
formData.append('content', jsonBlob);
6060

6161
if (req.images?.length) {
62-
// fileData: images,
6362
appendFilesToFormData(
6463
formData,
6564
req.images.flatMap(image => (image.data !== null ? [image.data] : [])),
6665
);
6766
}
6867

69-
const response = await axiosInstance.post<ApiResponse<WithPostId>>(
68+
const { data } = await axiosInstance.post<ApiResponse<WithPostId>>(
7069
`/lost-posts/${id}`,
7170
formData,
7271
{
@@ -76,5 +75,5 @@ export const editLostFound = async (id: number, req: LostFoundEditForm) => {
7675
},
7776
);
7877

79-
return response.data;
78+
return data;
8079
};

services/ahhachul.com/src/apis/request/token.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ import { BASE_URL } from '../baseUrl';
66
import { API_PREFIX } from '../endpointPrefix';
77

88
export const renewAccessToken = async (refreshToken: string) => {
9-
const { data } = await axios.get<ApiResponse<AuthTokens>>(
9+
const { data } = await axios.post<ApiResponse<AuthTokens>>(
1010
`${BASE_URL.SERVER}${API_PREFIX}/auth/token/refresh`,
1111
{
12-
params: {
13-
refreshToken,
14-
},
12+
refreshToken,
1513
},
1614
);
1715

services/ahhachul.com/src/apis/request/user.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,11 @@ export const fetchUserProfile = async () => {
66

77
return data;
88
};
9+
10+
export const fetchUserFavoriteStations = async () => {
11+
const { data } = await axiosInstance.get<ApiResponse<{ id: 'hello' }>>(
12+
'/members/bookmarks/stations',
13+
);
14+
15+
return data;
16+
};

services/ahhachul.com/src/components/common/filter/drawerFilter/DrawerFilter.component.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,9 @@ const DrawerFilter: React.FC<DrawerFilterProps> = ({ label, drawerTitle }) => {
2626
onOpenChange={handleToggleDrawer}
2727
>
2828
<Drawer.Trigger asChild>
29-
<S.FilterButton>
29+
<S.FilterButton isActive={false}>
3030
<span>{label}</span>
31-
<S.ChevronIconWrapper>
32-
<ChevronIcon />
33-
</S.ChevronIconWrapper>
31+
<ChevronIcon />
3432
</S.FilterButton>
3533
</Drawer.Trigger>
3634
<Drawer.Portal>
Lines changed: 49 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,23 @@
1+
import { css } from '@emotion/react';
12
import styled from '@emotion/styled';
23
import { Drawer } from 'vaul';
34

4-
export const FilterButton = styled.button<{ isActive?: boolean }>`
5-
position: relative;
6-
text-align: left;
7-
width: calc(100% - 40px);
8-
margin: 0 auto;
9-
border: 1px solid rgba(196, 212, 252, 0.37);
10-
height: 44px;
11-
border-radius: 6px;
12-
padding: 0 12px;
13-
color: ${({ isActive }) => (isActive ? '#FFFFFF' : '#9da5b6')};
14-
font-size: 14px;
15-
16-
&[aria-invalid='true'] {
17-
border-color: #e02020;
18-
}
19-
`;
20-
21-
export const ChevronIconWrapper = styled.div`
22-
width: 14px;
23-
height: 14px;
24-
position: absolute;
25-
top: 51.5%;
26-
right: 12px;
27-
transform: translateY(-50%);
5+
export const FilterButton = styled.button<{ isActive: boolean }>`
6+
flex-shrink: 0;
7+
height: 30px;
8+
background-color: ${({ theme }) => theme.colors.gray[10]};
9+
border: 1px solid
10+
${({ theme, isActive }) => (isActive ? theme.colors.gray[100] : theme.colors.gray[20])};
11+
border-radius: 18px;
12+
display: flex;
13+
align-items: center;
14+
padding: 0 10px;
15+
transition: background-color 0.2s ease-out;
16+
gap: 2px;
2817
29-
& > svg > path {
30-
stroke: #9da5b6;
18+
& > span {
19+
${({ theme }) => theme.fonts.labelMedium};
20+
color: ${({ theme }) => theme.colors.gray[90]};
3121
}
3222
`;
3323

@@ -41,21 +31,22 @@ export const Overlay = styled(Drawer.Overlay)`
4131
`;
4232

4333
export const DrawerContent = styled(Drawer.Content)`
44-
z-index: 999999999;
34+
z-index: ${({ theme }) => theme.zIndex.drawer};
4535
display: flex;
4636
flex-direction: column;
4737
border-top-left-radius: 10px;
4838
border-top-right-radius: 10px;
49-
height: 96%;
39+
height: max-content;
5040
position: fixed;
5141
bottom: 0;
5242
left: 0;
5343
right: 0;
44+
box-shadow: 0px -10px 16px 0px rgba(0, 0, 0, 0.17);
5445
`;
5546

5647
export const ContentWrapper = styled.div`
5748
padding: 1.2rem 1rem;
58-
background-color: #222226;
49+
background-color: ${({ theme }) => theme.colors.gray[10]};
5950
border-top-left-radius: 12px;
6051
border-top-right-radius: 12px;
6152
height: 100%;
@@ -71,12 +62,13 @@ export const Header = styled.div`
7162
export const HeaderTitle = styled(Drawer.Title)`
7263
font-size: 16px;
7364
font-weight: 600;
74-
color: #ffffff;
65+
color: ${({ theme }) => theme.colors.gray[90]};
7566
`;
7667

7768
export const ActionButton = styled.button<{ variant?: 'cancel' | 'done' }>`
7869
font-size: 16px;
79-
color: #025fac;
70+
color: ${({ theme, variant }) =>
71+
variant === 'cancel' ? theme.colors.gray[90] : theme.colors['key-color']};
8072
font-weight: ${({ variant }) => (variant === 'done' ? 600 : 400)};
8173
`;
8274

@@ -86,8 +78,9 @@ export const SearchContainer = styled.div`
8678
align-items: center;
8779
gap: 14px;
8880
width: 100%;
89-
background-color: #222226;
81+
background-color: ${({ theme }) => theme.colors.gray[20]};
9082
margin-bottom: 16px;
83+
border-radius: 12px;
9184
`;
9285

9386
export const SearchIconWrapper = styled.button`
@@ -107,29 +100,32 @@ export const SearchIconWrapper = styled.button`
107100
`;
108101

109102
export const SearchInput = styled.input`
110-
width: 100%;
111-
max-width: 100%;
112-
height: 36px;
113-
background-color: #2e3034;
114-
border-radius: 9px;
115-
padding: 0 12px 0 30px;
116-
font-size: 16px;
117-
color: #f0f4ff;
118-
caret-color: rgba(0, 255, 163, 0.5);
119-
120-
&::placeholder {
121-
color: #999aa1;
122-
}
123-
124-
&:active:not(:focus) {
125-
background-color: rgba(119, 119, 119, 0.8);
126-
}
127-
128-
transition: all 0.3s ease;
103+
${({ theme }) => css`
104+
width: 100%;
105+
max-width: 100%;
106+
height: 36px;
107+
border: 0;
108+
color: ${theme.colors.gray[90]};
109+
background-color: ${theme.colors.gray[20]};
110+
padding: 0 12px 0 30px;
111+
font-size: 16px;
112+
caret-color: ${theme.colors['key-color']};
113+
border-radius: 12px;
114+
115+
&::placeholder {
116+
color: ${theme.colors.gray[70]};
117+
}
118+
119+
&:active:not(:focus) {
120+
background-color: rgba(119, 119, 119, 0.8);
121+
}
122+
123+
transition: all 0.3s ease;
124+
`}
129125
`;
130126

131127
export const ContentArea = styled.div`
132-
background-color: #2e2f37;
133-
height: calc(100% - 200px);
128+
background-color: ${({ theme }) => theme.colors.gray[20]};
129+
height: 500px;
134130
border-radius: 12px;
135131
`;

0 commit comments

Comments
 (0)