From 1753181249df7a03783b02427e622a11e5d32d1e Mon Sep 17 00:00:00 2001 From: PollyGotACracker Date: Sat, 2 May 2026 03:23:35 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor(meetup):=20=EB=AA=A8=EC=9E=84=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B8=B0=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EC=A4=91=EB=B3=B5=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meetup/components/CapacityField/index.tsx | 2 +- features/meetup/constants.ts | 8 +++ .../create/components/StepSchedule/index.tsx | 48 +++++++++------ features/meetup/utils.test.ts | 59 ++++++------------- features/meetup/utils.ts | 38 +++--------- 5 files changed, 65 insertions(+), 90 deletions(-) create mode 100644 features/meetup/constants.ts diff --git a/features/meetup/components/CapacityField/index.tsx b/features/meetup/components/CapacityField/index.tsx index bdc299ca..24526066 100644 --- a/features/meetup/components/CapacityField/index.tsx +++ b/features/meetup/components/CapacityField/index.tsx @@ -1,7 +1,7 @@ "use client"; import InputField from "@/components/ui/Inputs/InputField"; -import { MIN_CONFIRMED_COUNT } from "@/features/meetupDetail/components/PersonnelContainer"; +import { MIN_CONFIRMED_COUNT } from "@/features/meetup/constants"; interface CapacityFieldProps { /** 필드 이름 @default "capacity" */ diff --git a/features/meetup/constants.ts b/features/meetup/constants.ts new file mode 100644 index 00000000..42d7d3cc --- /dev/null +++ b/features/meetup/constants.ts @@ -0,0 +1,8 @@ +import { MIN_CONFIRMED_COUNT as MIN_CONFIRMED_COUNT_DEFAULT } from "@/features/meetupDetail/components/PersonnelContainer"; + +/** 모임 개설 확정 등에 쓰는 최소 인원(모집 정원 하한) */ +export const MIN_CONFIRMED_COUNT = MIN_CONFIRMED_COUNT_DEFAULT; +/** 모임 이름 최대 길이 */ +export const MAX_NAME_LENGTH = 20; +/** 주소 상세 최대 길이 */ +export const MAX_ADDRESS_LENGTH = 50; diff --git a/features/meetup/create/components/StepSchedule/index.tsx b/features/meetup/create/components/StepSchedule/index.tsx index 5eed7cfe..c71df788 100644 --- a/features/meetup/create/components/StepSchedule/index.tsx +++ b/features/meetup/create/components/StepSchedule/index.tsx @@ -4,9 +4,13 @@ import { useEffect } from "react"; import { useFormData } from "../../providers/FormDataProvider"; import CapacityField from "@/features/meetup/components/CapacityField"; import DateTimeField from "@/features/meetup/components/DateTimeField"; -import { validateCapacity, validateDateTime, validateDateTimeOrder } from "../../../utils"; -import { validateMaxCapacity } from "@/features/meetupDetail/edit/utils"; -import { MIN_CONFIRMED_COUNT } from "@/features/meetupDetail/components/PersonnelContainer"; +import { + validateDateTimeIsFuture, + validateDateTimeOrder, + validateMaxCapacity, +} from "@/features/meetupDetail/edit/utils"; +import { validateCapacity } from "@/features/meetup/utils"; +import { MIN_CONFIRMED_COUNT } from "@/features/meetup/constants"; import { useToast } from "@/providers/toast-provider"; interface StepScheduleProps { @@ -39,14 +43,14 @@ export default function StepSchedule({ step }: StepScheduleProps) { !!meetTime && !!regDate && !!regTime && - !validateDateTimeOrder({ - dateTime: next._dateTime, - registrationEnd: next._registrationEnd, - }); + !validateDateTimeOrder( + `${next._dateTime.date} ${next._dateTime.time}`, + `${next._registrationEnd.date} ${next._registrationEnd.time}`, + ); if (isDateTimeKey) { if (!meetDate || !meetTime) return; - if (!validateDateTime(meetDate, meetTime)) { + if (!validateDateTimeIsFuture(`${meetDate} ${meetTime}`)) { handleShowToast({ message: MESSAGE_SCHEDULE_AFTER_NOW, status: "error" }); return; } @@ -56,7 +60,7 @@ export default function StepSchedule({ step }: StepScheduleProps) { } } else { if (!regDate || !regTime) return; - if (!validateDateTime(regDate, regTime)) { + if (!validateDateTimeIsFuture(`${regDate} ${regTime}`)) { handleShowToast({ message: MESSAGE_REGISTRATION_END_AFTER_NOW, status: "error" }); return; } @@ -73,21 +77,27 @@ export default function StepSchedule({ step }: StepScheduleProps) { // 데이터 유효성 검사 useEffect(() => { - const isDateTimeValid = validateDateTime(data._dateTime.date, data._dateTime.time); - const isRegEndValid = validateDateTime(data._registrationEnd.date, data._registrationEnd.time); - const isDateTimeOrderValid = validateDateTimeOrder({ - dateTime: data._dateTime, - registrationEnd: data._registrationEnd, - }); - const isCapacityValid = validateCapacity(data.capacity); - const isMaxCapacityValid = validateMaxCapacity(data.capacity, MIN_CONFIRMED_COUNT); + const isDateTimeValid = + !!data._dateTime.date && + !!data._dateTime.time && + validateDateTimeIsFuture(`${data._dateTime.date} ${data._dateTime.time}`); + const isRegEndValid = + !!data._registrationEnd.date && + !!data._registrationEnd.time && + validateDateTimeIsFuture(`${data._registrationEnd.date} ${data._registrationEnd.time}`); + const isDateTimeOrderValid = validateDateTimeOrder( + `${data._dateTime.date} ${data._dateTime.time}`, + `${data._registrationEnd.date} ${data._registrationEnd.time}`, + ); + const isCapacityEntered = validateCapacity(data.capacity); + const isCapacityMinOk = validateMaxCapacity(data.capacity, MIN_CONFIRMED_COUNT); setStepValid( step, isDateTimeValid && isRegEndValid && isDateTimeOrderValid && - isCapacityValid && - isMaxCapacityValid, + isCapacityEntered && + isCapacityMinOk, ); }, [data, setStepValid, step]); diff --git a/features/meetup/utils.test.ts b/features/meetup/utils.test.ts index 24a0793d..147eb76a 100644 --- a/features/meetup/utils.test.ts +++ b/features/meetup/utils.test.ts @@ -1,11 +1,8 @@ -import dayjs from "dayjs"; import { getAddress, getRegion, splitAddress, validateCapacity, - validateDateTime, - validateDateTimeOrder, validatePlaceSearch, validateText, } from "./utils"; @@ -25,48 +22,28 @@ describe("모임 생성 데이터 유효성 검사 테스트", () => { }); }); - describe("모임 일시, 모집 마감 일시 유효성 검사 테스트", () => { - const after1Today = dayjs().add(1, "day").format("YYYY-MM-DD"); - const before1Today = dayjs().subtract(1, "day").format("YYYY-MM-DD"); - const after2Today = dayjs().add(2, "day").format("YYYY-MM-DD"); - const time = "14:00"; - - test("날짜가 오늘보다 이후이면 true를 반환", () => { - const result = validateDateTime(after1Today, time); - expect(result).toBe(true); - }); - - test("날짜가 오늘보다 이전이면 false를 반환", () => { - const invalidDateResult = validateDateTime(before1Today, time); - expect(invalidDateResult).toBe(false); - }); - - test("모집 마감 일시 이후 모임 일시이면 true를 반환", () => { - const result = validateDateTimeOrder({ - dateTime: { date: after2Today, time }, - registrationEnd: { date: after1Today, time }, - }); - expect(result).toBe(true); - }); - - test("모임 마감 일시 이전 모임 일시이면 false를 반환", () => { - const result = validateDateTimeOrder({ - dateTime: { date: after1Today, time }, - registrationEnd: { date: after2Today, time }, - }); - expect(result).toBe(false); + describe("모임 정원 유효성 검사 테스트", () => { + test("유한한 양의 정수면 true를 반환", () => { + expect(validateCapacity(1)).toBe(true); + expect(validateCapacity(2)).toBe(true); + expect(validateCapacity(100)).toBe(true); + expect(validateCapacity("12")).toBe(true); }); - }); - describe("모임 정원 유효성 검사 테스트", () => { - test("모임 정원이 3명 이상이면 true를 반환", () => { - const result = validateCapacity(3); - expect(result).toBe(true); + test("0·음수·소수·비유한·null·undefined면 false를 반환", () => { + expect(validateCapacity(0)).toBe(false); + expect(validateCapacity(-1)).toBe(false); + expect(validateCapacity(2.5)).toBe(false); + expect(validateCapacity(Number.NaN)).toBe(false); + expect(validateCapacity(Number.POSITIVE_INFINITY)).toBe(false); + expect(validateCapacity(null)).toBe(false); + expect(validateCapacity(undefined)).toBe(false); }); - test("모임 정원이 3명 미만이면 false를 반환", () => { - const result = validateCapacity(2); - expect(result).toBe(false); + test("빈 문자열·숫자가 아닌 문자열·소수 문자열이면 false를 반환", () => { + expect(validateCapacity("")).toBe(false); + expect(validateCapacity("abc")).toBe(false); + expect(validateCapacity("2.5")).toBe(false); }); }); diff --git a/features/meetup/utils.ts b/features/meetup/utils.ts index 5861b7dd..5dd04c4a 100644 --- a/features/meetup/utils.ts +++ b/features/meetup/utils.ts @@ -1,8 +1,7 @@ -import dayjs from "dayjs"; +import { MAX_ADDRESS_LENGTH, MAX_NAME_LENGTH, MIN_CONFIRMED_COUNT } from "./constants"; +export { MAX_ADDRESS_LENGTH, MAX_NAME_LENGTH, MIN_CONFIRMED_COUNT }; -export const MAX_NAME_LENGTH = 20; -export const MAX_ADDRESS_LENGTH = 50; -export const MIN_CONFIRMED_COUNT = 3; +// meetupDetail/edit/utils: validateDateTimeIsFuture, validateDateTimeOrder, validateMaxCapacity /** 텍스트 유효성 검사 */ export function validateText(value: string) { @@ -19,32 +18,13 @@ export function validateAddressDetail(value: string) { return validateText(value) && value.length <= MAX_ADDRESS_LENGTH; } -/** 모임 일시, 모집 마감 일시 유효성 검사 - * date: YYYY-MM-DD - * time: HH:mm - */ -export function validateDateTime(date: string, time: string) { - if (!date || !time) return false; - const dt = dayjs(date + " " + time); - return dt.isValid() && dt.isAfter(dayjs()); -} - -/** 모집 마감 일시 이후 모임 일시인지 유효성 검사 - * YYYY-MM-DD HH:mm - */ -type ValidateDateTimeOrderProps = { - dateTime: { date: string; time: string }; - registrationEnd: { date: string; time: string }; -}; -export function validateDateTimeOrder({ dateTime, registrationEnd }: ValidateDateTimeOrderProps) { - const dateTimeValue = dayjs(`${dateTime.date} ${dateTime.time}`); - const registrationEndValue = dayjs(`${registrationEnd.date} ${registrationEnd.time}`); - return dateTimeValue.isAfter(registrationEndValue); -} +/** 모집 정원 유효성 검사(최소 인원 검증 제외) */ +export function validateCapacity(capacity: number | string | null | undefined): boolean { + if (capacity === null || capacity === undefined) return false; -/** 모집 정원 유효성 검사 */ -export function validateCapacity(capacity: number) { - return capacity >= MIN_CONFIRMED_COUNT; + const n = Number(capacity); + if (n <= 0 || !Number.isInteger(n) || !Number.isFinite(n)) return false; + return true; } /** 장소 검색 시 입력 값 유효성 검사 */ From ece2a338937020411b7e36b840e36380cdb09ac0 Mon Sep 17 00:00:00 2001 From: PollyGotACracker Date: Sat, 2 May 2026 09:24:16 +0900 Subject: [PATCH 2/4] =?UTF-8?q?chore(meetup):=20=EB=AA=A8=EC=9E=84=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=EA=B4=80=EB=A0=A8=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/meetup/list/page.tsx | 8 ++-- features/meetup/apis.ts | 12 ++---- .../list/components/ListFilters/index.tsx | 7 +--- features/meetup/list/constants.ts | 4 +- features/meetup/list/utils.ts | 37 ++++++++++++++++++- features/meetup/queries.ts | 34 +++++++---------- features/meetup/types.ts | 19 ++++++++++ 7 files changed, 78 insertions(+), 43 deletions(-) diff --git a/app/(main)/meetup/list/page.tsx b/app/(main)/meetup/list/page.tsx index 2f75c196..9ae1b256 100644 --- a/app/(main)/meetup/list/page.tsx +++ b/app/(main)/meetup/list/page.tsx @@ -18,8 +18,8 @@ export const metadata: Metadata = { }, }; -const size = 10; - +const defaultPageSize = 10; +const ListFiltersStyle = "mx-0 bg-gray-50 px-6 py-2 md:-mx-4 md:px-6"; export default function MeetupListPage() { return ( @@ -33,7 +33,7 @@ export default function MeetupListPage() { - + @@ -42,5 +42,3 @@ export default function MeetupListPage() { ); } - -const ListFiltersStyle = "mx-0 bg-gray-50 px-6 py-2 md:-mx-4 md:px-6"; diff --git a/features/meetup/apis.ts b/features/meetup/apis.ts index bee31ce0..04bd16f5 100644 --- a/features/meetup/apis.ts +++ b/features/meetup/apis.ts @@ -5,6 +5,7 @@ import { MeetupListRequest, MeetupListResponse, } from "./types"; +import { buildMeetupListQuery } from "./list/utils"; const BASE_URL = process.env.NEXT_PUBLIC_API_URL; @@ -33,15 +34,8 @@ const ROUTE_MEETINGS = "/meetings"; /** 모임 찾기 */ export async function getMeetups(params: MeetupListRequest): Promise { - // encodeURIComponent 자동 적용 - const queryParams = new URLSearchParams(); - for (const [key, value] of Object.entries(params)) { - if (value != null) { - queryParams.append(key, String(value)); - } - } - - const res = await clientFetch(`${ROUTE_MEETINGS}?${queryParams}`, { + const qs = buildMeetupListQuery(params); + const res = await clientFetch(qs ? `${ROUTE_MEETINGS}?${qs}` : ROUTE_MEETINGS, { method: "GET", headers: { "Content-Type": "application/json" }, }); diff --git a/features/meetup/list/components/ListFilters/index.tsx b/features/meetup/list/components/ListFilters/index.tsx index f739d2ae..a068e098 100644 --- a/features/meetup/list/components/ListFilters/index.tsx +++ b/features/meetup/list/components/ListFilters/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { useCategoryStore } from "@/store/category.store"; +import type { RegionFilterValue } from "@/features/meetup/types"; import { CATEGORY_TYPE_ALL, QUERY_KEYS, @@ -20,7 +21,6 @@ import useScrollVisibilityDynamic from "@/hooks/useScrollVisibilityDynamic"; import TabButton from "@/components/ui/Buttons/TabButton"; import DateFilter from "@/components/ui/Filter/DateFilter"; import RegionFilter from "@/components/ui/Filter/RegionFilter"; -import type { Option } from "@/components/ui/Filter/RegionFilter/option"; import { FilterDropdown } from "@/components/ui/Filter/FilterDropdown"; import SearchInput from "@/components/ui/SearchInput"; import IcSearch from "@/components/ui/icons/IcSearch"; @@ -231,10 +231,7 @@ function KeywordFilter() { } // 우측 드롭다운 필터 목록 -export type RegionFilterValue = { - region: Option | null; - district: Option | null; -}; + export type RegionFilterParams = { fullLabel: string; } & RegionFilterValue; diff --git a/features/meetup/list/constants.ts b/features/meetup/list/constants.ts index 920c0d13..74c26270 100644 --- a/features/meetup/list/constants.ts +++ b/features/meetup/list/constants.ts @@ -13,12 +13,12 @@ export const QUERY_KEYS = { * 클라이언트 기본값 createdAt * 서버 기본값 dateTime */ - SORT_BY: "sort", + SORT_BY: "sortBy", /** 정렬 순서 (오름차, 내림차) * 클라이언트 기본값 desc, 서버 기본값 asc * sortBy=createAt의 경우 서버 기본값 desc */ - SORT_ORDER: "order", + SORT_ORDER: "sortOrder", }; /** 정렬 기준 항목 */ diff --git a/features/meetup/list/utils.ts b/features/meetup/list/utils.ts index e63bfd75..1cf49f55 100644 --- a/features/meetup/list/utils.ts +++ b/features/meetup/list/utils.ts @@ -1,8 +1,13 @@ import dayjs from "@/libs/dayjs"; +import type { + MeetupListRequest, + MeetupListSearchInput, + RegionFilterValue, + SortBy, + SortOrder, +} from "../types"; import { isDeadlinePassed } from "@/utils/date"; -import { SortBy, SortOrder } from "../types"; import { CATEGORY_TYPE_ALL, SORT_BY_OPTIONS, SORT_ORDER_OPTIONS } from "./constants"; -import { RegionFilterValue } from "../list/components/ListFilters"; /** 정렬 기준 항목 조회 */ export function getSortByItem(param: string | null) { @@ -108,3 +113,31 @@ export function transformRegionData(data: string | null | undefined): RegionFilt }; } } + +/** search, URL 파라미터로부터 모임 목록 API 요청 객체 생성 */ +export function buildMeetupListRequest( + search: MeetupListSearchInput, + pageSize: number, +): MeetupListRequest { + return { + type: transformTypeValue(search.type), + keyword: transformKeywordQuery(search.keyword), + region: transformQueryValue(search.region), + dateStart: transformDateStartQuery(search.dateStart), + dateEnd: transformDateEndQuery(search.dateEnd), + sortBy: transformSortByQuery(search.sortBy), + sortOrder: transformSortOrderQuery(search.sortOrder), + size: pageSize, + }; +} + +/** 모임 목록 조회 요청 객체를 쿼리스트링 문자열로 변환 */ +export function buildMeetupListQuery(params: MeetupListRequest): string { + const queryParams = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value != null) { + queryParams.append(key, String(value)); + } + } + return queryParams.toString(); +} diff --git a/features/meetup/queries.ts b/features/meetup/queries.ts index c8dc93c7..ae7e039f 100644 --- a/features/meetup/queries.ts +++ b/features/meetup/queries.ts @@ -5,7 +5,7 @@ import { UseMutationOptions, useQueryClient, } from "@tanstack/react-query"; -import type { MeetupCreateRequest, MeetupItemResponse } from "./types"; +import type { MeetupCreateRequest, MeetupItemResponse, MeetupListResponse } from "./types"; import { getMeetups, postMeetup } from "./apis"; import { deleteMeetingsFavorite, @@ -15,15 +15,7 @@ import { } from "@/apis/meetings"; import { uploadImage } from "@/apis/images"; import { useUser } from "@/hooks/useUser"; -import { - transformDateEndQuery, - transformDateStartQuery, - transformKeywordQuery, - transformQueryValue, - transformSortByQuery, - transformSortOrderQuery, - transformTypeValue, -} from "./list/utils"; +import { buildMeetupListRequest } from "./list/utils"; import { QUERY_KEYS } from "./list/constants"; import { useQueryParams } from "@/hooks/useQueryParams"; import { useEffect } from "react"; @@ -39,16 +31,18 @@ export function useGetMeetups(size: number) { const queryClient = useQueryClient(); const { user } = useUser(); const { get } = useQueryParams(); - const params = { - type: transformTypeValue(get(QUERY_KEYS.TYPE)), - keyword: transformKeywordQuery(get(QUERY_KEYS.KEYWORD)), - region: transformQueryValue(get(QUERY_KEYS.REGION)), - dateStart: transformDateStartQuery(get(QUERY_KEYS.DATE_START)), - dateEnd: transformDateEndQuery(get(QUERY_KEYS.DATE_END)), - sortBy: transformSortByQuery(get(QUERY_KEYS.SORT_BY)), - sortOrder: transformSortOrderQuery(get(QUERY_KEYS.SORT_ORDER)), + const params = buildMeetupListRequest( + { + type: get(QUERY_KEYS.TYPE), + keyword: get(QUERY_KEYS.KEYWORD), + region: get(QUERY_KEYS.REGION), + dateStart: get(QUERY_KEYS.DATE_START), + dateEnd: get(QUERY_KEYS.DATE_END), + sortBy: get(QUERY_KEYS.SORT_BY), + sortOrder: get(QUERY_KEYS.SORT_ORDER), + }, size, - }; + ); useEffect(() => { queryClient.invalidateQueries({ @@ -60,7 +54,7 @@ export function useGetMeetups(size: number) { return useInfiniteQuery({ queryKey: meetupQueryKeys.listWithParams(params), queryFn: ({ pageParam }) => getMeetups({ ...params, cursor: pageParam }), - getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, + getNextPageParam: (lastPage: MeetupListResponse) => lastPage.nextCursor ?? undefined, initialPageParam: undefined as string | undefined, placeholderData: keepPreviousData, refetchOnWindowFocus: false, diff --git a/features/meetup/types.ts b/features/meetup/types.ts index b0f54f3a..61e1fcd5 100644 --- a/features/meetup/types.ts +++ b/features/meetup/types.ts @@ -1,3 +1,5 @@ +import { Option } from "@/components/ui/Filter/RegionFilter/option"; + export interface KakaoPlaceItem { address_name: string; category_group_code: string; @@ -100,6 +102,8 @@ export type SortOrder = "asc" | "desc"; export interface MeetupListRequest { /** 모임 ID */ id?: number; + /** 검색 키워드 */ + keyword?: string; /** 모임 종류 */ type?: string; /** 모임 지역 */ @@ -127,6 +131,15 @@ export interface MeetupListRequest { size?: number; } +/** 목록 검색 쿼리 필드 키 */ +type MeetupListSearchKeys = Exclude< + keyof MeetupListRequest, + "id" | "createdBy" | "cursor" | "size" +>; +export type MeetupListSearchInput = Partial< + Record +>; + /** 모임 목록 조회 항목 데이터 */ export interface MeetupItem { /** 모임 ID */ @@ -203,3 +216,9 @@ export type MeetupItemSelected = time: string; }) | null; + +/** 지역 필터 값 */ +export type RegionFilterValue = { + region: Option | null; + district: Option | null; +}; From cb720dc42357c18da24b758031e1fbe0ad3f692b Mon Sep 17 00:00:00 2001 From: PollyGotACracker Date: Sat, 2 May 2026 09:38:31 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor(apis):=20=EB=AA=A8=EC=9E=84=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=83=9D=EC=84=B1=20api?= =?UTF-8?q?=EC=97=90=20throwApiError=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apis/images.ts | 17 +++-------------- apis/meetingTypes.ts | 9 ++------- apis/meetings.ts | 29 +++++------------------------ features/meetup/apis.ts | 16 ++++------------ 4 files changed, 14 insertions(+), 57 deletions(-) diff --git a/apis/images.ts b/apis/images.ts index 7666ddd8..630e9a50 100644 --- a/apis/images.ts +++ b/apis/images.ts @@ -1,4 +1,5 @@ import { clientFetch } from "@/libs/clientFetch"; +import { throwApiError } from "@/utils/api"; export interface ErrorResponse { code: string; @@ -43,13 +44,7 @@ async function getPresignedUrl(fileName: string, contentType: string, folder: st body: JSON.stringify({ fileName, contentType, folder }), }); - if (!res.ok) { - const error: ErrorResponse = await res.json().catch(() => ({ - code: "UNKNOWN_ERROR", - message: "업로드 주소 생성 중 알 수 없는 에러가 발생했습니다.", - })); - throw new Error(error.message); - } + await throwApiError(res, "업로드 주소 생성 중 알 수 없는 에러가 발생했습니다."); return res.json(); } @@ -62,13 +57,7 @@ async function uploadToS3(presignedUrl: string, file: File) { body: file, }); - if (!res.ok) { - const error: ErrorResponse = await res.json().catch(() => ({ - code: "UNKNOWN_ERROR", - message: "업로드 중 알 수 없는 에러가 발생했습니다.", - })); - throw new Error(error.message); - } + await throwApiError(res, "업로드 중 알 수 없는 에러가 발생했습니다."); return res.json(); } diff --git a/apis/meetingTypes.ts b/apis/meetingTypes.ts index 4fe07b45..fe05a49a 100644 --- a/apis/meetingTypes.ts +++ b/apis/meetingTypes.ts @@ -1,4 +1,5 @@ import type { Category } from "@/store/category.store"; +import { throwApiError } from "@/utils/api"; const BASE_URL = process.env.NEXT_PUBLIC_API_URL; @@ -15,13 +16,7 @@ export async function getMeetingTypes() { cache: "force-cache", }); - if (!res.ok) { - const error = await res.json().catch(() => ({ - code: "UNKNOWN_ERROR", - message: "모임 카테고리 조회에 실패했습니다.", - })); - throw new Error(error.message); - } + await throwApiError(res, "모임 카테고리 조회에 실패했습니다."); return res.json(); } diff --git a/apis/meetings.ts b/apis/meetings.ts index 4b1409eb..b5179f9b 100644 --- a/apis/meetings.ts +++ b/apis/meetings.ts @@ -1,4 +1,5 @@ import { clientFetch } from "@/libs/clientFetch"; +import { throwApiError } from "@/utils/api"; const ROUTE_MEETINGS_FAVORITES = (meetingId: number) => `/meetings/${meetingId}/favorites`; const ROUTE_MEETINGS_JOIN = (meetingId: number) => `/meetings/${meetingId}/join`; @@ -10,12 +11,7 @@ export async function postMeetingsFavorite({ meetingId }: { meetingId: number }) method: "POST", headers: { "Content-Type": "application/json" }, }); - if (!res.ok) { - const error: ErrorResponse = await res - .json() - .catch(() => ({ code: "UNKNOWN_ERROR", message: "모임 찜 추가에 실패했습니다." })); - throw new Error(error.message); - } + await throwApiError(res, "모임 찜 추가에 실패했습니다."); return res.json(); } @@ -26,12 +22,7 @@ export async function deleteMeetingsFavorite({ meetingId }: { meetingId: number method: "DELETE", headers: { "Content-Type": "application/json" }, }); - if (!res.ok) { - const error: ErrorResponse = await res - .json() - .catch(() => ({ code: "UNKNOWN_ERROR", message: "모임 찜 해제에 실패했습니다." })); - throw new Error(error.message); - } + await throwApiError(res, "모임 찜 해제에 실패했습니다."); return res.json(); } @@ -42,12 +33,7 @@ export async function postMeetingsJoin({ meetingId }: { meetingId: number }): Pr method: "POST", headers: { "Content-Type": "application/json" }, }); - if (!res.ok) { - const error: ErrorResponse = await res - .json() - .catch(() => ({ code: "UNKNOWN_ERROR", message: "모임 참여에 실패했습니다." })); - throw new Error(error.message); - } + await throwApiError(res, "모임 참여에 실패했습니다."); return res.json(); } @@ -58,12 +44,7 @@ export async function deleteMeetingsJoin({ meetingId }: { meetingId: number }): method: "DELETE", headers: { "Content-Type": "application/json" }, }); - if (!res.ok) { - const error: ErrorResponse = await res - .json() - .catch(() => ({ code: "UNKNOWN_ERROR", message: "모임 참여 취소에 실패했습니다." })); - throw new Error(error.message); - } + await throwApiError(res, "모임 참여 취소에 실패했습니다."); return res.json(); } diff --git a/features/meetup/apis.ts b/features/meetup/apis.ts index 04bd16f5..49d1aa1e 100644 --- a/features/meetup/apis.ts +++ b/features/meetup/apis.ts @@ -1,4 +1,5 @@ import { clientFetch } from "@/libs/clientFetch"; +import { throwApiError } from "@/utils/api"; import { MeetupCreateRequest, MeetupItemResponse, @@ -20,10 +21,7 @@ export type getKakaoPlaceFn = typeof getKakaoPlace; const ROUTE_KAKAO_PLACE = "/kakao/place"; export async function getKakaoPlace(query: string) { const res = await clientFetch(`${ROUTE_KAKAO_PLACE}?query=${query}`); - if (!res.ok) { - const error = await res.json().catch(() => null); - throw new Error(error?.message ?? "카카오 장소 검색 API 호출에 실패했습니다."); - } + await throwApiError(res, "카카오 장소 검색 API 호출에 실패했습니다."); const data = await res.json(); const { documents } = data; @@ -40,10 +38,7 @@ export async function getMeetups(params: MeetupListRequest): Promise null); - throw new Error(error?.message ?? "모임 목록을 불러오는데 실패했습니다."); - } + await throwApiError(res, "모임 목록을 불러오는데 실패했습니다."); return res.json(); } @@ -55,9 +50,6 @@ export async function postMeetup(data: MeetupCreateRequest): Promise null); - throw new Error(error?.message ?? "모임 생성에 실패했습니다."); - } + await throwApiError(res, "모임 생성에 실패했습니다."); return res.json(); } From ed7fa590c21f198ddb9b5e11de87b341314b98f2 Mon Sep 17 00:00:00 2001 From: PollyGotACracker Date: Sat, 2 May 2026 10:14:22 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs(meetup):=20=EB=AA=A8=EC=9E=84=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EC=97=90=20=EB=8F=99=EC=9D=BC=20=EC=A1=B0=EA=B1=B4=EC=9D=98=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/meetup/list/utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/meetup/list/utils.ts b/features/meetup/list/utils.ts index 1cf49f55..e5ce7fcd 100644 --- a/features/meetup/list/utils.ts +++ b/features/meetup/list/utils.ts @@ -19,11 +19,13 @@ export function getSortOrderItem(param: string | null) { return SORT_ORDER_OPTIONS.find((o) => o.value === param) ?? SORT_ORDER_OPTIONS[0]; } +// meetupDetail/components/PersonnelContainer /** 개설 확정 여부 체크 */ export function checkIsConfirmed(confirmedAt: string | null) { return confirmedAt !== null; } +// meetupDetail/components/InformationContainer /** 모집 마감 여부 체크: registrationEnd 가 지났거나 정원이 가득 찼으면 true */ export function checkIsRegClosed( registrationEnd: string,