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/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..49d1aa1e 100644
--- a/features/meetup/apis.ts
+++ b/features/meetup/apis.ts
@@ -1,10 +1,12 @@
import { clientFetch } from "@/libs/clientFetch";
+import { throwApiError } from "@/utils/api";
import {
MeetupCreateRequest,
MeetupItemResponse,
MeetupListRequest,
MeetupListResponse,
} from "./types";
+import { buildMeetupListQuery } from "./list/utils";
const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
@@ -19,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;
@@ -33,23 +32,13 @@ 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" },
});
- if (!res.ok) {
- const error = await res.json().catch(() => null);
- throw new Error(error?.message ?? "모임 목록을 불러오는데 실패했습니다.");
- }
+ await throwApiError(res, "모임 목록을 불러오는데 실패했습니다.");
return res.json();
}
@@ -61,9 +50,6 @@ export async function postMeetup(data: MeetupCreateRequest): Promise null);
- throw new Error(error?.message ?? "모임 생성에 실패했습니다.");
- }
+ await throwApiError(res, "모임 생성에 실패했습니다.");
return res.json();
}
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/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..e5ce7fcd 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) {
@@ -14,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,
@@ -108,3 +115,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;
+};
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;
}
/** 장소 검색 시 입력 값 유효성 검사 */