Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
44d254e
πŸ› fix: 데이터 κ°’ μˆ˜μ •
HaeJungg Nov 14, 2024
77585af
♻️ refactor: μ½”λ“œ λ¦¬νŒ©ν† λ§
HaeJungg Nov 14, 2024
0344be1
πŸ› fix: liked 전달
HaeJungg Nov 14, 2024
340752f
πŸ“ docs: μ£Όμ„μ‚­μ œ
HaeJungg Nov 14, 2024
7a31287
✨ feat: μ°œν•˜κΈ° api μΆ”κ°€
HaeJungg Nov 14, 2024
c1fc0d7
✨ feat: μ°œν•œ 리슀트 μ»΄ν¬λ„ŒνŠΈ μƒˆλ‘œ μž‘μ„±
HaeJungg Nov 14, 2024
1f75511
✨ feat: μ°œν•˜κΈ° κΈ°λŠ₯ μΆ”κ°€, λ‘œκ·ΈμΈμ—¬λΆ€ μΆ”κ°€
HaeJungg Nov 14, 2024
d3649f7
πŸ› fix: μ»΄ν¬λ„ŒνŠΈ 변경에 따라 import μˆ˜μ •
HaeJungg Nov 14, 2024
e075627
✨ feat: μ°œν•˜κΈ° κΈ°λŠ₯ μΆ”κ°€
HaeJungg Nov 14, 2024
79d9191
πŸ’„ design: 확인 λͺ¨λ‹¬ λ””μžμΈ μˆ˜μ •
HaeJungg Nov 14, 2024
ef72b00
πŸ› fix: νƒ€μž… 변경에 따라 더미데이터 λ³€κ²½
HaeJungg Nov 14, 2024
84a8539
πŸ› fix: νƒ€μž… 변경에 따라 데이터 λ³€κ²½, μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” 파일 μ‚­μ œ
HaeJungg Nov 14, 2024
b243456
Merge branch 'develop' into Feat/121/LikedApi
HaeJungg Nov 15, 2024
9e1b752
♻️ refactor: 쀑볡 μ½”λ“œ λ¦¬νŒ©ν† λ§
HaeJungg Nov 15, 2024
4317a4c
πŸ› fix: 잘λͺ»λœ ν† μŠ€νŠΈ μ‚¬μš©λ²• κ°œμ„ 
HaeJungg Nov 15, 2024
5bf99c2
πŸ› fix: λ¦¬λ””λ ‰μ…˜ κ°œμ„ 
HaeJungg Nov 15, 2024
e82ff4c
πŸ“¦ chore: ν† μŠ€νŠΈ μŠ€νƒ€μΌλ§ import
HaeJungg Nov 15, 2024
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
39 changes: 39 additions & 0 deletions src/_apis/liked/liked-apis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { fetchApi } from '@/src/utils/api';
import { GatheringResponseType } from '@/src/types/gathering-data';

// 찜 λͺ©λ‘ 쑰회
export async function getLikedList(page: number): Promise<GatheringResponseType> {
const url = `/api/liked/memberLikes?page=${page}&size=6`;

const response = await fetchApi<{ data: GatheringResponseType }>(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
}

// 찜 μΆ”κ°€ν•˜κΈ°
export async function addLike(gatheringId: number): Promise<void> {
const url = `/api/liked/${gatheringId}`;

await fetchApi<void>(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
}

// 찜 ν•΄μ œν•˜κΈ°
export async function removeLike(gatheringId: number): Promise<void> {
const url = `/api/liked/${gatheringId}`;

await fetchApi<void>(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
}
9 changes: 9 additions & 0 deletions src/_queries/liked/liked-queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import { getLikedList } from '@/src/_apis/liked/liked-apis';

export function useGetLikedListQuery(page: number) {
return useQuery({
queryKey: ['likedList', page],
queryFn: () => getLikedList(page),
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const ModalWithUser = Template.bind({});
ModalWithUser.args = {
opened: false,
data: {
crewId: 1,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codebase verification

crewId 속성이 storiesμ—λ§Œ μΆ”κ°€λ˜μ–΄ 있고 μ‹€μ œ μ»΄ν¬λ„ŒνŠΈμ—μ„œλŠ” μ‚¬μš©λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€

검증 κ²°κ³Ό:

  • stories νŒŒμΌμ—μ„œλ§Œ crewId: 1 속성이 μΆ”κ°€λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€
  • container.tsx와 presenter.tsxμ—μ„œ GatheringDetailType νƒ€μž…μ„ μ‚¬μš©ν•˜κ³  μžˆμ§€λ§Œ, crewIdλ₯Ό μ‹€μ œλ‘œ μ‚¬μš©ν•˜λŠ” λ‘œμ§μ€ μ—†μŠ΅λ‹ˆλ‹€
  • μ’‹μ•„μš” API 연동을 μœ„ν•΄ μΆ”κ°€λœ 속성이라면, κ΄€λ ¨ κΈ°λŠ₯ κ΅¬ν˜„μ΄ λˆ„λ½λœ κ²ƒμœΌλ‘œ λ³΄μž…λ‹ˆλ‹€
πŸ”— Analysis chain

crewId 속성이 μΌκ΄€λ˜κ²Œ μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

λͺ¨λ“  μŠ€ν† λ¦¬μ— crewId: 1이 μΆ”κ°€λœ 것을 ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” μ’‹μ•„μš” API 연동을 μœ„ν•œ 변경사항과 잘 λΆ€ν•©ν•©λ‹ˆλ‹€.

μ»΄ν¬λ„ŒνŠΈμ—μ„œ 이 속성이 μ˜¬λ°”λ₯΄κ²Œ μ‚¬μš©λ˜λŠ”μ§€ ν™•μΈν•˜κΈ° μœ„ν•΄ λ‹€μŒ 슀크립트λ₯Ό μ‹€ν–‰ν•˜κ² μŠ΅λ‹ˆλ‹€:

Also applies to: 101-101, 144-144

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: GatheringDetailModalContainer μ»΄ν¬λ„ŒνŠΈμ—μ„œ crewId μ‚¬μš© μ—¬λΆ€ 확인

# container.tsx νŒŒμΌμ—μ„œ crewId μ‚¬μš© 확인
ast-grep --pattern 'interface GatheringDetailType {
  $$$
  crewId: number
  $$$
}'

# crewIdλ₯Ό props둜 μ „λ‹¬ν•˜λŠ” νŒ¨ν„΄ 확인
ast-grep --pattern 'function GatheringDetailModalContainer({ $$$, data }: GatheringDetailModalContainerProps) {
  $$$
  crewId
  $$$
}'

Length of output: 223


Script:

#!/bin/bash
# λͺ¨λ‹¬ μ»΄ν¬λ„ŒνŠΈ νŒŒμΌμ—μ„œ crewId μ‚¬μš© νŒ¨ν„΄ 검색
rg -l "crewId" "src/app/(crew)/crew/_components/gathering-detail-modal/"

# λͺ¨λ‹¬ μ»΄ν¬λ„ŒνŠΈμ˜ μΈν„°νŽ˜μ΄μŠ€μ™€ νƒ€μž… μ •μ˜ 검색
rg "interface|type.*GatheringDetail" "src/app/(crew)/crew/_components/gathering-detail-modal/" -A 5

# crewId μ‚¬μš© μ»¨ν…μŠ€νŠΈ 확인
rg "crewId" "src/app/(crew)/crew/_components/gathering-detail-modal/" -B 2 -A 2

Length of output: 3064


Script:

#!/bin/bash
# GatheringDetailType μΈν„°νŽ˜μ΄μŠ€ μ •μ˜ 검색
rg "type.*GatheringDetailType" -A 10 "src/app/(crew)/crew/_components"

# λͺ¨λ‹¬ μ»΄ν¬λ„ŒνŠΈμ—μ„œ data prop의 μ‚¬μš© 확인
rg "data: GatheringDetailType" -B 5 -A 10 "src/app/(crew)/crew/_components"

# crewIdκ°€ μ‹€μ œλ‘œ μ‚¬μš©λ˜λŠ” 둜직 검색
rg "data\.crewId" "src/app/(crew)/crew/_components"

Length of output: 3085

id: 1,
title: 'μ‹ λ‚˜λŠ” μš΄λ™...즐거운..μ½”λ”©..',
introduce: 'κ³΅μ§€μ‚¬ν•­μž…λ‹ˆλ‹€. λ‹€λ“€ 이번 약속 μžŠμ§€ μ•ŠμœΌμ…¨μ£ ? κΌ­ μ°Έμ—¬ λΆ€νƒλ“œλ¦½λ‹ˆλ‹€~',
Expand Down Expand Up @@ -97,6 +98,7 @@ export const ModalWithCaptain = Template.bind({});
ModalWithCaptain.args = {
opened: false,
data: {
crewId: 1,
id: 2,
title: 'μ‹ λ‚˜λŠ” μš΄λ™...즐거운..μ½”λ”©..',
introduce: 'κ³΅μ§€μ‚¬ν•­μž…λ‹ˆλ‹€. λ‹€λ“€ 이번 약속 μžŠμ§€ μ•ŠμœΌμ…¨μ£ ? κΌ­ μ°Έμ—¬ λΆ€νƒλ“œλ¦½λ‹ˆλ‹€~',
Expand Down Expand Up @@ -139,6 +141,7 @@ export const ModalWithCrew = Template.bind({});
ModalWithCrew.args = {
opened: false,
data: {
crewId: 1,
id: 3,
title: 'μ•„μΉ¨ νƒ€μž„ μ—λ„ˆμ§€ μš”κ°€',
introduce: 'κ³΅μ§€μ‚¬ν•­μž…λ‹ˆλ‹€. λ‹€λ“€ 이번 약속 μžŠμ§€ μ•ŠμœΌμ…¨μ£ ? κΌ­ μ°Έμ—¬ λΆ€νƒλ“œλ¦½λ‹ˆλ‹€~',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { addLike, removeLike } from '@/src/_apis/liked/liked-apis';
import { useGetGatheringListQuery } from '@/src/_queries/detail/gathering-list-queries';
import { ApiError } from '@/src/utils/api';
import ConfirmModal from '@/src/components/common/modal/confirm-modal';
import Toast from '@/src/components/common/toast';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codebase verification

Toast μ»΄ν¬λ„ŒνŠΈκ°€ 잘λͺ»λœ λ°©μ‹μœΌλ‘œ μ‚¬μš©λ˜κ³  μžˆμŠ΅λ‹ˆλ‹€

Toast μ»΄ν¬λ„ŒνŠΈλŠ” React μ»΄ν¬λ„ŒνŠΈλ‘œ κ΅¬ν˜„λ˜μ–΄ μžˆμœΌλ‚˜, ν˜„μž¬ ν•¨μˆ˜μ²˜λŸΌ 호좜되고 μžˆμŠ΅λ‹ˆλ‹€. λ‹€μŒκ³Ό 같이 μˆ˜μ •μ΄ ν•„μš”ν•©λ‹ˆλ‹€:

  • gathering-list-section.tsx의 Toast 호좜 뢀뢄을 JSX λ¬Έλ²•μœΌλ‘œ λ³€κ²½ν•΄μ•Ό ν•©λ‹ˆλ‹€:
{apiError instanceof ApiError && (
  <Toast message={`μ°œν•˜κΈ°μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€: ${apiError.message}`} type="error" />
)}
  • ToastContainerκ°€ νŽ˜μ΄μ§€μ—μ„œ ν•œ 번만 λ Œλ”λ§λ˜λ„λ‘ μƒμœ„ λ ˆμ΄μ•„μ›ƒ μ»΄ν¬λ„ŒνŠΈλ‘œ 이동해야 ν•©λ‹ˆλ‹€.
πŸ”— Analysis chain

λΆˆν•„μš”ν•œ import ꡬ문이 μžˆλŠ”μ§€ 확인이 ν•„μš”ν•©λ‹ˆλ‹€.

Toast μ»΄ν¬λ„ŒνŠΈκ°€ μ‹€μ œλ‘œ μ‚¬μš©λ˜λŠ” 방식이 μ˜¬λ°”λ₯Έμ§€ ν™•μΈν•΄μ£Όμ„Έμš”. React μ»΄ν¬λ„ŒνŠΈμ²˜λŸΌ μ‚¬μš©λ˜κ³  μžˆμ§€λ§Œ, ν•¨μˆ˜μ²˜λŸΌ 호좜되고 μžˆμŠ΅λ‹ˆλ‹€.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Toast μ»΄ν¬λ„ŒνŠΈμ˜ μ •μ˜λ₯Ό 확인
ast-grep --pattern 'export $_ function Toast'

Length of output: 47


Script:

#!/bin/bash
# Toast μ»΄ν¬λ„ŒνŠΈμ˜ μ •μ˜μ™€ μ‚¬μš© 방식을 확인
rg -A 5 "export.*Toast" 

Length of output: 1582


Script:

#!/bin/bash
# gathering-list-section.tsx νŒŒμΌμ—μ„œ Toast의 μ‚¬μš© 방식을 확인
rg -A 3 "Toast\." src/app/\(crew\)/crew/detail/\[id\]/_components/gathering-list-section.tsx

Length of output: 92


Script:

#!/bin/bash
# Toast μ»΄ν¬λ„ŒνŠΈ 파일 μ°ΎκΈ°
fd -t f "toast" -I

Length of output: 64


Script:

#!/bin/bash
# gathering-list-section.tsx 파일의 전체 λ‚΄μš©μ„ ν™•μΈν•˜μ—¬ Toast μ‚¬μš© 방식 νŒŒμ•…
rg -l "Toast" src/app/\(crew\)/crew/detail/\[id\]/_components/gathering-list-section.tsx && cat src/app/\(crew\)/crew/detail/\[id\]/_components/gathering-list-section.tsx

Length of output: 2435


Script:

#!/bin/bash
# Toast μ»΄ν¬λ„ŒνŠΈμ˜ 전체 κ΅¬ν˜„ 확인
cat src/components/common/toast/index.tsx

Length of output: 762

import GatheringCardCarousel from '@/src/components/gathering-list/gathering-card-carousel';

interface GatheringListSectionProps {
Expand All @@ -9,12 +15,57 @@ interface GatheringListSectionProps {

export default function GatheringListSection({ id }: GatheringListSectionProps) {
const { data: gatheringList, isLoading, error } = useGetGatheringListQuery(id);
const [showLoginModal, setShowLoginModal] = useState(false);
const router = useRouter();

if (isLoading) return <p>λ‘œλ”© 쀑...</p>;
const handleLike = async (gatheringId: number) => {
try {
await addLike(gatheringId);
} catch (apiError) {
if (apiError instanceof ApiError) {
Toast({ message: `μ°œν•˜κΈ°μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€: ${apiError.message}`, type: 'error' });
}
}
};

const handleUnlike = async (gatheringId: number) => {
try {
await removeLike(gatheringId);
} catch (apiError) {
if (apiError instanceof ApiError) {
Toast({ message: `μ°œν•˜κΈ° ν•΄μ œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€: ${apiError.message}`, type: 'error' });
}
}
};

if (error) return <p>데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 데 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€: {error.message}</p>;
const handleLoginRedirect = () => {
const currentPath = window.location.href;
router.push(`/login?redirect=${encodeURIComponent(currentPath)}`);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

URL 인코딩 λ³΄μ•ˆ κ°•ν™”κ°€ ν•„μš”ν•©λ‹ˆλ‹€

ν˜„μž¬ κ΅¬ν˜„μ—μ„œλŠ” window.location.hrefλ₯Ό 직접 μ‚¬μš©ν•˜κ³  μžˆμ–΄ 잠재적인 λ³΄μ•ˆ μœ„ν—˜μ΄ μžˆμ„ 수 μžˆμŠ΅λ‹ˆλ‹€. URL νŒŒλΌλ―Έν„°μ˜ μ μ ˆν•œ 검증과 μ •μ œκ°€ ν•„μš”ν•©λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같은 κ°œμ„ μ„ μ œμ•ˆν•©λ‹ˆλ‹€:

  const handleLoginRedirect = () => {
-   const currentPath = window.location.href;
+   const currentPath = window.location.pathname + window.location.search;
+   const sanitizedPath = currentPath.replace(/[^\w\-\.\~\/\?\[\]\@\!\$\&\'\(\)\*\+\,\;\=]/g, '');
-   router.push(`/login?redirect=${encodeURIComponent(currentPath)}`);
+   router.push(`/login?redirect=${encodeURIComponent(sanitizedPath)}`);
  };
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleLoginRedirect = () => {
const currentPath = window.location.href;
router.push(`/login?redirect=${encodeURIComponent(currentPath)}`);
};
const handleLoginRedirect = () => {
const currentPath = window.location.pathname + window.location.search;
const sanitizedPath = currentPath.replace(/[^\w\-\.\~\/\?\[\]\@\!\$\&\'\(\)\*\+\,\;\=]/g, '');
router.push(`/login?redirect=${encodeURIComponent(sanitizedPath)}`);
};


// TODO: μΆ”ν›„ μ—λŸ¬, λ‘œλ”© μˆ˜μ •
if (isLoading) return <p>λ‘œλ”© 쀑...</p>;
if (error) return <p>데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 데 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€</p>;
Comment on lines +46 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

λ‘œλ”© 및 μ—λŸ¬ μƒνƒœ 처리λ₯Ό κ°œμ„ ν•΄μ£Όμ„Έμš”

ν˜„μž¬ κ΅¬ν˜„μ€ 기본적인 ν…μŠ€νŠΈλ§Œ ν‘œμ‹œν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. μ‚¬μš©μž κ²½ν—˜μ„ ν–₯μƒμ‹œν‚€κΈ° μœ„ν•΄ 더 λ‚˜μ€ UI μ»΄ν¬λ„ŒνŠΈλ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같은 κ°œμ„ μ‚¬ν•­μ„ μ œμ•ˆν•©λ‹ˆλ‹€:

  1. λ‘œλ”© μƒνƒœ: Skeleton UI λ˜λŠ” λ‘œλ”© μŠ€ν”Όλ„ˆ μ‚¬μš©
  2. μ—λŸ¬ μƒνƒœ: μ—λŸ¬ λ©”μ‹œμ§€μ™€ ν•¨κ»˜ μž¬μ‹œλ„ λ²„νŠΌ μΆ”κ°€
  3. 데이터 μ—†μŒ μƒνƒœ: μ μ ˆν•œ μ•ˆλ‚΄ λ©”μ‹œμ§€μ™€ ν•¨κ»˜ μ‹œκ°μ  ν”Όλ“œλ°± 제곡
if (isLoading) return <LoadingSpinner />;
if (error) return (
  <ErrorState 
    message="데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 데 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€" 
    onRetry={() => refetch()} 
  />
);
if (!gatheringList || gatheringList.length === 0) return (
  <EmptyState message="아직 λ“±λ‘λœ λͺ¨μž„이 μ—†μŠ΅λ‹ˆλ‹€" />
);

if (!gatheringList || gatheringList.length === 0) return <p>데이터가 μ—†μŠ΅λ‹ˆλ‹€.</p>;

return <GatheringCardCarousel gatheringData={gatheringList} crewId={id} />;
return (
<>
<GatheringCardCarousel
gatheringData={gatheringList}
crewId={id}
onLike={handleLike}
onUnlike={handleUnlike}
onShowLoginModal={() => setShowLoginModal(true)}
/>
{showLoginModal && (
<ConfirmModal
opened={showLoginModal}
onClose={() => setShowLoginModal(false)}
onConfirm={handleLoginRedirect}
>
둜그인이 ν•„μš”ν•©λ‹ˆλ‹€!
</ConfirmModal>
)}
</>
);
}
5 changes: 2 additions & 3 deletions src/app/(crew)/my-favorite/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import GatheringList from '@/src/components/gathering-list/gathering-list';
import { gatheringData } from '@/src/mock/gathering-data';
import LikedList from '@/src/components/gathering-list/liked-list-container';

export default function FavoritePage() {
return (
<div className="mt-4 md:mt-10">
<GatheringList gatheringData={gatheringData} />
<LikedList />
</div>
);
}
25 changes: 0 additions & 25 deletions src/app/api/test-api/route.ts

This file was deleted.

21 changes: 17 additions & 4 deletions src/components/common/gathering-card/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import GatheringCardPresenter from './presenter';
interface GatheringCardContainerProps extends GatheringType {
className?: string;
crewId: number;
onLike: (gatheringId: number) => Promise<void>;
onUnlike: (gatheringId: number) => Promise<void>;
}

export default function GatheringCard({
Expand All @@ -25,9 +27,10 @@ export default function GatheringCard({
liked: initialIsLiked,
className,
crewId,
onLike,
onUnlike,
}: GatheringCardContainerProps) {
const [opened, { open, close }] = useDisclosure(false);
// μž„μ‹œ μ°œν•˜κΈ°
const [isLiked, setIsLiked] = useState(initialIsLiked);

// λ‚ μ§œ 비ꡐ
Expand All @@ -42,9 +45,19 @@ export default function GatheringCard({
// 마감 μ‹œκ°„ λ¬Έμžμ—΄ 생성
const deadlineMessage = `였늘 ${gatheringDate.getHours()}μ‹œ 마감`;

// μΆ”ν›„ μ°œν•˜κΈ° μ»΄ν¬λ„ŒνŠΈ μž‘μ„±λ˜λ©΄ μˆ˜μ •
const handleLikeToggle = () => {
setIsLiked((prev) => !prev);
// μ°œν•˜κΈ° μƒνƒœ μ—…λ°μ΄νŠΈ
const handleLikeToggle = async () => {
try {
if (isLiked) {
await onUnlike(id);
setIsLiked(false);
} else {
await onLike(id);
setIsLiked(true);
}
} catch (error) {
Toast({ message: '찜 μƒνƒœλ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” 데 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.', type: 'error' });
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

μ’‹μ•„μš” ν† κΈ€ ν•Έλ“€λŸ¬μ˜ κ°œμ„ μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

ν˜„μž¬ κ΅¬ν˜„μ—μ„œ λ‹€μŒ 사항듀을 κ°œμ„ ν•˜λ©΄ 쒋을 것 κ°™μŠ΅λ‹ˆλ‹€:

  1. λ‘œλ”© μƒνƒœ 관리가 μ—†μ–΄ μ‚¬μš©μž ν”Όλ“œλ°±μ΄ λΆ€μ‘±ν•©λ‹ˆλ‹€.
  2. 비동기 μž‘μ—… 쀑 λ°œμƒν•  수 μžˆλŠ” 레이슀 μ»¨λ””μ…˜μ— μ·¨μ•½ν•©λ‹ˆλ‹€.
  3. μ—λŸ¬ μ²˜λ¦¬κ°€ λͺ¨λ“  μΌ€μ΄μŠ€λ₯Ό ν¬κ΄„μ μœΌλ‘œ 닀루지 μ•ŠμŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 κ°œμ„ ν•΄λ³΄μ„Έμš”:

+ const [isLoading, setIsLoading] = useState(false);
  const handleLikeToggle = async () => {
+   if (isLoading) return;
+   setIsLoading(true);
    try {
      if (isLiked) {
        await onUnlike(id);
-       setIsLiked(false);
+       setIsLiked(prev => !prev);
      } else {
        await onLike(id);
-       setIsLiked(true);
+       setIsLiked(prev => !prev);
      }
    } catch (error) {
+     // μ›λž˜ μƒνƒœλ‘œ 볡ꡬ
+     setIsLiked(prev => !prev);
+     if (error instanceof Error) {
+       Toast({ message: `μ’‹μ•„μš” μ—…λ°μ΄νŠΈ μ‹€νŒ¨: ${error.message}`, type: 'error' });
+     } else {
        Toast({ message: '찜 μƒνƒœλ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” 데 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.', type: 'error' });
+     }
    } finally {
+     setIsLoading(false);
    }
  };
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// μ°œν•˜κΈ° μƒνƒœ μ—…λ°μ΄νŠΈ
const handleLikeToggle = async () => {
try {
if (isLiked) {
await onUnlike(id);
setIsLiked(false);
} else {
await onLike(id);
setIsLiked(true);
}
} catch (error) {
Toast({ message: '찜 μƒνƒœλ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” 데 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.', type: 'error' });
}
const [isLoading, setIsLoading] = useState(false);
const handleLikeToggle = async () => {
if (isLoading) return;
setIsLoading(true);
try {
if (isLiked) {
await onUnlike(id);
setIsLiked(prev => !prev);
} else {
await onLike(id);
setIsLiked(prev => !prev);
}
} catch (error) {
// μ›λž˜ μƒνƒœλ‘œ 볡ꡬ
setIsLiked(prev => !prev);
if (error instanceof Error) {
Toast({ message: `μ’‹μ•„μš” μ—…λ°μ΄νŠΈ μ‹€νŒ¨: ${error.message}`, type: 'error' });
} else {
Toast({ message: '찜 μƒνƒœλ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” 데 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.', type: 'error' });
}
} finally {
setIsLoading(false);
}
};

};

const { data: gatheringData, error } = useGetGatheringDetailQuery(crewId, id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function ScheduledGatheringCardContainer({
// TODO: modalData μ—°κ²°
// const { data: modalData } = useQuery<GatheringDetailType>(useGetGatheringQuery());
const dummyModalData = {
crewId: 1,
id: 1,
title: 'μ‹ λ‚˜λŠ” μš΄λ™...즐거운..μ½”λ”©..',
introduce: 'κ³΅μ§€μ‚¬ν•­μž…λ‹ˆλ‹€. λ‹€λ“€ 이번 약속 μžŠμ§€ μ•ŠμœΌμ…¨μ£ ? κΌ­ μ°Έμ—¬ λΆ€νƒλ“œλ¦½λ‹ˆλ‹€~',
Expand Down
6 changes: 3 additions & 3 deletions src/components/common/modal/confirm-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function ConfirmModal({ children, opened, onClose, onConfirm }: C
opened={opened}
onClose={onClose}
centered
withCloseButton
withCloseButton={false}
size="xs"
styles={{
content: { boxShadow: '0 25px 50px -12px rgba(0,0,0,0.1)', borderRadius: '12px' },
Expand All @@ -46,8 +46,8 @@ export default function ConfirmModal({ children, opened, onClose, onConfirm }: C
blur: 2,
}}
>
<div className="space-y-8 text-center">
{children}
<div className="space-y-8 p-4 text-center">
<div>{children}</div>
<div className="flex justify-end">
<Button className="btn-filled w-full" onClick={handleConfirm}>
확인
Expand Down
31 changes: 30 additions & 1 deletion src/components/gathering-list/gathering-card-carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useState } from 'react';
import Image from 'next/image';
import { useAuthStore } from '@/src/store/use-auth-store';
import GatheringCard from '@/src/components/common/gathering-card/container';
import { GatheringType } from '@/src/types/gathering-data';
import IcoLeft from '@/public/assets/icons/ic-left.svg';
Expand All @@ -10,16 +11,26 @@ import IcoRight from '@/public/assets/icons/ic-right.svg';
interface GatheringCardCarouselProps {
gatheringData: GatheringType[];
crewId: number;
onLike: (gatheringId: number) => Promise<void>;
onUnlike: (gatheringId: number) => Promise<void>;
onShowLoginModal: () => void;
}

export default function CustomGatheringCardCarousel({
gatheringData,
crewId,
onLike,
onUnlike,
onShowLoginModal,
}: GatheringCardCarouselProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const [slidesToShow, setSlidesToShow] = useState(1);
const [slideSize, setSlideSize] = useState('w-full');

// 둜그인 μ—¬λΆ€ 확인
const token = useAuthStore((state) => state.token);
const isLoggedIn = !!token;

useEffect(() => {
const handleResize = () => {
const screenWidth = window.innerWidth;
Expand Down Expand Up @@ -75,7 +86,25 @@ export default function CustomGatheringCardCarousel({
>
{gatheringData.map((card) => (
<div key={card.id} className={`flex-shrink-0 ${slideSize} mb-5 lg:min-w-[362px]`}>
<GatheringCard crewId={crewId} {...card} className="w-full" />
<GatheringCard
crewId={crewId}
{...card}
className="w-full"
onLike={() => {
if (isLoggedIn) {
return onLike(card.id);
}
onShowLoginModal(); // 둜그인이 μ•ˆ λ˜μ–΄ 있으면 λͺ¨λ‹¬ ν‘œμ‹œ
return Promise.resolve();
}}
onUnlike={() => {
if (isLoggedIn) {
return onUnlike(card.id);
}
onShowLoginModal(); // 둜그인이 μ•ˆ λ˜μ–΄ 있으면 λͺ¨λ‹¬ ν‘œμ‹œ
return Promise.resolve();
}}
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

μ’‹μ•„μš”/μ’‹μ•„μš” μ·¨μ†Œ ν•Έλ“€λŸ¬ 쀑볡 μ½”λ“œ κ°œμ„  ν•„μš”

onLike와 onUnlike ν•Έλ“€λŸ¬μ˜ ꡬ쑰가 맀우 μœ μ‚¬ν•˜μ—¬ 쀑볡 μ½”λ“œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 곡톡 λ‘œμ§μ„ μΆ”μΆœν•˜μ—¬ λ¦¬νŒ©ν† λ§ν•˜λŠ” 것을 μΆ”μ²œλ“œλ¦½λ‹ˆλ‹€:

const createAuthenticatedHandler = (
  handler: (id: number) => Promise<void>
) => (id: number) => {
  if (isLoggedIn) {
    return handler(id);
  }
  onShowLoginModal();
  return Promise.resolve();
};

<GatheringCard
  crewId={crewId}
  {...card}
  className="w-full"
  onLike={createAuthenticatedHandler((id) => onLike(id))}
  onUnlike={createAuthenticatedHandler((id) => onUnlike(id))}
/>

μ΄λ ‡κ²Œ ν•˜λ©΄:

  1. μ½”λ“œ 쀑볡이 μ œκ±°λ©λ‹ˆλ‹€
  2. 인증 둜직이 ν•œ κ³³μ—μ„œ κ΄€λ¦¬λ©λ‹ˆλ‹€
  3. ν–₯ν›„ 인증 κ΄€λ ¨ 둜직 λ³€κ²½ μ‹œ μœ μ§€λ³΄μˆ˜κ°€ μš©μ΄ν•©λ‹ˆλ‹€

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

μ’‹μ•„μš”/μ’‹μ•„μš” μ·¨μ†Œ ν•Έλ“€λŸ¬μ˜ κ°œμ„ μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

ν˜„μž¬ κ΅¬ν˜„μ—μ„œ λ‹€μŒκ³Ό 같은 λ¬Έμ œμ λ“€μ΄ λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€:

  1. μ’‹μ•„μš”μ™€ μ’‹μ•„μš” μ·¨μ†Œ ν•Έλ“€λŸ¬μ— 쀑볡 μ½”λ“œκ°€ μ‘΄μž¬ν•©λ‹ˆλ‹€.
  2. λ‘œκ·ΈμΈν•˜μ§€ μ•Šμ€ μƒνƒœμ—μ„œ Promise.resolve()λ₯Ό λ°˜ν™˜ν•˜λŠ” 것은 μƒμœ„ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ˜ˆμƒμΉ˜ λͺ»ν•œ λ™μž‘μ„ μΌμœΌν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.
  3. μ—λŸ¬ μ²˜λ¦¬κ°€ λˆ„λ½λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같은 κ°œμ„ λœ κ΅¬ν˜„μ„ μ œμ•ˆν•©λ‹ˆλ‹€:

+const createAuthenticatedHandler = (
+  action: (id: number) => Promise<void>,
+  gatheringId: number,
+  showLoginModal: () => void,
+) => async () => {
+  if (!isLoggedIn) {
+    showLoginModal();
+    return;
+  }
+  
+  try {
+    await action(gatheringId);
+  } catch (error) {
+    console.error('Failed to process action:', error);
+    // μ—λŸ¬ 처리 둜직 μΆ”κ°€ ν•„μš”
+  }
+};

 <GatheringCard
   crewId={crewId}
   {...card}
   className="w-full"
-  onLike={() => {
-    if (isLoggedIn) {
-      return onLike(card.id);
-    }
-    onShowLoginModal();
-    return Promise.resolve();
-  }}
-  onUnlike={() => {
-    if (isLoggedIn) {
-      return onUnlike(card.id);
-    }
-    onShowLoginModal();
-    return Promise.resolve();
-  }}
+  onLike={createAuthenticatedHandler(onLike, card.id, onShowLoginModal)}
+  onUnlike={createAuthenticatedHandler(onUnlike, card.id, onShowLoginModal)}
 />
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<GatheringCard
crewId={crewId}
{...card}
className="w-full"
onLike={() => {
if (isLoggedIn) {
return onLike(card.id);
}
onShowLoginModal(); // 둜그인이 μ•ˆ λ˜μ–΄ 있으면 λͺ¨λ‹¬ ν‘œμ‹œ
return Promise.resolve();
}}
onUnlike={() => {
if (isLoggedIn) {
return onUnlike(card.id);
}
onShowLoginModal(); // 둜그인이 μ•ˆ λ˜μ–΄ 있으면 λͺ¨λ‹¬ ν‘œμ‹œ
return Promise.resolve();
}}
/>
<GatheringCard
crewId={crewId}
{...card}
className="w-full"
onLike={createAuthenticatedHandler(onLike, card.id, onShowLoginModal)}
onUnlike={createAuthenticatedHandler(onUnlike, card.id, onShowLoginModal)}
/>

</div>
))}
</div>
Expand Down
Loading