Skip to content

Commit

Permalink
feat: change vote api (#37)
Browse files Browse the repository at this point in the history
* feat: change vote api

* fix post error
  • Loading branch information
Najeong-Kim authored Oct 20, 2024
1 parent 7c38c4c commit b05d3cf
Show file tree
Hide file tree
Showing 10 changed files with 67 additions and 61 deletions.
8 changes: 4 additions & 4 deletions src/entities/discovery/api/discovery.queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ export const discoveryQueries = {
queryKey: [...discoveryQueries.listQueryKey()],
queryFn: () => getDiscoveries(),
}),
voteQueryKey: (key: string) => [...discoveryQueries.all(), 'vote', key],
vote: (key: string) =>
voteQueryKey: (resortId: number) => [...discoveryQueries.all(), 'vote', resortId],
vote: (resortId: number) =>
queryOptions({
queryKey: discoveryQueries.voteQueryKey(key),
queryFn: () => getVote(key),
queryKey: discoveryQueries.voteQueryKey(resortId),
queryFn: () => getVote(resortId),
}),
};
4 changes: 2 additions & 2 deletions src/entities/discovery/api/get-vote.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { apiClient } from '@/shared/api/base';
import type { Vote } from '../model';

export const getVote = async (key: string): Promise<Vote> => {
const result = await apiClient.get<Vote>(`/ski/${key}/snowmaking`);
export const getVote = async (resortId: number): Promise<Vote> => {
const result = await apiClient.get<Vote>(`/api/snow-maker/${resortId}`);

return result;
};
5 changes: 2 additions & 3 deletions src/entities/discovery/api/post-vote.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { apiClient } from '@/shared/api/base';
import type { PostVoteRequest } from '../model';

export const postVote = async (key: string, body: PostVoteRequest) => {
const res = await apiClient.post<PostVoteRequest>(`/ski/${key}/snowmaking`, body);
export const postVote = async (resortId: number, {isPositive}: {isPositive: boolean}) => {
const res = await apiClient.post(`/api/snow-maker/${resortId}/vote`, {isPositive});
return res;
};
6 changes: 3 additions & 3 deletions src/entities/discovery/api/use-post-vote.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { discoveryApi } from '..';

export const usePostVote = (key: string) => {
export const usePostVote = (resortId: number) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: ({ isLike }: { isLike: boolean }) => discoveryApi.postVote(key, { isLike }),
mutationFn: ({ isPositive }: { isPositive: boolean }) => discoveryApi.postVote(resortId, {isPositive}),
async onSettled() {
await queryClient.invalidateQueries({
queryKey: discoveryApi.discoveryQueries.voteQueryKey(key),
queryKey: discoveryApi.discoveryQueries.voteQueryKey(resortId),
});
},
});
Expand Down
2 changes: 1 addition & 1 deletion src/entities/discovery/model/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { DiscoveryData } from './constants';
export type { Weather, WeeklyWeather, Discovery, Vote, PostVoteRequest } from './model';
export type { Weather, WeeklyWeather, Discovery, Vote } from './model';
12 changes: 5 additions & 7 deletions src/entities/discovery/model/model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ export type Discovery = {
};

export type Vote = {
totalNum: number;
likeNum: number;
};

export type PostVoteRequest = {
isLike: boolean;
};
resortId: number;
totalVotes: number;
positiveVotes: number;
status: string;
};
29 changes: 14 additions & 15 deletions src/features/discovery-detail/ui/vote-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
DialogTrigger,
} from '@/shared/ui/dialog';
import { formatDate } from '../lib/formatDate';
import { getVoteText } from '../lib/getVoteText';
import { canVote, getVoteData, saveVoteData } from '../lib/vote';

interface VoteDialogProps {
Expand All @@ -25,18 +24,18 @@ interface VoteDialogProps {
}

const VoteDialog = ({ id, trigger }: VoteDialogProps) => {
const [isGood, setIsGood] = useState<boolean>(true);
const { data: voteData } = useQuery(discoveryApi.discoveryQueries.vote(id.toString()));
const [isPositive, setIsPositive] = useState<boolean>(true);
const { data: voteData } = useQuery(discoveryApi.discoveryQueries.vote(id));

const { mutateAsync } = usePostVote(id.toString());
const { mutateAsync } = usePostVote(id);

const handleVote = useCallback(async () => {
if (!canVote(id.toString())) {
toast.error('하루에 한 번만 투표할 수 있어요');
return;
}
try {
await mutateAsync({ isLike: isGood });
await mutateAsync({ isPositive });
} catch (error) {
console.log(error);
} finally {
Expand All @@ -45,7 +44,7 @@ const VoteDialog = ({ id, trigger }: VoteDialogProps) => {
saveVoteData(voteData);
toast.success('고마워요! 투표의 결과가 반영되었어요');
}
}, [id, isGood, mutateAsync]);
}, [id, isPositive, mutateAsync]);

return (
<Dialog>
Expand All @@ -57,10 +56,10 @@ const VoteDialog = ({ id, trigger }: VoteDialogProps) => {
<DialogHeader>
<p className={cn('title3-semibold')}>오늘의 설질</p>
<div className={cn('flex flex-col gap-1')}>
<DialogTitle>{getVoteText(voteData?.totalNum, voteData?.likeNum)}</DialogTitle>
<DialogTitle>{voteData?.status}</DialogTitle>
<p className={cn('body1-semibold text-gray-60')}>
{voteData?.totalNum}명 중{' '}
<span className={cn('body1-bold text-main-1')}>{voteData?.likeNum}</span>
{voteData?.totalVotes}명 중{' '}
<span className={cn('body1-bold text-main-1')}>{voteData?.positiveVotes}</span>
명이 긍정적으로 투표했어요.
</p>
</div>
Expand All @@ -71,22 +70,22 @@ const VoteDialog = ({ id, trigger }: VoteDialogProps) => {
<button
className={cn(
'flex h-10 w-full items-center justify-between rounded-[8px] border border-main-1 pl-4 pr-3',
!isGood && 'border-gray-30'
!isPositive && 'border-gray-30'
)}
onClick={() => setIsGood(true)}
onClick={() => setIsPositive(true)}
>
<p className={cn('body1-regular text-gray-60')}>괜찮을 것 같아요</p>
{isGood && <CheckIcon className={cn('text-main-1')} />}
{isPositive && <CheckIcon className={cn('text-main-1')} />}
</button>
<button
className={cn(
'flex h-10 items-center justify-between rounded-[8px] border border-main-1 pl-4 pr-3',
isGood && 'border-gray-30'
isPositive && 'border-gray-30'
)}
onClick={() => setIsGood(false)}
onClick={() => setIsPositive(false)}
>
<p className={cn('body1-regular text-gray-60')}>별로일 것 같아요</p>
{!isGood && <CheckIcon className={cn('text-main-1')} />}
{!isPositive && <CheckIcon className={cn('text-main-1')} />}
</button>
</div>
<button
Expand Down
14 changes: 11 additions & 3 deletions src/pages/api/weski/[...path].ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,25 @@ import { API_URL } from '@/shared/config';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { path } = req.query;
const url = `${API_URL}/${(path as string[]).join('/')}`;
const searchParams = new URLSearchParams(req.url?.split('?')[1]);
const url = `${API_URL}/${(path as string[]).join('/')}` + (searchParams.size ? `?${searchParams}` : '');

const response = await fetch(url, {
method: req.method,
headers: {
...(req.headers as Record<string, string>),
},
body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined,
body: req.method !== 'GET' && req.body ? JSON.stringify(req.body) : undefined,
});

const data = await response.json();
const contentType = response.headers.get('Content-Type');
let data;

if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}

res.status(response.status).json(data);
}
17 changes: 11 additions & 6 deletions src/shared/api/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class ApiClient {

public async get<TResult = unknown>(
endpoint: string,
queryParams?: Record<string, string | number>
queryParams?: Record<string, string | number | boolean>
): Promise<TResult> {
const url = new URL('/api/weski' + endpoint, window.location.origin);

Expand All @@ -42,16 +42,21 @@ export class ApiClient {

public async post<TResult = unknown, TData = Record<string, unknown>>(
endpoint: string,
body: TData
queryParams?: Record<string, string | number | boolean>,
body?: TData
): Promise<TResult> {
const url = new URL('/api/weski' + endpoint, window.location.origin);

if (queryParams) {
Object.entries(queryParams).forEach(([key, value]) => {
url.searchParams.append(key, value.toString());
});
}


const response = await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
body: body ? JSON.stringify(body) : undefined,
});

return this.handleResponse<TResult>(response);
Expand Down
31 changes: 14 additions & 17 deletions src/views/discovery-detail/ui/discovery-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import DiscoverySummary from '@/widgets/discovery-detail/ui/discovery-summary';
import { Header } from '@/widgets/header/ui';
import { WebcamMap, WebcamSlopList } from '@/widgets/webcam/ui';
import { formatDate } from '@/features/discovery-detail/lib/formatDate';
import { getVoteText } from '@/features/discovery-detail/lib/getVoteText';
import { canVote, getVoteData, saveVoteData } from '@/features/discovery-detail/lib/vote';
import AppDownloadDialog from '@/features/discovery-detail/ui/app-download-dialog';
import useMapPinch from '@/features/slop/hooks/useMapPinch';
Expand All @@ -27,13 +26,13 @@ const DiscoveryDetailPage = ({ params }: { params: { resortId: string } }) => {
const discovery = DiscoveryData.find(
(discovery) => discovery.id === +params?.resortId
) as Discovery;
const { data: voteData } = useQuery(discoveryApi.discoveryQueries.vote(params?.resortId));
const { data: voteData } = useQuery(discoveryApi.discoveryQueries.vote(+params?.resortId));
const data = RESORT_DOMAIN[discovery?.map as keyof typeof RESORT_DOMAIN];
const [selectedTab, setSelectedTab] = useState('webcam');
const [showAppDownloadDialog, setShowAppDownloadDialog] = useState(true);
const { mutateAsync } = usePostVote(params?.resortId);
const { mutateAsync } = usePostVote(+params?.resortId);

const [isGood, setIsGood] = useState<boolean>(true);
const [isPositive, setIsPositive] = useState<boolean>(true);
const [cameraPositions, setCameraPositions] = useState<{
[key: string]: Position;
}>({});
Expand Down Expand Up @@ -62,7 +61,7 @@ const DiscoveryDetailPage = ({ params }: { params: { resortId: string } }) => {
return;
}
try {
await mutateAsync({ isLike: isGood });
await mutateAsync({ isPositive });
} catch (error) {
console.log(error);
} finally {
Expand All @@ -71,7 +70,7 @@ const DiscoveryDetailPage = ({ params }: { params: { resortId: string } }) => {
saveVoteData(voteData);
toast.success('고마워요! 투표의 결과가 반영되었어요');
}
}, [isGood, mutateAsync, params?.resortId]);
}, [isPositive, mutateAsync, params?.resortId]);

if (!discovery) return;

Expand Down Expand Up @@ -119,12 +118,10 @@ const DiscoveryDetailPage = ({ params }: { params: { resortId: string } }) => {
<div className={cn('flex flex-col gap-6')}>
<p className={cn('title3-semibold')}>오늘의 설질</p>
<div className={cn('flex flex-col gap-1')}>
<p className={cn('h3-semibold')}>
{getVoteText(voteData?.totalNum, voteData?.likeNum)}
</p>
<p className={cn('h3-semibold')}>{voteData?.status}</p>
<p className={cn('body1-semibold text-gray-60')}>
{voteData?.totalNum}명 중{' '}
<span className={cn('body1-bold text-main-1')}>{voteData?.likeNum}</span>
{voteData?.totalVotes}명 중{' '}
<span className={cn('body1-bold text-main-1')}>{voteData?.positiveVotes}</span>
명이 긍정적으로 투표했어요.
</p>
</div>
Expand All @@ -135,22 +132,22 @@ const DiscoveryDetailPage = ({ params }: { params: { resortId: string } }) => {
<button
className={cn(
'flex h-10 w-full items-center justify-between rounded-[8px] border border-main-1 pl-4 pr-3',
!isGood && 'border-gray-30'
!isPositive && 'border-gray-30'
)}
onClick={() => setIsGood(true)}
onClick={() => setIsPositive(true)}
>
<p className={cn('body1-regular text-gray-60')}>괜찮을 것 같아요</p>
{isGood && <CheckIcon className={cn('text-main-1')} />}
{isPositive && <CheckIcon className={cn('text-main-1')} />}
</button>
<button
className={cn(
'flex h-10 items-center justify-between rounded-[8px] border border-main-1 pl-4 pr-3',
isGood && 'border-gray-30'
isPositive && 'border-gray-30'
)}
onClick={() => setIsGood(false)}
onClick={() => setIsPositive(false)}
>
<p className={cn('body1-regular text-gray-60')}>별로일 것 같아요</p>
{!isGood && <CheckIcon className={cn('text-main-1')} />}
{!isPositive && <CheckIcon className={cn('text-main-1')} />}
</button>
</div>
<button
Expand Down

0 comments on commit b05d3cf

Please sign in to comment.