Skip to content
Merged
6 changes: 6 additions & 0 deletions constants/default-images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// 나중에 회원 프로필, 모임 디폴트 시안 나오면 각각 '/public/default-images/' 경로에서 지정
export const DEFAULT_PROFILE_IMAGE =
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D';

export const DEFAULT_GROUP_IMAGE =
'https://images.unsplash.com/photo-1705599359461-f99dc9e80efa?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D';
Comment on lines +1 to +6
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

외부 URL 의존성에 대한 리스크를 추적하세요.

현재 Unsplash 외부 URL을 사용하고 있어, 해당 이미지가 삭제되거나 변경될 경우 문제가 발생할 수 있습니다. 주석에서 언급한 대로 로컬 이미지로 교체하는 작업을 이슈로 추적하는 것을 권장합니다.

이 작업을 추적하기 위한 이슈를 생성해 드릴까요?

🤖 Prompt for AI Agents
In constants/default-images.ts lines 1-6, the file uses external Unsplash URLs
which creates a runtime dependency risk if those images are removed/changed;
create an issue to replace these with local assets under /public/default-images
and update the constants to point to those local paths (e.g.,
/public/default-images/default-profile.png and
/public/default-images/default-group.png), and add a fallback strategy (serve a
bundled placeholder or log a warning) if local files are missing; include the
issue description, acceptance criteria, and assign to the frontend/assets owner
so the images are added and the constants updated.

3 changes: 1 addition & 2 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ const nextConfig: NextConfig = {
remotePatterns: [
{
protocol: 'https',
hostname: 'sprint-fe-project.s3.ap-northeast-2.amazonaws.com',
port: '',
hostname: 'we-go-bucket.s3.ap-northeast-2.amazonaws.com',
pathname: '/**',
},
{
Expand Down
34 changes: 22 additions & 12 deletions src/api/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { notFound, redirect } from 'next/navigation';

import axios from 'axios';

import { CommonErrorResponse, CommonSuccessResponse } from '@/types/service/common';
Expand Down Expand Up @@ -37,16 +35,6 @@ baseAPI.interceptors.response.use(
return response;
},
async (error) => {
const status = error.response?.status;
if (status) {
if (status === 401) {
redirect('/signin?error=unauthorized');
}
if (status === 404) {
notFound();
}
}

const errorResponse: CommonErrorResponse = error.response?.data || {
type: 'about:blank',
title: 'Network Error',
Expand All @@ -56,6 +44,28 @@ baseAPI.interceptors.response.use(
errorCode: 'NETWORK_ERROR',
};

const status = error.response?.status ?? errorResponse.status;
const isServer = typeof window === 'undefined';

if (status === 401) {
if (isServer) {
const { redirect } = await import('next/navigation');
redirect('/login');
} else {
if (window.location.pathname === '/login') {
throw errorResponse;
}
const currentPath = window.location.pathname + window.location.search;
window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`;
}
}
Comment on lines +50 to +61
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

클라이언트 측 401 처리 시 코드 흐름 문제가 있습니다.

window.location.href 할당 후에도 코드가 계속 실행되어 line 69의 throw errorResponse가 실행됩니다. 리다이렉트가 비동기적으로 발생하기 때문에, 불필요한 에러가 throw될 수 있습니다.

      } else {
        if (window.location.pathname === '/login') {
          throw errorResponse;
        }
        const currentPath = window.location.pathname + window.location.search;
        window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`;
+       return; // 리다이렉트 후 추가 실행 방지
      }
    }
📝 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
if (status === 401) {
if (isServer) {
const { redirect } = await import('next/navigation');
redirect('/login');
} else {
if (window.location.pathname === '/login') {
throw errorResponse;
}
const currentPath = window.location.pathname + window.location.search;
window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`;
}
}
if (status === 401) {
if (isServer) {
const { redirect } = await import('next/navigation');
redirect('/login');
} else {
if (window.location.pathname === '/login') {
throw errorResponse;
}
const currentPath = window.location.pathname + window.location.search;
window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`;
return; // 리다이렉트 후 추가 실행 방지
}
}
🤖 Prompt for AI Agents
In src/api/core/index.ts around lines 50 to 61, the client-side 401 branch
assigns window.location.href but execution continues and later reaches the throw
at line 69; stop further execution after initiating the redirect by immediately
returning (or using window.location.replace and then returning) right after
setting window.location.href so the subsequent throw/error logic is not executed
on the client.

if (status === 404) {
if (isServer) {
const { notFound } = await import('next/navigation');
notFound();
}
}

throw errorResponse;
},
);
Expand Down
16 changes: 14 additions & 2 deletions src/api/service/group-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { api } from '@/api/core';
import {
CreateGroupPayload,
CreateGroupResponse,
GetGroupDetailsPayload,
GetGroupDetailsResponse,
GetGroupsPayload,
GetGroupsResponse,
GetMyGroupsPayload,
GetMyGroupsResponse,
GroupIdPayload,
PreUploadGroupImagePayload,
PreUploadGroupImageResponse,
} from '@/types/service/group';
Expand Down Expand Up @@ -69,7 +69,19 @@ export const groupServiceRemote = () => ({
return api.post<CreateGroupResponse>('/groups/create', payload);
},

getGroupDetails: (payload: GetGroupDetailsPayload) => {
getGroupDetails: (payload: GroupIdPayload) => {
return api.get<GetGroupDetailsResponse>(`/groups/${payload.groupId}`);
},

attendGroup: (payload: GroupIdPayload) => {
return api.post<GetGroupDetailsResponse>(`/groups/${payload.groupId}/attend`);
},

cancelGroup: (payload: GroupIdPayload) => {
return api.post<GetGroupDetailsResponse>(`/groups/${payload.groupId}/cancel`);
},

deleteGroup: (payload: GroupIdPayload) => {
return api.delete(`/groups/${payload.groupId}`);
},
});
98 changes: 3 additions & 95 deletions src/app/meetup/[groupId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,103 +25,11 @@ const MeetupDetailPage = ({ params }: Props) => {
return (
<div>
<MeetupBannerImages images={data.images} />
<MeetupDescriptions description={DUMMY_MEETUP_DATA.description} />
<MeetupMembers members={DUMMY_MEETUP_DATA.members} />
<MeetupButtons
members={DUMMY_MEETUP_DATA.members}
ownerInfo={DUMMY_MEETUP_DATA.description.ownerInfo}
progress={DUMMY_MEETUP_DATA.description.progress}
/>
<MeetupDescriptions descriptions={data} />
<MeetupMembers members={data.joinedMembers} />
<MeetupButtons conditions={data} groupId={groupId} />
</div>
);
};

export default MeetupDetailPage;

// 바인딩 테스트용 더미 데이터임 (무시하세요)
export const DUMMY_MEETUP_DATA = {
bannerImages: [
'https://images.unsplash.com/photo-1546512347-3ad629c193c5?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1527529482837-4698179dc6ce?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://plus.unsplash.com/premium_photo-1700554043895-6e87a46a2b21?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
],
description: {
ownerInfo: {
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'Dummy Name',
bio: 'Some Dummy Bio Message',
},
title: '동탄 호수공원에서 피크닉하실 분 구해요!',
tags: ['동탄2', '피크닉', '노가리'],
content:
'동탄 호수공원에서 가볍게 피그닉 하실 분을 기다리고 있어요! 간단히 먹거리 나누고, 잔디밭에서 편하게 이야기하며 쉬는 느낌의 소규모 모임입니다.\n혼자 오셔도 편하게 어울릴 수 있어요!',
setting: {
location: '화성시 산척동',
date: '25.11.28',
time: '10:00',
},
progress: {
current: 9,
max: 12,
},
createdAt: '30분 전',
},
members: [
{
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'UserNickName01',
},
{
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'UserNickName02',
},
{
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'UserNickName03',
},
{
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'UserNickName04',
},
{
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'UserNickName05',
},
{
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'UserNickName06',
},
{
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'UserNickName07',
},
{
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'UserNickName08',
},
{
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'UserNickName09',
},
{
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'UserNickName10',
},
{
profileImage:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
name: 'UserNickName11',
},
],
};
2 changes: 1 addition & 1 deletion src/app/schedule/(components)/meeting-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const MeetingList = ({
{meetings.map((meeting) => (
<Card
key={meeting.id}
dateTime={formatDateTime(meeting.startTime, meeting.endTime)}
dateTime={formatDateTime(meeting.startTime)}
images={meeting.images}
leaveAndChatActions={
showActions
Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/meetup/meetup-banner-images/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface Props {
}

export const MeetupBannerImages = ({ images }: Props) => {
const hasImages = images.length;
const hasImages = Boolean(images.length);

const defaultImageUrl =
'https://images.unsplash.com/photo-1705599359461-f99dc9e80efa?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D';
Expand Down
77 changes: 44 additions & 33 deletions src/components/pages/meetup/meetup-buttons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,33 @@

import { useRouter } from 'next/navigation';

import { useState } from 'react';
import { useEffect, useState } from 'react';

import Cookies from 'js-cookie';

import { MeetupModal } from '@/components/pages/meetup/meetup-modal';
import { Button } from '@/components/ui/button';
import { useModal } from '@/components/ui/modal';
import { GetGroupDetailsResponse } from '@/types/service/group';

interface Props {
progress: {
current: number;
max: number;
};
ownerInfo: {
name: string;
};
members: {
name: string;
}[];
conditions: Pick<
GetGroupDetailsResponse,
'userStatus' | 'createdBy' | 'participantCount' | 'maxParticipants'
>;
groupId: string;
}

export const MeetupButtons = ({ progress: { current, max }, ownerInfo }: Props) => {
const [isJoined, _] = useState(true);
export const MeetupButtons = ({
conditions: {
userStatus: { isJoined },
createdBy,
participantCount,
maxParticipants,
},
groupId,
}: Props) => {
const [isHost, setIsHost] = useState<boolean | null>(null);
const { open } = useModal();
const { push } = useRouter();

Expand All @@ -31,36 +37,41 @@ export const MeetupButtons = ({ progress: { current, max }, ownerInfo }: Props)
push('/message/id');
};

// 방 주인이냐
const isOwner = ownerInfo.name === '본인 계정 닉네임 ㅇㅇ';
useEffect(() => {
const sessionId = Number(Cookies.get('userId'));
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsHost(sessionId === createdBy.userId);
}, [createdBy]);
Comment on lines +40 to +44
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

isHost 상태 초기화 및 쿠키 파싱 관련 문제가 있습니다.

  1. Cookies.get('userId')undefined를 반환할 수 있으며, Number(undefined)NaN이 됩니다.
  2. isHostnull인 초기 렌더링 시점에 Line 54의 isHost ? 'delete' : 'cancel''cancel'로 평가됩니다.
  useEffect(() => {
-   const sessionId = Number(Cookies.get('userId'));
-   // eslint-disable-next-line react-hooks/set-state-in-effect
-   setIsHost(sessionId === createdBy.userId);
+   const userIdCookie = Cookies.get('userId');
+   if (userIdCookie) {
+     const sessionId = Number(userIdCookie);
+     setIsHost(sessionId === createdBy.userId);
+   } else {
+     setIsHost(false);
+   }
  }, [createdBy]);
🤖 Prompt for AI Agents
In src/components/pages/meetup/meetup-buttons/index.tsx around lines 40-44, the
effect blindly does Number(Cookies.get('userId')) which can produce NaN and you
currently initialize isHost to null causing the early render to treat the
ternary as 'cancel'; fix by parsing the cookie safely (e.g. read cookie string,
use parseInt and check isNaN) and only setIsHost when the parsed sessionId is a
valid number, and also initialize isHost to false (or compute synchronously from
the cookie) so the initial render doesn't treat null as falsy; ensure the effect
compares the numeric sessionId with createdBy.userId (or createdBy.userId as
Number) and update the dependency list appropriately.


// 모임의 참가자라면 -> (모임 탈퇴 + 채팅 입장) 버튼
// 본인이 생성한 방이면 -> (모임 취소 + 채팅 입장) 버튼
if (isJoined) {
return (
<div className='sticky bottom-[56px] border-t-1 border-gray-200 bg-white px-4 py-3'>
return (
<div className='sticky bottom-[56px] border-t-1 border-gray-200 bg-white px-4 py-3'>
{isJoined ? (
<div className='flex gap-[10px]'>
<Button
className='flex-[1.2]'
variant='tertiary'
onClick={() => open(<MeetupModal type={isOwner ? 'cancel' : 'leave'} />)}
onClick={() =>
open(<MeetupModal groupId={groupId} type={isHost ? 'delete' : 'cancel'} />)
}
>
{isOwner ? '모임 취소' : '모임 탈퇴'}
{isHost ? '모임 취소' : '모임 탈퇴'}
</Button>
<Button className='flex-2' disabled={current >= max} onClick={onEnterChatClick}>
<Button
className='flex-2'
disabled={participantCount >= maxParticipants}
onClick={onEnterChatClick}
>
채팅 입장
</Button>
</div>
</div>
);
}

// 방 주인 아니고 참가자도 아닐때 -> 참여 버튼
return (
<div className='sticky bottom-[56px] border-t-1 border-gray-200 bg-white px-4 py-3'>
<Button disabled={current >= max} onClick={() => open(<MeetupModal type='join' />)}>
참여하기
</Button>
) : (
<Button
disabled={participantCount >= maxParticipants}
onClick={() => open(<MeetupModal groupId={groupId} type='attend' />)}
>
참여하기
</Button>
)}
</div>
);
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { GetGroupDetailsResponse } from '@/types/service/group';

interface Props {
detail: GetGroupDetailsResponse['description'];
}

export const DescriptionDetail = ({ detail }: Props) => {
return (
<div className='mt-6'>
<p className='text-text-md-regular break-keep text-gray-800'>{detail}</p>
</div>
);
};
Loading