Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2692658
feat: ํ…์Šคํƒ์ฟผ๋ฆฌssr+ํ”„๋ฆฌํŽ˜์น˜ ๊ตฌ์ƒํ•˜๊ธฐ
626-ju Jul 25, 2025
3c94078
Merge branch 'dev' into feature/wineid-api
626-ju Jul 25, 2025
d5047a3
Merge branch 'dev' into feature/wineid-api
626-ju Jul 25, 2025
23b5e1f
Merge branch 'dev' into feature/wineid-api
626-ju Jul 26, 2025
7fdf36c
feat: HydrationBoundary ์ถ”๊ฐ€
626-ju Jul 26, 2025
a9fefac
feat:์™€์ธ ์ƒ์„ธ ssr+์™€์ธ ๋ชฉ๋ก์—์„œ ํ”„๋ฆฌํŒจ์นญ ๋Ÿฌํ”„ํ•˜๊ฒŒ
626-ju Jul 26, 2025
681c714
feat: ์„œ๋ฒ„์šฉ ์ธํ„ฐ์…‰ํ„ฐ๋กœ ์ฟ ํ‚ค ๋””์ฝ”๋”ฉ ๋˜๋‚˜ ํ™•์ธํ•˜๊ธฐ
626-ju Jul 27, 2025
f44453b
Merge branch 'dev' into feature/wineid-api
626-ju Jul 27, 2025
6667cb2
Merge branch 'dev' into feature/wineid-api
626-ju Jul 27, 2025
7940b3b
Merge branch 'dev' into feature/wineid-api
626-ju Jul 27, 2025
430bd2a
chore:์ž„์‹œ์ €์žฅ
626-ju Jul 28, 2025
b808747
merge dev ์ถฉ๋Œ ํ•ด๊ฒฐ
626-ju Jul 29, 2025
f42f977
fix: ํ…Œ์ŠคํŠธ์šฉ ๋“ค์–ด๊ฐ„ ๊ฑฐ ์‚ญ์ œํ•˜๊ธฐ
626-ju Jul 29, 2025
1fa7586
fix:์ด์ƒํ•˜๊ฒŒ ๋จธ์ง€๋œ ๊ฑฐ ์ˆ˜์ •
626-ju Jul 29, 2025
4a082aa
fix: ๋จธ์ง€ ์ž˜๋ชป๋œ ๊ฑฐ ์ˆ˜์ •
626-ju Jul 29, 2025
3891459
fix:๋จธ์ง€์ถฉ๋Œ ํ•ด๊ฒฐ
626-ju Jul 29, 2025
b7f7b8c
Merge branch 'dev' into feature/wineid-api
626-ju Jul 29, 2025
d032056
feat:์™€์ธ ๋ฆฌ๋ทฐ ๋‹ฌ๊ธฐ ํ˜ธ์ถœ์„ ์œ„ํ•œ ์™€์ธ์Šคํ† ์–ด ์ถ”๊ฐ€
626-ju Jul 29, 2025
c63c7a6
Merge branch 'dev' into feature/wineid-api
626-ju Jul 29, 2025
b3b4b69
fix: edit๋ชจ๋‹ฌ ์‘๋‹ต์†์„ฑ์ด๋ฆ„ ๋งž์ถ”๊ธฐ + ์นด๋“œ ํ•˜๋‚˜๋งŒ ํŽผ์ณ์ง€๋Š” ๊ธฐ๋Šฅ ์—†์• ๊ธฐ
626-ju Jul 29, 2025
c37c8a2
feat:๋ฆฌ๋ทฐ์นด๋“œ ํƒœ๊ทธ ํ•œ๊ธ€๋กœ ๋ฐ”๊พธ๊ธฐ+์—๋Ÿฌ๋ฐ”์šด๋”๋ฆฌ ๋„์ž…ํ•˜๋‹ค ์ž ๊น ๋ฉˆ์ถ”๊ณ  ๋‹ค๋ฅธ ์ผ๋ถ€ํ„ฐ
626-ju Jul 29, 2025
72c5955
refactor: ๋ผ์ดํฌ ๋“ฑ๋ก ํ•ด์ œ ํ•จ์ˆ˜ ๋‹ค๋ฅธ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌ
626-ju Jul 29, 2025
07186e5
์™œ ์ด ํŒŒ์ผ๋“ค์€ ์ปค๋ฐ‹ ์•ˆ ๋“ค์–ด๊ฐ”์ง€?
626-ju Jul 29, 2025
d605ec3
fix: ๋จธ์ง€์ถฉ๋Œ ํ•ด๊ฒฐ
626-ju Jul 29, 2025
a2315df
๋„ˆ ์™œ ์•ˆ๋“ค์–ด๊ฐ”์–ด
626-ju Jul 29, 2025
4acdd50
ratingํ‘œ๊ธฐ์˜ค๋ฅ˜ ์ˆ˜์ •
626-ju Jul 29, 2025
aa14d3b
feat:ssr์šฉ ๊ฒŸ ์„œ๋ฒ„์‚ฌ์ด๋“œ ํ”„๋กญ์Šค, api๋ผ์šฐ์ธ  ์ถ”๊ฐ€+์™€์ธ ๋ชฉ๋ก์—์„œ csr์šฉ ํ”„๋ฆฌํŒจ์นญ
626-ju Jul 29, 2025
aed377d
Merge branch 'dev' into feature/wineid-api
626-ju Jul 29, 2025
f63b886
chore(์ž„์‹œ์ €์žฅ): ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ์˜ค๋ฅ˜ ์ˆ˜์ • ์ค‘
626-ju Jul 30, 2025
015e1e7
merge:๋จธ์ง€์ถฉ๋Œ ํ•ด๊ฒฐ
626-ju Jul 30, 2025
d0a47a1
fix:gnb ๋จธ์ง€ํ•œ ํŒŒ์ผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ˆ˜์ •
626-ju Jul 30, 2025
2d2dd2a
chore: ๋ถˆํ•„์š”ํ•œ ์ฃผ์„ ์ œ๊ฑฐ
626-ju Jul 30, 2025
6e72f35
์—๋Ÿฌ ๋ฐ”์šด๋”๋ฆฌ๋กœ ๋ฎคํ…Œ์ด์…˜, ์ฟผ๋ฆฌ๋“ค ์—๋Ÿฌ ์žก๊ธฐ
626-ju Jul 30, 2025
5440eff
refactor:์—๋Ÿฌ๋ฐ”์šด๋”๋ฆฌ routerํ”„๋กญ์œผ๋กœ ๋ฐ›์•„ ์“ฐ๊ธฐ
626-ju Jul 30, 2025
3bd43ed
chore:์ž„์‹œ์ €์žฅ
626-ju Jul 31, 2025
5864f15
merge:์ถฉ๋Œ ํ•ด๊ฒฐ
626-ju Jul 31, 2025
496f453
Merge branch 'dev' into feature/wineid-api
626-ju Jul 31, 2025
5921924
chore:๋จธ์ง€ ์ „ wines/index ์‚ญ์ œ
626-ju Jul 31, 2025
08d664a
fix:404์ˆ˜์ •
626-ju Jul 31, 2025
ac979b2
chore: ๋น ์ง„ ๊ฑฐ ๋„ฃ๊ธฐ
626-ju Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/assets/not-found.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/assets/notFound.png
Binary file not shown.
117 changes: 117 additions & 0 deletions src/api/apiServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { GetServerSidePropsContext } from 'next';

export interface RetryRequestConfig extends InternalAxiosRequestConfig {
_retry?: boolean;
_refreshToken?: string;
_context?: GetServerSidePropsContext;
}

export const createSeverApiInstance = (accessToken: string | undefined) => {
const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
});

instance.interceptors.request.use((config) => {
if (accessToken && config.headers) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});

instance.interceptors.response.use(
(res) => res,
(error) => handleAxiosResponseError(error, instance),
);

return instance;
};

function handleAxiosResponseError(error: AxiosError, instance: AxiosInstance) {
const status = error.response?.status;
const originalRequest = error.config as RetryRequestConfig;

/* _retry๊ฐ’ ์กฐํšŒ๋กœ ๋ฌดํ•œ ์š”์ฒญx */
if (originalRequest._retry) return handleCommonError(error);
/* ์—†์œผ๋ฉด ์žฌ์‹œ๋„ ํ”Œ๋ž˜๊ทธ ์„ค์ • */
originalRequest._retry = true;

const refreshToken = originalRequest._refreshToken;
const context = originalRequest._context;

/* 401 ์—๋Ÿฌ๊ฐ€ ์•„๋‹ˆ๊ฑฐ๋‚˜ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ์—†๊ฑฐ๋‚˜ SSR ์ปจํ…์ŠคํŠธ๊ฐ€ ์—†์œผ๋ฉด ์—๋Ÿฌ */
if (status !== 401 || !refreshToken || !context) return handleCommonError(error);

try {
/* 401 ์—๋Ÿฌ๋ฉด ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๊ฐฑ์‹  */
return handleRetryReuquest(originalRequest, refreshToken, context, instance);
} catch (refreshTokenError) {
console.error('๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๊ฐฑ์‹  ์‹คํŒจ:', refreshTokenError);
/*์ฟ ํ‚ค ์‚ญ์ œ */
context.res.setHeader('Set-Cookie', [
`accessToken=; Path=/; Max-Age=0; SameSite=Lax`,
`refreshToken=; Path=/; Max-Age=0; SameSite=Lax`,
]);

return handleCommonError(refreshTokenError as AxiosError);
}
}

function handleCommonError(error: AxiosError) {
if (!error.response) {
return Promise.reject(new Error('๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒ. ์ธํ„ฐ๋„ท ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.'));
}

const { status, data } = error.response;
let errorMessage = (data as { message?: string })?.message ?? '์„œ๋ฒ„์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.';

console.error('API ์—๋Ÿฌ ๋ฐœ์ƒ:', { status, errorMessage, data });
return Promise.reject(error);
}

export async function fetchNewRefreshTokens(refreshToken: string) {
if (!refreshToken) throw new Error('๋ฆฌํ”„๋ ˆ์‰ฌํ† ํฐ์ด ์—†์–ด์š”');
const response = await axios.post(
`${process.env.NEXT_PUBLIC_BASE_URL}/${process.env.NEXT_PUBLIC_TEAM}/auth/refresh-token`,
{
refreshToken,
},
);

return {
newAccessToken: response.data.accessToken,
newRefreshToken: response.data.refreshToken,
};
}

async function handleRetryReuquest(
originalRequest: RetryRequestConfig,
refreshToken: string,
context: GetServerSidePropsContext,
instance: AxiosInstance,
): Promise<AxiosResponse> {
const refreshResult = await fetchNewRefreshTokens(refreshToken);
const { newAccessToken, newRefreshToken } = refreshResult;

/* ํด๋ผ์ด์–ธํŠธ ์ฟ ํ‚ค์— ๋ฐ˜์˜๋˜๊ฒŒ setCookie ์˜ต์…˜ ๋„ฃ์–ด์„œ ์ฃผ๊ธฐ */
setAuthCookies(context.res, newAccessToken, newRefreshToken);

/* ์ƒˆ ํ† ํฐ์œผ๋กœ ์›๋ž˜ ์š”์ฒญ์˜ ํ—ค๋” ์ˆ˜์ • */
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
}

/* ํ† ํฐ ๊ฐฑ์‹  ํ›„ ์›๋ž˜ ์š”์ฒญ์„ ์žฌ์‹œ๋„ */
return instance(originalRequest);
}

function setAuthCookies(
res: GetServerSidePropsContext['res'],
accessToken: string,
refreshToken: string,
) {
res.setHeader('Set-Cookie', [
`accessToken=${accessToken}; Path=/; Max-Age=${60 * 5}; SameSite=Lax;`,
`refreshToken=${refreshToken}; Path=/; Max-Age=${60 * 60 * 24 * 7}; SameSite=Lax;`,
]);
}
6 changes: 3 additions & 3 deletions src/api/editreview.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import apiClient from '@/api/apiClient';

interface UpdateReviewRequest {
reviewId: number;
id: number;
rating: number;
lightBold: number;
smoothTannic: number;
Expand All @@ -17,11 +17,11 @@ interface UpdateReviewResponse {
}

export const updateReview = async ({
reviewId,
id,
...body
}: UpdateReviewRequest): Promise<UpdateReviewResponse> => {
const response = await apiClient.patch<UpdateReviewResponse>(
`/${process.env.NEXT_PUBLIC_TEAM}/reviews/${reviewId}`,
`/${process.env.NEXT_PUBLIC_TEAM}/reviews/${id}`,
body,
);
return response.data;
Expand Down
1 change: 0 additions & 1 deletion src/api/wineid.ts โ†’ src/api/getWineInfo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// import { createSeverApiInstance, RetryRequestConfig } from './apiServer';
import { GetWineInfoResponse } from '@/types/WineTypes';

import apiClient from './apiClient';
Expand Down
9 changes: 9 additions & 0 deletions src/api/handleLikeRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import apiClient from '@/api/apiClient';

export async function postLike(reviewId: number) {
return apiClient.post(`${process.env.NEXT_PUBLIC_TEAM}/reviews/${reviewId}/like`);
}

export async function deleteLike(reviewId: number) {
return apiClient.delete(`${process.env.NEXT_PUBLIC_TEAM}/reviews/${reviewId}/like`);
}
3 changes: 3 additions & 0 deletions src/components/Modal/DeleteModal/DeleteModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ const DeleteModal = ({ type, id, showDeleteModal, setShowDeleteModal }: DeleteMo

const deleteWineMutation = useMutation<DeleteResponse, AxiosError, number>({
mutationFn: (id) => deleteWine(id),
throwOnError: true,
});
const deleteReviewMutation = useMutation<DeleteResponse, AxiosError, number>({
mutationFn: (id) => deleteReview(id),
throwOnError: true,
});

const handleDelete = () => {
Expand All @@ -38,6 +40,7 @@ const DeleteModal = ({ type, id, showDeleteModal, setShowDeleteModal }: DeleteMo
deleteReviewMutation.mutate(id, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reviews'] });
queryClient.invalidateQueries({ queryKey: ['wineDetail'] });
console.log('๋ฆฌ๋ทฐ ์‚ญ์ œ ์„ฑ๊ณต');
setShowDeleteModal(false);
},
Expand Down
66 changes: 34 additions & 32 deletions src/components/Modal/ReviewModal/EditReviewModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';

import { useMutation, useQueryClient } from '@tanstack/react-query';
import Image from 'next/image';
Expand All @@ -15,21 +15,21 @@ import { Button } from '../../ui/button';

interface ReviewForm {
rating: number;
sliderLightBold: number;
sliderSmoothTanic: number;
sliderdrySweet: number;
slidersoftAcidic: number;
lightBold: number;
smoothTannic: number;
drySweet: number;
softAcidic: number;
aroma: Array<string>;
content: string;
}

interface ReviewData {
reviewId: number;
id: number;
rating: number;
sliderLightBold: number;
sliderSmoothTanic: number;
sliderdrySweet: number;
slidersoftAcidic: number;
lightBold: number;
smoothTannic: number;
drySweet: number;
softAcidic: number;
aroma: string[];
content: string;
}
Expand All @@ -56,7 +56,7 @@ const aromaOptions = [
'๊ฐ€์ฃฝ',
];

const aromaMap: Record<string, string> = {
export const aromaMap: Record<string, string> = {
์ฒด๋ฆฌ: 'CHERRY',
๋ฒ ๋ฆฌ: 'BERRY',
์˜คํฌ: 'OAK',
Expand All @@ -81,18 +81,23 @@ const aromaMap: Record<string, string> = {
const EditReviewModal = ({
wineName,
reviewData,
showEditModal,
setShowEditModal,
}: {
wineName: string;
reviewData: ReviewData;
showEditModal: boolean;
setShowEditModal: (state: boolean) => void;
}) => {
const [showEditModal, setShowEditModal] = useState(false);
const queryClient = useQueryClient();

const updateReviewMutation = useMutation({
mutationFn: updateReview,
throwOnError: true,
onSuccess: () => {
console.log('๋ฆฌ๋ทฐ ์ˆ˜์ • ์™„๋ฃŒ');
queryClient.invalidateQueries({ queryKey: ['reviews'] });
queryClient.invalidateQueries({ queryKey: ['wineDetail'] });
setShowEditModal(false);
},
onError: (error) => {
Expand All @@ -112,10 +117,10 @@ const EditReviewModal = ({
} = useForm<ReviewForm>({
defaultValues: {
rating: reviewData.rating,
sliderLightBold: reviewData.sliderLightBold,
sliderSmoothTanic: reviewData.sliderSmoothTanic,
sliderdrySweet: reviewData.sliderdrySweet,
slidersoftAcidic: reviewData.slidersoftAcidic,
lightBold: reviewData.lightBold,
smoothTannic: reviewData.smoothTannic,
drySweet: reviewData.drySweet,
softAcidic: reviewData.softAcidic,
content: reviewData.content,
aroma: reviewData.aroma.map((eng) => {
const kor = Object.keys(aromaMap).find((key) => aromaMap[key] === eng);
Expand Down Expand Up @@ -146,12 +151,12 @@ const EditReviewModal = ({
return;
}
const fullData = {
reviewId: reviewData.reviewId,
id: reviewData.id,
rating: data.rating,
lightBold: data.sliderLightBold,
smoothTannic: data.sliderSmoothTanic,
drySweet: data.sliderdrySweet,
softAcidic: data.slidersoftAcidic,
lightBold: data.lightBold,
smoothTannic: data.smoothTannic,
drySweet: data.drySweet,
softAcidic: data.softAcidic,
aroma: data.aroma.map((a) => aromaMap[a]).filter(Boolean),
content: data.content,
};
Expand All @@ -167,9 +172,6 @@ const EditReviewModal = ({

return (
<div>
<Button variant='purpleDark' size='xs' width='sm' onClick={() => setShowEditModal(true)}>
๋ฆฌ๋ทฐ ์ˆ˜์ •ํ•˜๊ธฐ
</Button>
<BasicModal
type='review'
title='๋ฆฌ๋ทฐ ์ˆ˜์ •'
Expand Down Expand Up @@ -234,41 +236,41 @@ const EditReviewModal = ({

<div className='mb-[40px] space-y-[18px]'>
<FlavorSlider
value={watch('sliderLightBold')}
value={watch('lightBold')}
min={0}
max={10}
step={1}
onChange={(val) => setValue('sliderLightBold', val)}
onChange={(val) => setValue('lightBold', val)}
labelLeft='๊ฐ€๋ฒผ์›Œ์š”'
labelRight='์ง„ํ•ด์š”'
badgeLabel='๋ฐ”๋””๊ฐ'
/>
<FlavorSlider
value={watch('sliderSmoothTanic')}
value={watch('smoothTannic')}
min={0}
max={10}
step={1}
onChange={(val) => setValue('sliderSmoothTanic', val)}
onChange={(val) => setValue('smoothTannic', val)}
labelLeft='๋ถ€๋“œ๋Ÿฌ์›Œ์š”'
labelRight='๋–ซ์–ด์š”'
badgeLabel='ํƒ€๋‹Œ'
/>
<FlavorSlider
value={watch('sliderdrySweet')}
value={watch('drySweet')}
min={0}
max={10}
step={1}
onChange={(val) => setValue('sliderdrySweet', val)}
onChange={(val) => setValue('drySweet', val)}
labelLeft='๋“œ๋ผ์ดํ•ด์š”'
labelRight='๋‹ฌ์•„์š”'
badgeLabel='๋‹น๋„'
/>
<FlavorSlider
value={watch('slidersoftAcidic')}
value={watch('softAcidic')}
min={0}
max={10}
step={1}
onChange={(val) => setValue('slidersoftAcidic', val)}
onChange={(val) => setValue('softAcidic', val)}
labelLeft='์•ˆ ์…”์š”'
labelRight='๋งŽ์ด ์…”์š”'
badgeLabel='์‚ฐ๋ฏธ'
Expand Down
Loading