diff --git a/constants/default-images.ts b/constants/default-images.ts new file mode 100644 index 00000000..2db1b2e8 --- /dev/null +++ b/constants/default-images.ts @@ -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'; diff --git a/next.config.ts b/next.config.ts index b882f61f..0a3dfb23 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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: '/**', }, { diff --git a/src/api/core/index.ts b/src/api/core/index.ts index 51c9e952..1b3310f2 100644 --- a/src/api/core/index.ts +++ b/src/api/core/index.ts @@ -1,5 +1,3 @@ -import { notFound, redirect } from 'next/navigation'; - import axios from 'axios'; import { CommonErrorResponse, CommonSuccessResponse } from '@/types/service/common'; @@ -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', @@ -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)}`; + } + } + if (status === 404) { + if (isServer) { + const { notFound } = await import('next/navigation'); + notFound(); + } + } + throw errorResponse; }, ); diff --git a/src/api/service/group-service/index.ts b/src/api/service/group-service/index.ts index 0f364afb..d6af4e22 100644 --- a/src/api/service/group-service/index.ts +++ b/src/api/service/group-service/index.ts @@ -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'; @@ -69,7 +69,19 @@ export const groupServiceRemote = () => ({ return api.post('/groups/create', payload); }, - getGroupDetails: (payload: GetGroupDetailsPayload) => { + getGroupDetails: (payload: GroupIdPayload) => { return api.get(`/groups/${payload.groupId}`); }, + + attendGroup: (payload: GroupIdPayload) => { + return api.post(`/groups/${payload.groupId}/attend`); + }, + + cancelGroup: (payload: GroupIdPayload) => { + return api.post(`/groups/${payload.groupId}/cancel`); + }, + + deleteGroup: (payload: GroupIdPayload) => { + return api.delete(`/groups/${payload.groupId}`); + }, }); diff --git a/src/app/meetup/[groupId]/page.tsx b/src/app/meetup/[groupId]/page.tsx index e030095c..9b9d3394 100644 --- a/src/app/meetup/[groupId]/page.tsx +++ b/src/app/meetup/[groupId]/page.tsx @@ -25,103 +25,11 @@ const MeetupDetailPage = ({ params }: Props) => { return (
- - - + + +
); }; 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', - }, - ], -}; diff --git a/src/app/schedule/(components)/meeting-list.tsx b/src/app/schedule/(components)/meeting-list.tsx index 5b57880e..5547c84a 100644 --- a/src/app/schedule/(components)/meeting-list.tsx +++ b/src/app/schedule/(components)/meeting-list.tsx @@ -38,7 +38,7 @@ export const MeetingList = ({ {meetings.map((meeting) => ( { - 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'; diff --git a/src/components/pages/meetup/meetup-buttons/index.tsx b/src/components/pages/meetup/meetup-buttons/index.tsx index f11b1eaa..91aa6338 100644 --- a/src/components/pages/meetup/meetup-buttons/index.tsx +++ b/src/components/pages/meetup/meetup-buttons/index.tsx @@ -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(null); const { open } = useModal(); const { push } = useRouter(); @@ -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]); - // 모임의 참가자라면 -> (모임 탈퇴 + 채팅 입장) 버튼 - // 본인이 생성한 방이면 -> (모임 취소 + 채팅 입장) 버튼 - if (isJoined) { - return ( -
+ return ( +
+ {isJoined ? (
-
-
- ); - } - - // 방 주인 아니고 참가자도 아닐때 -> 참여 버튼 - return ( -
- + ) : ( + + )}
); }; diff --git a/src/components/pages/meetup/meetup-descriptions/description-sections/description-content/index.tsx b/src/components/pages/meetup/meetup-descriptions/description-sections/description-content/index.tsx deleted file mode 100644 index 9a35ca65..00000000 --- a/src/components/pages/meetup/meetup-descriptions/description-sections/description-content/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -interface Props { - content: string; -} - -export const DescriptionContent = ({ content }: Props) => { - return ( -
-

{content}

-
- ); -}; diff --git a/src/components/pages/meetup/meetup-descriptions/description-sections/description-detail/index.tsx b/src/components/pages/meetup/meetup-descriptions/description-sections/description-detail/index.tsx new file mode 100644 index 00000000..ca871f8e --- /dev/null +++ b/src/components/pages/meetup/meetup-descriptions/description-sections/description-detail/index.tsx @@ -0,0 +1,13 @@ +import { GetGroupDetailsResponse } from '@/types/service/group'; + +interface Props { + detail: GetGroupDetailsResponse['description']; +} + +export const DescriptionDetail = ({ detail }: Props) => { + return ( +
+

{detail}

+
+ ); +}; diff --git a/src/components/pages/meetup/meetup-descriptions/description-sections/description-profile/index.tsx b/src/components/pages/meetup/meetup-descriptions/description-sections/description-profile/index.tsx index da3dbe85..a1d46623 100644 --- a/src/components/pages/meetup/meetup-descriptions/description-sections/description-profile/index.tsx +++ b/src/components/pages/meetup/meetup-descriptions/description-sections/description-profile/index.tsx @@ -1,28 +1,31 @@ import Image from 'next/image'; import Link from 'next/link'; +import { DEFAULT_PROFILE_IMAGE } from 'constants/default-images'; + +import { GetGroupDetailsResponse } from '@/types/service/group'; + interface Props { - profileImage: string; - name: string; - bio: string; + hostInfo: GetGroupDetailsResponse['createdBy']; } -export const DescriptionProfile = ({ profileImage, name, bio }: Props) => { +export const DescriptionProfile = ({ hostInfo: { nickName, profileImage, userId } }: Props) => { return ( -
- +
+ 프로필 사진
-

{name}

-

{bio}

+

{nickName}

+

some dummy bio text

diff --git a/src/components/pages/meetup/meetup-descriptions/description-sections/description-progress/index.tsx b/src/components/pages/meetup/meetup-descriptions/description-sections/description-progress/index.tsx index 9cd0435e..6d45ec62 100644 --- a/src/components/pages/meetup/meetup-descriptions/description-sections/description-progress/index.tsx +++ b/src/components/pages/meetup/meetup-descriptions/description-sections/description-progress/index.tsx @@ -1,21 +1,27 @@ +import { formatTimeAgo } from '@/lib/formatDateTime'; +import { GetGroupDetailsResponse } from '@/types/service/group'; + interface Props { progress: { - current: number; - max: number; + maxParticipants: GetGroupDetailsResponse['maxParticipants']; + participantCount: GetGroupDetailsResponse['participantCount']; }; createdAt: string; } -export const DescriptionProgress = ({ progress: { current, max }, createdAt }: Props) => { - const progressRate = Math.ceil((current / max) * 100); +export const DescriptionProgress = ({ + progress: { maxParticipants, participantCount }, + createdAt, +}: Props) => { + const progressRate = Math.ceil((participantCount / maxParticipants) * 100); return ( -
+

참여 인원

- {current}/{max} + {participantCount}/{maxParticipants}
@@ -24,7 +30,7 @@ export const DescriptionProgress = ({ progress: { current, max }, createdAt }: P
-

{createdAt}

+

{formatTimeAgo(createdAt)}

); diff --git a/src/components/pages/meetup/meetup-descriptions/description-sections/description-setting/index.tsx b/src/components/pages/meetup/meetup-descriptions/description-sections/description-setting/index.tsx index e29583c1..4bf8d3ce 100644 --- a/src/components/pages/meetup/meetup-descriptions/description-sections/description-setting/index.tsx +++ b/src/components/pages/meetup/meetup-descriptions/description-sections/description-setting/index.tsx @@ -1,12 +1,12 @@ import { Icon } from '@/components/icon'; +import { formatDateTime } from '@/lib/formatDateTime'; +import { GetGroupDetailsResponse } from '@/types/service/group'; interface Props { - location: string; - date: string; - time: string; + setting: Pick; } -export const DescriptionSetting = ({ location, date, time }: Props) => { +export const DescriptionSetting = ({ setting: { location, startTime } }: Props) => { return (
    @@ -16,9 +16,7 @@ export const DescriptionSetting = ({ location, date, time }: Props) => {
  • -

    - {date} - {time} -

    +

    {formatDateTime(startTime)}

diff --git a/src/components/pages/meetup/meetup-descriptions/description-sections/description-tags/index.tsx b/src/components/pages/meetup/meetup-descriptions/description-sections/description-tags/index.tsx index d3094526..3648486a 100644 --- a/src/components/pages/meetup/meetup-descriptions/description-sections/description-tags/index.tsx +++ b/src/components/pages/meetup/meetup-descriptions/description-sections/description-tags/index.tsx @@ -1,8 +1,14 @@ +import { GetGroupDetailsResponse } from '@/types/service/group'; + interface Props { - tags: string[]; + tags: GetGroupDetailsResponse['tags']; } export const DescriptionTags = ({ tags }: Props) => { + const hasTags = Boolean(tags.length); + + if (!hasTags) return null; + return (
{tags.map((tag) => ( diff --git a/src/components/pages/meetup/meetup-descriptions/description-sections/description-title/index.tsx b/src/components/pages/meetup/meetup-descriptions/description-sections/description-title/index.tsx index 7075b29d..d2dc5434 100644 --- a/src/components/pages/meetup/meetup-descriptions/description-sections/description-title/index.tsx +++ b/src/components/pages/meetup/meetup-descriptions/description-sections/description-title/index.tsx @@ -1,11 +1,13 @@ +import { GetGroupDetailsResponse } from '@/types/service/group'; + interface Props { - title: string; + title: GetGroupDetailsResponse['title']; } export const DescriptionTitle = ({ title }: Props) => { return (
-

{title}

+

{title}

); }; diff --git a/src/components/pages/meetup/meetup-descriptions/description-sections/index.ts b/src/components/pages/meetup/meetup-descriptions/description-sections/index.ts index b7a1881c..6f5ff7ea 100644 --- a/src/components/pages/meetup/meetup-descriptions/description-sections/index.ts +++ b/src/components/pages/meetup/meetup-descriptions/description-sections/index.ts @@ -1,4 +1,4 @@ -export { DescriptionContent } from './description-content'; +export { DescriptionDetail } from './description-detail'; export { DescriptionProfile } from './description-profile'; export { DescriptionProgress } from './description-progress'; export { DescriptionSetting } from './description-setting'; diff --git a/src/components/pages/meetup/meetup-descriptions/index.tsx b/src/components/pages/meetup/meetup-descriptions/index.tsx index 7842dcfe..b17c3e1b 100644 --- a/src/components/pages/meetup/meetup-descriptions/index.tsx +++ b/src/components/pages/meetup/meetup-descriptions/index.tsx @@ -1,28 +1,49 @@ -import type { DUMMY_MEETUP_DATA } from '@/app/meetup/[groupId]/page'; import { - DescriptionContent, + DescriptionDetail, DescriptionProfile, DescriptionProgress, DescriptionSetting, DescriptionTags, DescriptionTitle, } from '@/components/pages/meetup/meetup-descriptions/description-sections'; +import { GetGroupDetailsResponse } from '@/types/service/group'; interface Props { - description: typeof DUMMY_MEETUP_DATA.description; + descriptions: Pick< + GetGroupDetailsResponse, + | 'createdBy' + | 'createdAt' + | 'title' + | 'tags' + | 'description' + | 'location' + | 'startTime' + | 'maxParticipants' + | 'participantCount' + >; } export const MeetupDescriptions = ({ - description: { ownerInfo, title, tags, content, setting, progress, createdAt }, + descriptions: { + createdBy, + createdAt, + title, + tags, + description, + location, + startTime, + maxParticipants, + participantCount, + }, }: Props) => { return (
- + - - - + + +
); }; diff --git a/src/components/pages/meetup/meetup-members/index.tsx b/src/components/pages/meetup/meetup-members/index.tsx index 54e21539..93f5e19a 100644 --- a/src/components/pages/meetup/meetup-members/index.tsx +++ b/src/components/pages/meetup/meetup-members/index.tsx @@ -7,48 +7,53 @@ import { useState } from 'react'; import clsx from 'clsx'; -import type { DUMMY_MEETUP_DATA } from '@/app/meetup/[groupId]/page'; import { Icon } from '@/components/icon'; +import { AnimateDynamicHeight } from '@/components/shared'; import { Button } from '@/components/ui'; +import { GetGroupDetailsResponse } from '@/types/service/group'; interface Props { - members: typeof DUMMY_MEETUP_DATA.members; + members: GetGroupDetailsResponse['joinedMembers']; } export const MeetupMembers = ({ members }: Props) => { - const [showMore, setShowMore] = useState(false); + const [expand, setExpand] = useState(false); const [coverMember, setCoverMember] = useState(2 < Math.ceil(members.length / 3)); const hasMoreMember = 2 < Math.ceil(members.length / 3); - const onShowMoreClick = () => { - setShowMore((prev) => !prev); + const defaultProfileImageUrl = + 'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?q=80&w=717&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'; + + const onExpandClick = () => { + setExpand((prev) => !prev); setCoverMember((prev) => !prev); }; return ( -
+

참여자 정보

-
-
    - {members.map(({ name, profileImage }, idx) => ( + +
      + {members.map(({ nickName, profileImage, userId }, idx) => (
    • -
      - +
      + 프로필 사진

      { coverMember && idx === 5 ? 'hidden' : 'block', )} > - {name} + {nickName}

      @@ -71,7 +76,7 @@ export const MeetupMembers = ({ members }: Props) => {
    • ))}
    -
+ {hasMoreMember && (
@@ -79,10 +84,10 @@ export const MeetupMembers = ({ members }: Props) => { className='flex-center h-9 w-auto border-none bg-gray-50 px-4' size='sm' variant='secondary' - onClick={onShowMoreClick} + onClick={onExpandClick} > - {showMore ? '접기' : '더보기'} - + {expand ? '접기' : '더보기'} +
)} diff --git a/src/components/pages/meetup/meetup-modal/index.tsx b/src/components/pages/meetup/meetup-modal/index.tsx index 7cf1d2cd..6427a642 100644 --- a/src/components/pages/meetup/meetup-modal/index.tsx +++ b/src/components/pages/meetup/meetup-modal/index.tsx @@ -1,66 +1,81 @@ 'use client'; -import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Button } from '@/components/ui'; import { ModalContent, ModalDescription, ModalTitle, useModal } from '@/components/ui/modal'; +import { useAttendGroup } from '@/hooks/use-group/use-group-attend'; +import { useCancelGroup } from '@/hooks/use-group/use-group-cancel'; +import { useDeleteGroup } from '@/hooks/use-group/use-group-delete'; interface Props { - type: 'join' | 'leave' | 'cancel'; + type: 'attend' | 'cancel' | 'delete'; + groupId: string; } -export const MeetupModal = ({ type }: Props) => { +export const MeetupModal = ({ type, groupId }: Props) => { + const { replace } = useRouter(); const { close } = useModal(); - const [isPending, setIsPending] = useState(false); + const { mutate: attendMutate, isPending: isAttending } = useAttendGroup({ groupId }, close); + const { mutate: cancelMutate, isPending: isCanceling } = useCancelGroup({ groupId }, close); + const { mutate: deleteMutate, isPending: isDeleting } = useDeleteGroup({ groupId }, () => { + close(); + replace('/'); + }); - const handleConfirm = () => { - setIsPending(true); - - setTimeout(() => { - setIsPending(false); - close(); - }, 1000); - }; + const isPending = isAttending || isCanceling || isDeleting; const { title, description, confirm } = MODAL_MESSAGE[type]; + const handleConfirmClick = () => { + if (type === 'attend') attendMutate(); + else if (type === 'cancel') cancelMutate(); + else if (type === 'delete') deleteMutate(); + }; + return ( - - {title} - {description} -
- - +
); }; const MODAL_MESSAGE = { - join: { + attend: { title: '모임에 참여하시겠어요?', description: '참여 후 바로 그룹채팅에 참여할 수 있어요!', confirm: '참여하기', + onConfirm: (attendMutate: () => void) => attendMutate(), }, - leave: { + cancel: { title: '모임을 정말 탈퇴하시겠어요?', description: '탈퇴 시 그룹채팅과 모임 활동이 종료돼요.', confirm: '탈퇴하기', + onConfirm: (cancelMutate: () => void) => cancelMutate(), }, - cancel: { + delete: { title: '모임을 정말 취소하시겠어요?', description: '취소 후에는 다시 복구할 수 없어요.', confirm: '취소하기', + onConfirm: (deleteMutate: () => void) => deleteMutate(), }, }; diff --git a/src/components/pages/post-meetup/fields/date-field/index.tsx b/src/components/pages/post-meetup/fields/date-field/index.tsx index b62a188f..47070cc2 100644 --- a/src/components/pages/post-meetup/fields/date-field/index.tsx +++ b/src/components/pages/post-meetup/fields/date-field/index.tsx @@ -7,6 +7,7 @@ import { Icon } from '@/components/icon'; import { DatePickerModal } from '@/components/pages/post-meetup/modals/date-picker-modal'; import { Label } from '@/components/ui'; import { useModal } from '@/components/ui'; +import { formatDateTime } from '@/lib/formatDateTime'; interface Props { field: AnyFieldApi; @@ -14,7 +15,9 @@ interface Props { export const MeetupDateField = ({ field }: Props) => { const { open } = useModal(); - const formattedDate = formatDate(new Date(field.state.value), 'YY.MM.DD - HH:mm'); + + const hasValue = Boolean(field.state.value); + const formattedDate = formatDateTime(field.state.value); const onInputClick = () => { open(); @@ -39,31 +42,12 @@ export const MeetupDateField = ({ field }: Props) => {

- {formattedDate ? formattedDate : '날짜와 시간을 선택해주세요'} + {hasValue ? formattedDate : '날짜와 시간을 선택해주세요'}

); }; - -const formatDate = (date: Date, formatString: string) => { - if (isNaN(date.getTime())) return false; - - const year = date.getFullYear().toString().substring(2, 4); - const month = (date.getMonth() + 1).toString().padStart(2, '0'); - const day = date.getDate().toString().padStart(2, '0'); - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - const seconds = date.getSeconds().toString().padStart(2, '0'); - - return formatString - .replace(/YY/g, year) - .replace(/MM/g, month) - .replace(/DD/g, day) - .replace(/HH/g, hours) - .replace(/mm/g, minutes) - .replace(/ss/g, seconds); -}; diff --git a/src/hooks/use-group/use-group-attend/index.ts b/src/hooks/use-group/use-group-attend/index.ts new file mode 100644 index 00000000..112729e9 --- /dev/null +++ b/src/hooks/use-group/use-group-attend/index.ts @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { API } from '@/api'; +import { groupKeys } from '@/lib/query-key/query-key-group'; +import { GroupIdPayload } from '@/types/service/group'; + +export const useAttendGroup = (payload: GroupIdPayload, callback: () => void) => { + const queryClient = useQueryClient(); + + const query = useMutation({ + mutationFn: () => API.groupService.attendGroup(payload), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: groupKeys.detail(payload.groupId) }); + callback(); + console.log('모임 참여 성공.'); + }, + onError: () => { + console.log('모임 참여 실패.'); + }, + }); + return query; +}; diff --git a/src/hooks/use-group/use-group-cancel/index.ts b/src/hooks/use-group/use-group-cancel/index.ts new file mode 100644 index 00000000..f217ca86 --- /dev/null +++ b/src/hooks/use-group/use-group-cancel/index.ts @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { API } from '@/api'; +import { groupKeys } from '@/lib/query-key/query-key-group'; +import { GroupIdPayload } from '@/types/service/group'; + +export const useCancelGroup = (payload: GroupIdPayload, callback: () => void) => { + const queryClient = useQueryClient(); + + const query = useMutation({ + mutationFn: () => API.groupService.cancelGroup(payload), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: groupKeys.detail(payload.groupId) }); + callback(); + console.log('모임 탈퇴 성공.'); + }, + onError: () => { + console.log('모임 탈퇴 실패.'); + }, + }); + return query; +}; diff --git a/src/hooks/use-group/use-group-delete/index.ts b/src/hooks/use-group/use-group-delete/index.ts new file mode 100644 index 00000000..224b5d9a --- /dev/null +++ b/src/hooks/use-group/use-group-delete/index.ts @@ -0,0 +1,18 @@ +import { useMutation } from '@tanstack/react-query'; + +import { API } from '@/api'; +import { GroupIdPayload } from '@/types/service/group'; + +export const useDeleteGroup = (payload: GroupIdPayload, callback: () => void) => { + const query = useMutation({ + mutationFn: () => API.groupService.deleteGroup(payload), + onSuccess: async () => { + console.log('모임 삭제 성공.'); + callback(); + }, + onError: () => { + console.log('모임 삭제 실패.'); + }, + }); + return query; +}; diff --git a/src/hooks/use-group/use-group-get-details/index.ts b/src/hooks/use-group/use-group-get-details/index.ts index 6bacbf02..6499b965 100644 --- a/src/hooks/use-group/use-group-get-details/index.ts +++ b/src/hooks/use-group/use-group-get-details/index.ts @@ -2,9 +2,9 @@ import { useQuery } from '@tanstack/react-query'; import { API } from '@/api'; import { groupKeys } from '@/lib/query-key/query-key-group'; -import { GetGroupDetailsPayload } from '@/types/service/group'; +import { GroupIdPayload } from '@/types/service/group'; -export const useGetGroupDetails = (payload: GetGroupDetailsPayload) => { +export const useGetGroupDetails = (payload: GroupIdPayload) => { const query = useQuery({ queryKey: groupKeys.detail(payload.groupId), queryFn: () => API.groupService.getGroupDetails(payload), diff --git a/src/hooks/use-group/use-group-upload-images/index.ts b/src/hooks/use-group/use-group-upload-images/index.ts new file mode 100644 index 00000000..7851ff74 --- /dev/null +++ b/src/hooks/use-group/use-group-upload-images/index.ts @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; + +import { API } from '@/api'; +import { PreUploadGroupImagePayload } from '@/types/service/group'; + +export const useUploadGroupImages = () => { + const query = useMutation({ + mutationFn: (payload: PreUploadGroupImagePayload) => { + return API.groupService.uploadGroupImages(payload); + }, + onSuccess: () => { + console.log('이미지 등록 성공'); + }, + onError: () => { + console.log('이미지 등록 실패'); + }, + }); + return query; +}; diff --git a/src/lib/formatDateTime.ts b/src/lib/formatDateTime.ts index 8ced9bef..e171ae37 100644 --- a/src/lib/formatDateTime.ts +++ b/src/lib/formatDateTime.ts @@ -7,20 +7,20 @@ export const formatISO = (dateString: string) => { }; export const formatTimeAgo = (isoString: string) => { - const dateInput = new Date(isoString); + const dateInput = new Date(isoString.endsWith('Z') ? isoString : `${isoString}Z`); const dateNow = new Date(); - const diffPerSec = (dateNow.getTime() - dateInput.getTime()) / 1000; - if (diffPerSec < 60) return `${Math.ceil(diffPerSec)}초 전`; + const diffPerSec = Math.floor((dateNow.getTime() - dateInput.getTime()) / 1000); + if (diffPerSec < 60) return `${diffPerSec}초 전`; - const diffPerMin = diffPerSec / 60; - if (diffPerMin < 60) return `${Math.ceil(diffPerMin)}분 전`; + const diffPerMin = Math.floor(diffPerSec / 60); + if (diffPerMin < 60) return `${diffPerMin}분 전`; - const diffPerHour = diffPerMin / 60; - if (diffPerHour < 24) return `${Math.ceil(diffPerHour)}시간 전`; + const diffPerHour = Math.floor(diffPerMin / 60); + if (diffPerHour < 24) return `${diffPerHour}시간 전`; - const diffPerDay = diffPerHour / 30; - if (diffPerDay < 30) return `${Math.ceil(diffPerDay)}일 전`; + const diffPerDay = Math.floor(diffPerHour / 24); + if (diffPerDay < 30) return `${diffPerDay}일 전`; const yearDiff = dateNow.getFullYear() - dateInput.getFullYear(); const monthDiff = dateNow.getMonth() - dateInput.getMonth(); @@ -30,13 +30,25 @@ export const formatTimeAgo = (isoString: string) => { }; // 모임 시작 시간을 Card 컴포넌트용 형식으로 변환 (예: "25. 12. 25 - 19:00") -export const formatDateTime = (startTime: string, _endTime?: string | null): string => { +export const formatDateTime = (startTime: string, customFormat?: string): string => { const start = new Date(startTime); - const year = start.getFullYear().toString().slice(-2); + + const fullYear = start.getFullYear().toString(); + const shortYear = fullYear.slice(-2); const month = String(start.getMonth() + 1).padStart(2, '0'); const day = String(start.getDate()).padStart(2, '0'); const hours = String(start.getHours()).padStart(2, '0'); const minutes = String(start.getMinutes()).padStart(2, '0'); - - return `${year}. ${month}. ${day} - ${hours}:${minutes}`; + const seconds = String(start.getSeconds()).padStart(2, '0'); + + if (!customFormat) return `${shortYear}. ${month}. ${day} - ${hours}:${minutes}`; + + return customFormat + .replace(/yyyy/g, fullYear) + .replace(/yy/g, shortYear) + .replace(/MM/g, month) + .replace(/dd/g, day) + .replace(/HH/g, hours) + .replace(/mm/g, minutes) + .replace(/ss/g, seconds); }; diff --git a/src/types/service/group.ts b/src/types/service/group.ts index a9150bea..03a2d8f9 100644 --- a/src/types/service/group.ts +++ b/src/types/service/group.ts @@ -80,13 +80,11 @@ export interface PreUploadGroupImagePayload { } export interface PreUploadGroupImageResponse { - images: [ - { - sortOrder: number; - imageUrl440x240: string; - imageUrl100x100: string; - }, - ]; + images: { + sortOrder: number; + imageUrl440x240: string; + imageUrl100x100: string; + }[]; } export type CreateGroupImagePayload = PreUploadGroupImageResponse; @@ -121,11 +119,7 @@ export interface CreateGroupResponse { }; createdAt: string; updatedAt: string; - images?: PreUploadGroupImageResponse | null; -} - -export interface GetGroupDetailsPayload { - groupId: string; + images?: CreateGroupImagePayload['images'] | null; } export interface GetGroupDetailsResponse { @@ -165,3 +159,7 @@ export interface GetGroupDetailsResponse { joinedAt: string; }[]; } + +export interface GroupIdPayload { + groupId: string; +}