diff --git a/package.json b/package.json
index 4a5b8bd0..c7f4a3c2 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
"react-calendar": "^4.7.0",
"react-dom": "^18",
"react-error-boundary": "^4.0.12",
+ "react-highlight-words": "^0.20.0",
"react-hook-form": "^7.49.0",
"react-icons": "^4.12.0",
"react-kakao-maps-sdk": "^1.1.26",
@@ -90,6 +91,7 @@
"@types/prismjs": "^1.26.3",
"@types/react": "^18",
"@types/react-dom": "^18",
+ "@types/react-highlight-words": "^0.16.7",
"@types/react-paginate": "^7.1.4",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/sockjs": "^0.3.36",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d3803378..df17cfa6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -149,6 +149,9 @@ dependencies:
react-error-boundary:
specifier: ^4.0.12
version: 4.0.12(react@18.2.0)
+ react-highlight-words:
+ specifier: ^0.20.0
+ version: 0.20.0(react@18.2.0)
react-hook-form:
specifier: ^7.49.0
version: 7.49.0(react@18.2.0)
@@ -238,6 +241,9 @@ devDependencies:
'@types/react-dom':
specifier: ^18
version: 18.2.17
+ '@types/react-highlight-words':
+ specifier: ^0.16.7
+ version: 0.16.7
'@types/react-paginate':
specifier: ^7.1.4
version: 7.1.4
@@ -2307,6 +2313,12 @@ packages:
dependencies:
'@types/react': 18.2.43
+ /@types/react-highlight-words@0.16.7:
+ resolution: {integrity: sha512-+upXTIaRd3rGvh1aDQSs9z5X+sV3UM6Jrmjk03GN2GXl4v/+iOJKQj2LZHo6Vp2IoTvMdtxgME26feqo12xXLg==}
+ dependencies:
+ '@types/react': 18.2.43
+ dev: true
+
/@types/react-paginate@7.1.4:
resolution: {integrity: sha512-6fqZvDzRJHubOGl6c7cGFC9ysgQSWYy0Gpus9HjORpydlcXgPnT8x+aKgqwCdtpZrRTwcBz2Q7JWAOYaRrsXGg==}
dependencies:
@@ -4113,6 +4125,10 @@ packages:
resolution: {integrity: sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw==}
dev: true
+ /highlight-words-core@1.2.2:
+ resolution: {integrity: sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==}
+ dev: false
+
/highlight.js@10.7.3:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
dev: false
@@ -4716,6 +4732,10 @@ packages:
engines: {node: '>= 0.6'}
dev: false
+ /memoize-one@4.0.3:
+ resolution: {integrity: sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw==}
+ dev: false
+
/memoize-one@5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
dev: false
@@ -5483,6 +5503,17 @@ packages:
react: 18.2.0
dev: false
+ /react-highlight-words@0.20.0(react@18.2.0):
+ resolution: {integrity: sha512-asCxy+jCehDVhusNmCBoxDf2mm1AJ//D+EzDx1m5K7EqsMBIHdZ5G4LdwbSEXqZq1Ros0G0UySWmAtntSph7XA==}
+ peerDependencies:
+ react: ^0.14.0 || ^15.0.0 || ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0
+ dependencies:
+ highlight-words-core: 1.2.2
+ memoize-one: 4.0.3
+ prop-types: 15.8.1
+ react: 18.2.0
+ dev: false
+
/react-hook-form@7.49.0(react@18.2.0):
resolution: {integrity: sha512-gf4qyY4WiqK2hP/E45UUT6wt3Khl49pleEVcIzxhLBrD6m+GMWtLRk0vMrRv45D1ZH8PnpXFwRPv0Pewske2jw==}
engines: {node: '>=18', pnpm: '8'}
diff --git a/src/app/coding-meetings/_components/guard-page/Unauthorized.tsx b/src/app/coding-meetings/_components/guard-page/Unauthorized.tsx
new file mode 100644
index 00000000..9b554baa
--- /dev/null
+++ b/src/app/coding-meetings/_components/guard-page/Unauthorized.tsx
@@ -0,0 +1,25 @@
+"use client"
+
+import Mentee from "@/components/shared/animation/Mentee"
+import notificationMessage from "@/constants/message/notification"
+import { useClientSession } from "@/hooks/useClientSession"
+import { useEffect } from "react"
+
+function CreateCodingMeetingUnauthorized() {
+ const { clientSessionReset } = useClientSession()
+
+ useEffect(() => {
+ clientSessionReset()
+ }, []) /* eslint-disable-line */
+
+ return (
+
+
+
+
+
{notificationMessage.unauthorized}.
+
+ )
+}
+
+export default CreateCodingMeetingUnauthorized
diff --git a/src/app/coding-meetings/create/page.tsx b/src/app/coding-meetings/create/page.tsx
index 787c954b..24c2dcfc 100644
--- a/src/app/coding-meetings/create/page.tsx
+++ b/src/app/coding-meetings/create/page.tsx
@@ -1,10 +1,8 @@
-import Mentee from "@/components/shared/animation/Mentee"
-import notificationMessage from "@/constants/message/notification"
import CreateCodingMeetingPage from "@/page/coding-meetings/create/CreateCodingMeetingPage"
-
import { getServerSession } from "@/util/auth"
import { Metadata } from "next"
import { notFound } from "next/navigation"
+import CreateCodingMeetingUnauthorized from "../_components/guard-page/Unauthorized"
export const metadata: Metadata = {
title: `모각코 생성`,
@@ -33,17 +31,9 @@ export const metadata: Metadata = {
export default async function CreateCodingMeetingsPage() {
const { user } = getServerSession()
- // [TODO]
try {
if (!user) {
- return (
-
-
-
-
-
{notificationMessage.unauthorized}.
-
- )
+ return
}
return
diff --git a/src/app/coding-meetings/post/[token]/page.tsx b/src/app/coding-meetings/post/[token]/page.tsx
index 360953a4..8d0a2a5e 100644
--- a/src/app/coding-meetings/post/[token]/page.tsx
+++ b/src/app/coding-meetings/post/[token]/page.tsx
@@ -69,12 +69,13 @@ export default async function UpdateCodingMeetingsPage({
/>
)
}
+
return (
)
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 44e8e505..0399493f 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -15,6 +15,7 @@ import ScrollTop from "@/components/shared/ScrollTop"
import ToastDismissEventListener from "@/components/layout/ToastDismissListener"
import GoogleAnalyticsProvider from "@/google-analytics/GoogleAnalyticsProvider"
import HistorySession from "@/components/history/HistorySession"
+import CodingMeetingFormProvider from "@/page/coding-meetings/create/CodingMeetingFormProvider"
export const dynamic = "force-dynamic"
@@ -68,8 +69,10 @@ export default function RootLayout({
- {children}
-
+
+ {children}
+
+
diff --git a/src/components/LinkToListPage.tsx b/src/components/LinkToListPage.tsx
index 614360a9..18c752e1 100644
--- a/src/components/LinkToListPage.tsx
+++ b/src/components/LinkToListPage.tsx
@@ -4,7 +4,7 @@ import { getHistorySessionPath } from "@/util/historySession/path"
import { useRouter } from "next/navigation"
import { DirectionIcons } from "./icons/Icons"
-export type TargetPage = "qna" | "chat"
+export type TargetPage = "qna" | "chat" | "coding-meetings"
interface LinkToListPageProps {
to: TargetPage
@@ -13,11 +13,13 @@ interface LinkToListPageProps {
const initialPath: Record = {
qna: "/qna?page=0",
chat: "/chat?page=0",
+ "coding-meetings": "/coding-meetings?page=0&size=10&filter=all",
} as const
const targetPathname: Record = {
qna: "/qna",
chat: "/chat",
+ "coding-meetings": "/coding-meetings",
} as const
function LinkToListPage({ to }: LinkToListPageProps) {
diff --git a/src/components/shared/TextCounter.tsx b/src/components/shared/TextCounter.tsx
index fa95a47e..fbe1e207 100644
--- a/src/components/shared/TextCounter.tsx
+++ b/src/components/shared/TextCounter.tsx
@@ -17,7 +17,7 @@ interface TextCounterProps {
}
function TextCounter({
- text,
+ text = "",
min,
max,
className,
diff --git a/src/constants/limitation.ts b/src/constants/limitation.ts
index 060df4f1..89723399 100644
--- a/src/constants/limitation.ts
+++ b/src/constants/limitation.ts
@@ -30,6 +30,8 @@ const Limitation = {
mentoring_time: 10,
title_limit_under: 5,
title_limit_over: 100,
+ chat_content_min_length: 10,
+ chat_content_max_length: 1000,
chat_introduction_limit_under: 10,
chat_introduction_limit_over: 150,
content_limit_under: 10,
@@ -60,3 +62,18 @@ const Limitation = {
} as const
export default Limitation
+
+export const CODING_MEETING_LIMITS = {
+ title: {
+ minLength: 5,
+ maxLength: 100,
+ },
+ memberCount: {
+ min: 3,
+ max: 6,
+ },
+ content: {
+ minLength: 10,
+ maxLength: 10000,
+ },
+}
diff --git a/src/constants/message/validation.ts b/src/constants/message/validation.ts
index 1bcaedcd..3775d5e6 100644
--- a/src/constants/message/validation.ts
+++ b/src/constants/message/validation.ts
@@ -10,6 +10,7 @@ export const validationMessage = {
noTime: "정확한 시간대를 설정해주세요",
noLocation: "모임 위치를 설정해주세요",
noHeadCnt: "모임 인원을 설정해주세요",
+ chatContentLength: `소개글은 최소 ${Limitation.chat_content_min_length}자 이상 ${Limitation.chat_content_max_length}자 이하이어야 합니다.`,
underContentLimit: `본문 내용은 최소 ${Limitation.content_limit_under}자 이상이어야 합니다.`,
overContentLimit: `본문 내용은 최대 ${Limitation.content_limit_over}자 이하이어야 합니다.`,
underAnswerLimit: `댓글 내용은 최소 ${Limitation.answer_limit_under}자 이상이어야 합니다.`,
diff --git a/src/constants/select.ts b/src/constants/select.ts
deleted file mode 100644
index 4db07061..00000000
--- a/src/constants/select.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const HeadCountValue = ["3", "4", "5", "6"] as const
diff --git a/src/constants/timeOptions.ts b/src/constants/timeOptions.ts
index 7cc5c06b..180e39b6 100644
--- a/src/constants/timeOptions.ts
+++ b/src/constants/timeOptions.ts
@@ -1,9 +1,32 @@
+export type CodingMeetingHourMinuteTime = {
+ hour:
+ | (typeof CODING_MEETING_HOURS)["AM"][number]
+ | (typeof CODING_MEETING_HOURS)["PM"][number]
+ minute: (typeof CODING_MEETING_MINUTES)[number]
+}
+
+export type CodingMeetingTimeOption = {
+ AM: `${(typeof CODING_MEETING_HOURS)["AM"][number]}:${(typeof CODING_MEETING_MINUTES)[number]}`
+ PM: `${(typeof CODING_MEETING_HOURS)["PM"][number]}:${(typeof CODING_MEETING_MINUTES)[number]}`
+}
+export type CodingMeetingTimeOptions = {
+ AM: Array
+ PM: Array
+}
+
export const enum TimeZone {
AM = "AM",
PM = "PM",
}
-export const AM = [
+export const CODING_MEETING_HOURS = {
+ AM: ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11"],
+ PM: ["12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"],
+} as const
+
+export const CODING_MEETING_MINUTES = ["00", "30"] as const
+
+export const CODING_MEETING_AM_OPTIONS: CodingMeetingTimeOptions["AM"] = [
"00:00",
"00:30",
"01:00",
@@ -30,7 +53,7 @@ export const AM = [
"11:30",
]
-export const PM = [
+export const CODING_MEETING_PM_OPTIONS: CodingMeetingTimeOptions["PM"] = [
"12:00",
"12:30",
"13:00",
@@ -56,32 +79,3 @@ export const PM = [
"23:00",
"23:30",
]
-
-const hours = [
- "00",
- "01",
- "02",
- "03",
- "04",
- "05",
- "06",
- "07",
- "08",
- "09",
- "10",
- "11",
- "12",
- "13",
- "14",
- "15",
- "16",
- "17",
- "18",
- "19",
- "20",
- "21",
- "22",
- "23",
-]
-const minutes = ["00", "30"]
-export const timeSelect = { hours, minutes }
diff --git a/src/interfaces/form.ts b/src/interfaces/form.ts
index a3e3cbc3..bc7b1512 100644
--- a/src/interfaces/form.ts
+++ b/src/interfaces/form.ts
@@ -1,3 +1,5 @@
+import { CodingMeetingHourMinuteTime } from "@/constants/timeOptions"
+import { CodingMeetingHashTag } from "./coding-meetings"
export interface LoginFormData {
email: string
password: string
@@ -20,7 +22,42 @@ export interface AnswerFormData {
answer: string
}
-// comment
+// coding-meeting
+
+// [coding-meeting] form data
+export type CodingMeetingPageMode = "create" | "update"
+
+export interface CodingMeetingFormData {
+ title: string
+ content: string
+ member_upper_limit: number
+ date: {
+ day: Date
+ start_time: [
+ CodingMeetingHourMinuteTime["hour"],
+ CodingMeetingHourMinuteTime["minute"],
+ ]
+ end_time: [
+ CodingMeetingHourMinuteTime["hour"],
+ CodingMeetingHourMinuteTime["minute"],
+ ]
+ }
+ location: {
+ id: string
+ longitude: string
+ latitude: string
+ place_name: string
+ }
+ hashtags: { tag: CodingMeetingHashTag }[]
+}
+
+export interface CodingMeetingLocationSearchFormData {
+ keyword: string
+}
+
+export type CodingMeetingFormInitialValues = CodingMeetingFormData
+
+// [coding-meeting] comment
export interface CommentFormData {
comment: string
}
diff --git a/src/mocks/db/coding-meetings.ts b/src/mocks/db/coding-meetings.ts
index 5a5a5e87..22aaaed2 100644
--- a/src/mocks/db/coding-meetings.ts
+++ b/src/mocks/db/coding-meetings.ts
@@ -11,7 +11,7 @@ const mockCodingMeetings: MockCodingMeeting[] = [
member_level_image_url: badge_url[mockUsers[0].level],
created_date: "2024-02-23T04:30:00.696Z",
coding_meeting_closed: false,
- coding_meeting_token: "CMT00000000",
+ coding_meeting_token: "cm_T00000000",
coding_meeting_title: "봉은사역에서 모각코할 분들 모집합니다.",
coding_meeting_start_time: "2024-02-29T07:30:53.696Z",
coding_meeting_end_time: "2024-02-29T09:00:53.696Z",
@@ -64,7 +64,7 @@ const mockCodingMeetings: MockCodingMeeting[] = [
member_level_image_url: badge_url[mockUsers[1].level],
created_date: "2024-02-22T05:28:00.696Z",
coding_meeting_closed: true,
- coding_meeting_token: "CMT00000001",
+ coding_meeting_token: "cm_T00000001",
coding_meeting_title:
"[신림] 정기 오프라인 모각코 스터디 (주2회 이상 참여 必)",
coding_meeting_start_time: "2024-02-29T15:30:00.696Z",
@@ -87,7 +87,7 @@ const mockCodingMeetings: MockCodingMeeting[] = [
member_level_image_url: badge_url[mockUsers[2].level],
created_date: "2024-02-23T04:36:53.696Z",
coding_meeting_closed: false,
- coding_meeting_token: "CMT00000002",
+ coding_meeting_token: "cm_T00000002",
coding_meeting_title:
"[사당역] Next.js 스터디 같이 하실 초보분들 모십니다.",
coding_meeting_start_time: "2024-03-01T08:00:53.696Z",
diff --git a/src/mocks/db/questions.ts b/src/mocks/db/questions.ts
index d452b8e8..13c6a6e2 100644
--- a/src/mocks/db/questions.ts
+++ b/src/mocks/db/questions.ts
@@ -1,7 +1,7 @@
import badge_url from "@/assets/images/badges"
import { Question } from "@/interfaces/question"
import { mockUsers } from "./user"
-// 2023-12-16T23:10:12
+
export const mockQuestions: Array = [
{
id: 1,
diff --git a/src/mocks/handler/coding-meeting/create-coding-meeting.ts b/src/mocks/handler/coding-meeting/create-coding-meeting.ts
index b8a8d46c..a6351a57 100644
--- a/src/mocks/handler/coding-meeting/create-coding-meeting.ts
+++ b/src/mocks/handler/coding-meeting/create-coding-meeting.ts
@@ -7,7 +7,6 @@ import { RouteMap } from "@/service/route-map"
import { HttpResponse, PathParams, http } from "msw"
import jwt, { JwtPayload } from "jsonwebtoken"
import { mockUsers } from "@/mocks/db/user"
-import { HttpStatusCode } from "axios"
import mockCodingMeetings from "@/mocks/db/coding-meetings"
import { MockCodingMeeting } from "@/interfaces/coding-meetings"
import badge_url from "@/assets/images/badges"
@@ -23,10 +22,11 @@ export const mockCreateCodingMeetingApi = http.post<
const { ...createPayload } = await request.json()
const header = request.headers
- const header_token = header.get("Authorization")
+ const authHeader = header.get("Authorization")
- if (!header_token) {
- const { Code, HttpStatus } = ApiStatus.QnA.updateQustion.Unauthorized
+ if (!authHeader) {
+ const { Code, HttpStatus } =
+ ApiStatus.CodingMeetings.createCodingMeeting.Unauthorized
return HttpResponse.json(
{
code: Code,
@@ -36,25 +36,30 @@ export const mockCreateCodingMeetingApi = http.post<
)
}
- const decoded_token = jwt.decode(header_token) as JwtPayload & {
+ const accessToken = authHeader.replace(/^Bearer /, "")
+
+ const decoded_token = jwt.decode(accessToken) as JwtPayload & {
id: number
}
const targetMember = mockUsers.find((user) => user.id === decoded_token.id)
if (!targetMember) {
+ const { Code, HttpStatus } =
+ ApiStatus.CodingMeetings.createCodingMeeting.NotFound
+
return HttpResponse.json(
{
- code: -1,
- msg: "답변을 입력할 권한이 없습니다.",
+ code: Code,
+ msg: "유저를 찾을 수 없습니다.",
},
{
- status: HttpStatusCode.Forbidden,
+ status: HttpStatus,
},
)
}
- const token = "CMT" + (mockCodingMeetings.length + 10000)
+ const token = "cm_" + (mockCodingMeetings.length + 10000)
const newCodingMeetingPost: MockCodingMeeting = {
member_id: targetMember.id,
@@ -71,14 +76,16 @@ export const mockCreateCodingMeetingApi = http.post<
mockCodingMeetings.push(newCodingMeetingPost)
+ const { Code, HttpStatus } = ApiStatus.CodingMeetings.createCodingMeeting.Ok
+
return HttpResponse.json(
{
- code: 5144,
+ code: Code,
msg: "모각코 생성 성공",
data: { coding_meeting_token: token },
},
{
- status: HttpStatusCode.Ok,
+ status: HttpStatus,
},
)
},
diff --git a/src/mocks/handler/coding-meeting/update-coding-meeting.ts b/src/mocks/handler/coding-meeting/update-coding-meeting.ts
index c3d5cb3d..c6ac4644 100644
--- a/src/mocks/handler/coding-meeting/update-coding-meeting.ts
+++ b/src/mocks/handler/coding-meeting/update-coding-meeting.ts
@@ -6,6 +6,7 @@ import {
import mockCodingMeetings from "@/mocks/db/coding-meetings"
import { RouteMap } from "@/service/route-map"
import { HttpResponse, http } from "msw"
+import jwt, { JwtPayload } from "jsonwebtoken"
export const mockUpdateCodingMeetingApi = http.put<
{ coding_meeting_token: string },
@@ -18,10 +19,11 @@ export const mockUpdateCodingMeetingApi = http.put<
async ({ params, request }) => {
try {
const header = request.headers
- const token = header.get("Authorization")
+ const authHeader = header.get("Authorization")
- if (!token) {
- const { Code, HttpStatus } = ApiStatus.QnA.updateQustion.Unauthorized
+ if (!authHeader) {
+ const { Code, HttpStatus } =
+ ApiStatus.CodingMeetings.updateCodingMeeting.Unauthorized
return HttpResponse.json(
{
code: Code,
@@ -31,29 +33,47 @@ export const mockUpdateCodingMeetingApi = http.put<
)
}
- const targetToken = params.coding_meeting_token
+ const accessToken = authHeader.replace(/^Bearer /g, "")
+ const decoded_token = jwt.decode(accessToken) as JwtPayload & {
+ id: number
+ }
- const updatePayload = await request.json()
+ const targetCodingMeetingToken = params.coding_meeting_token
- const targetMockIdx = mockCodingMeetings.findIndex(
- (post) => post.coding_meeting_token === targetToken,
+ const targetMockCodingMeetingIdx = mockCodingMeetings.findIndex(
+ (post) => post.coding_meeting_token === targetCodingMeetingToken,
)
- if (targetMockIdx < 0) {
+ if (targetMockCodingMeetingIdx < 0) {
const { Code, HttpStatus } =
ApiStatus.CodingMeetings.updateCodingMeeting.NotFound
return HttpResponse.json(
{
code: Code,
- msg: "존재하지 않는 질문",
+ msg: "존재하지 않는 모각코",
},
{ status: HttpStatus },
)
}
- mockCodingMeetings[targetMockIdx] = {
- ...mockCodingMeetings[targetMockIdx],
+ const targetMockCodingMeeting =
+ mockCodingMeetings[targetMockCodingMeetingIdx]
+
+ if (targetMockCodingMeeting.member_id !== decoded_token.id) {
+ const { Code, HttpStatus } =
+ ApiStatus.CodingMeetings.updateCodingMeeting.Forbidden
+
+ return HttpResponse.json(
+ { code: Code, msg: "해당 모각코를 수정할 권한이 없는 유저입니다" },
+ { status: HttpStatus },
+ )
+ }
+
+ const updatePayload = await request.json()
+
+ mockCodingMeetings[targetMockCodingMeetingIdx] = {
+ ...mockCodingMeetings[targetMockCodingMeetingIdx],
...updatePayload,
}
@@ -63,7 +83,7 @@ export const mockUpdateCodingMeetingApi = http.put<
return HttpResponse.json(
{
code: Code,
- msg: "질문 수정 성공",
+ msg: "모각코 수정 성공",
},
{ status: HttpStatus },
)
diff --git a/src/page/coding-meetings/create/CodingMeetingFormProvider.tsx b/src/page/coding-meetings/create/CodingMeetingFormProvider.tsx
new file mode 100644
index 00000000..5e835edf
--- /dev/null
+++ b/src/page/coding-meetings/create/CodingMeetingFormProvider.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { usePathname } from "next/navigation"
+import { FormProvider, useForm } from "react-hook-form"
+
+function CodingMeetingFormProvider({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ const pathname = usePathname()
+
+ const methods = useForm()
+
+ if (
+ pathname === "/coding-meetings/create" ||
+ pathname.startsWith("/coding-meetings/post/")
+ ) {
+ return {children}
+ }
+
+ return children
+}
+
+export default CodingMeetingFormProvider
diff --git a/src/page/coding-meetings/create/CreateCodingMeetingPage.tsx b/src/page/coding-meetings/create/CreateCodingMeetingPage.tsx
index ce30fe93..8f598d5e 100644
--- a/src/page/coding-meetings/create/CreateCodingMeetingPage.tsx
+++ b/src/page/coding-meetings/create/CreateCodingMeetingPage.tsx
@@ -1,410 +1,131 @@
"use client"
-import { useClientSession } from "@/hooks/useClientSession"
-import { CodingMeetingHashTagList } from "@/recoil/atoms/coding-meeting/hashtags"
-import { useQueryClient } from "@tanstack/react-query"
-import { useRouter } from "next/navigation"
-import { FieldErrors, useForm } from "react-hook-form"
+import { FieldErrors, useFormContext } from "react-hook-form"
import { toast } from "react-toastify"
-import { useRecoilState, useRecoilValue } from "recoil"
-import CodingMeetingSection from "./components/CodingMeetingSection"
-import { Input } from "@/components/shared/input/Input"
import Spacing from "@/components/shared/Spacing"
-import Textarea from "@/components/shared/textarea/Textarea"
import Button from "@/components/shared/button/Button"
-import HashTagsSection from "./components/HashTagsSection"
-import LocationSection from "./components/LocationSection"
-import HeadCountSection from "./components/HeadCountSection"
-import DateTimeSection from "./components/DateTimeSection"
-import { CodingMeetingQueries } from "@/react-query/coding-meeting"
-import { CodingMeetingHeadCount } from "@/recoil/atoms/coding-meeting/headcount"
-import { DirectionIcons } from "@/components/icons/Icons"
-import { EndTime, StartTime } from "@/recoil/atoms/coding-meeting/dateTime"
-import { LocationForSubmit } from "@/recoil/atoms/coding-meeting/mapData"
-import Limitation from "@/constants/limitation"
-import type { CodingMeetingDetailPayload } from "@/interfaces/dto/coding-meeting/get-coding-meeting-detail.dto"
-import NotFound from "@/app/not-found"
-import { revalidatePage } from "@/util/actions/revalidatePage"
-import queryKey from "@/constants/queryKey"
-import useHandleCreateCodingMeetingTime from "./hooks/useHandleCreateCodingMeetingTime"
-import { AxiosError } from "axios"
-import { APIResponse } from "@/interfaces/dto/api-response"
-import TextCounter from "@/components/shared/TextCounter"
-import { twJoin } from "tailwind-merge"
-import notificationMessage from "@/constants/message/notification"
-import { validationMessage } from "@/constants/message/validation"
-import { errorMessage } from "@/constants/message/error"
+import HashTagsSection from "./components/section/hash-tags/HashTagsSection"
+import DateTimeSection from "./components/section/date-time/DateTimeSection"
+import { CodingMeetingFormData, CodingMeetingPageMode } from "@/interfaces/form"
+import TitleSection from "./components/section/title/TitleSection"
+import LocationSection from "./components/section/location/LocationSection"
+import MemberCountSection from "./components/section/member-count/MemberCountSection"
+import ContentSection from "./components/section/content/ContentSection"
+import LinkToListPage from "@/components/LinkToListPage"
+import { pickFirstError } from "@/util/hook-form/error"
+import { useCreateCodingMeeting } from "./hooks/useCreateCodingMeeting"
+import { formDataToPayload, payloadToFormData } from "../util/parser"
+import { useUpdateCodingMeeting } from "./hooks/useUpdateCodingMeeting"
+import { CodingMeetingDetailPayload } from "@/interfaces/dto/coding-meeting/get-coding-meeting-detail.dto"
interface CreateCodingMeetingPageProps {
- editMode: "create" | "update"
- initialValues?: CodingMeetingDetailPayload
+ editMode: CodingMeetingPageMode
coding_meeting_token?: string
+ initialCodingMeeting?: CodingMeetingDetailPayload
}
-interface CodingMeetingFormData {
- title: string
- content: string
-}
-
-const CreateCodingMeetingPage = ({
+function CreateCodingMeetingPage(props: { editMode: "create" }): JSX.Element
+function CreateCodingMeetingPage(props: {
+ editMode: "update"
+ coding_meeting_token: string
+ initialCodingMeeting: CodingMeetingDetailPayload
+}): JSX.Element
+function CreateCodingMeetingPage({
editMode,
- initialValues,
coding_meeting_token,
-}: CreateCodingMeetingPageProps) => {
- const [hash_tags, setHash_tags] = useRecoilState(CodingMeetingHashTagList)
- const [head_cnt, setHead_cnt] = useRecoilState(CodingMeetingHeadCount)
- const startTime = useRecoilValue(StartTime)
- const endTime = useRecoilValue(EndTime)
- const [location, setLocation] = useRecoilState(LocationForSubmit)
- const queryClient = useQueryClient()
- const { replace } = useRouter()
- const { user } = useClientSession()
- const {
- resetDateTimes,
- formatTime,
- formatByUTC,
- formattedStartTime,
- formattedEndTime,
- } = useHandleCreateCodingMeetingTime()
-
- const { register, handleSubmit, watch, getValues } =
- useForm(
- initialValues
- ? {
- defaultValues: {
- title: initialValues.coding_meeting_title,
- content: initialValues.coding_meeting_content,
- },
- }
- : {},
- )
+ initialCodingMeeting,
+}: CreateCodingMeetingPageProps): JSX.Element {
+ const { handleSubmit } = useFormContext()
- const { createCodingMeetingPost } =
- CodingMeetingQueries.useCreateCodingMeeting()
- const { updateCodingMeeting } = CodingMeetingQueries.useUpdateCodingMeeting()
+ const { createCodingMeetingApi, createCodingMeetingApiStatus } =
+ useCreateCodingMeeting()
- const goToListPage = () => replace("/coding-meetings")
+ const { updateCodingMeetingApi, updateCodingMeetingApiStatus } =
+ useUpdateCodingMeeting()
- const onSubmit = async (data: CodingMeetingFormData) => {
- // 사용자 권한 인증
- if (!user)
- return toast.error(notificationMessage.unauthorized, {
- toastId: "unauthorizedToCreateCodingMeeting",
- position: "top-center",
- })
- // 장소 유효성 검사
- if (!location)
- return toast.error(validationMessage.noLocation, {
- toastId: "emptyLocation",
- position: "top-center",
- })
- // 인원수 유효성 검사
- if (head_cnt === "0")
- return toast.error(validationMessage.noHeadCnt, {
- toastId: "emptyHeadCnt",
- position: "top-center",
- })
- // 시간 유효성 검사
- // 시간 값이 없을 경우
- if (!startTime || !endTime)
- return toast.error(validationMessage.noTime, {
- toastId: "emptyCodingMeetingTime",
- position: "top-center",
- })
- // 종료 시간이 시작 시간보다 빠를 경우
- if (formattedEndTime.isBefore(formattedStartTime))
- return toast.error(validationMessage.timeError, {
- toastId: "codingMeetingTimeError",
- position: "top-center",
- })
- // 시작 시간이 종료 시간과 같을 경우
- if (formattedEndTime.isSame(formattedStartTime, "minute"))
- return toast.error(validationMessage.sameTime, {
- toastId: "codingMeetingSameTimeError",
- position: "top-center",
- })
-
- const payload = {
- coding_meeting_title: data.title,
- coding_meeting_content: data.content,
- coding_meeting_hashtags: hash_tags,
- coding_meeting_location_id: location?.coding_meeting_location_id,
- coding_meeting_location_place_name:
- location.coding_meeting_location_place_name,
- coding_meeting_location_longitude:
- location.coding_meeting_location_longitude,
- coding_meeting_location_latitude:
- location.coding_meeting_location_latitude,
- coding_meeting_member_upper_limit: Number(head_cnt),
- coding_meeting_start_time: formatByUTC(formatTime(startTime)),
- coding_meeting_end_time: formatByUTC(formatTime(endTime)),
- }
+ const initialFormData = initialCodingMeeting
+ ? payloadToFormData(initialCodingMeeting)
+ : null
+ const onSubmit = async (formData: CodingMeetingFormData) => {
if (editMode === "create") {
- createCodingMeetingPost(payload, {
- onSuccess: (res) => {
- queryClient.invalidateQueries({
- queryKey: [queryKey.codingMeeting],
- })
-
- replace(`/coding-meetings/${res.data.data?.coding_meeting_token}`)
-
- setHash_tags([])
- setHead_cnt("3")
- resetDateTimes()
- setLocation(undefined)
- },
- onError: (error: Error | AxiosError) => {
- if (error instanceof AxiosError) {
- const { response } = error as AxiosError
-
- toast.error(
- response?.data.msg ?? errorMessage.createCodingMeeting,
- {
- toastId: "failToCreateCodingMeeting",
- position: "top-center",
- },
- )
- return
- }
-
- toast.error(errorMessage.createCodingMeeting, {
- toastId: "failToCreateCodingMeeting",
- position: "top-center",
- })
- },
- })
- }
-
- if (!coding_meeting_token) {
- return NotFound()
+ createCodingMeetingApi(formDataToPayload("create", formData))
+ return
}
- if (editMode === "update") {
- const editPayload = {
- ...payload,
- coding_meeting_token,
- }
- updateCodingMeeting(editPayload, {
- onSuccess: async (res) => {
- queryClient.resetQueries({
- queryKey: [queryKey.codingMeeting],
- })
-
- await revalidatePage("/coding-meetings/[token]", "page")
-
- setTimeout(() => {
- replace(`/coding-meetings/${coding_meeting_token}`)
-
- setHash_tags([])
- setHead_cnt("3")
- resetDateTimes()
- setLocation(undefined)
- }, 0)
- },
- onError: (error: Error | AxiosError) => {
- if (error instanceof AxiosError) {
- const { response } = error as AxiosError
-
- toast.error(
- response?.data.msg ?? errorMessage.updateCodingMeeting,
- {
- toastId: "failToUpdateCodingMeeting",
- position: "top-center",
- },
- )
- return
- }
-
- toast.error(errorMessage.updateCodingMeeting, {
- toastId: "failToUpdateCodingMeeting",
- position: "top-center",
- })
- },
- })
- }
+ // update
+ updateCodingMeetingApi(
+ formDataToPayload("update", formData, coding_meeting_token!),
+ )
}
- const onInvalid = async (errors: FieldErrors) => {
- if (errors?.title) {
- const titleErrorMessage = ((type: typeof errors.title.type) => {
- switch (type) {
- case "required":
- return validationMessage.notitle
- case "minLength":
- return validationMessage.underTitleLimit
- case "maxLength":
- return validationMessage.overTitleLimit
- }
- })(errors.title.type)
-
- toast.error(titleErrorMessage, {
- position: "top-center",
- toastId: "createCodingMeetingTitle",
- })
-
- window.scroll({
- top: 0,
- behavior: "smooth",
- })
-
- return
- }
- if (errors?.content) {
- const contentErrorMessage = ((type: typeof errors.content.type) => {
- switch (type) {
- case "required":
- return validationMessage.noContent
- case "minLength":
- return validationMessage.underContentLimit
- case "maxLength":
- return validationMessage.overContentLimit
- }
- })(errors.content.type)
+ const onInvalid = async (errors: FieldErrors) => {
+ const error = pickFirstError(errors)
- toast.error(contentErrorMessage, {
- position: "top-center",
- toastId: "createCodingMeetingContent",
- })
- return
- }
+ toast.error(error.message, {
+ position: "top-center",
+ toastId: "codingMeetingInvalidForm",
+ })
}
- const TitleInputClass = twJoin([
- "text-base placeholder:text-base",
- watch("title") &&
- (watch("title")?.length < Limitation.title_limit_under ||
- watch("title")?.length > Limitation.title_limit_over) &&
- "focus:border-danger border-danger",
- ])
-
return (
-
-
-
-
목록 보기
+
+
+
+
-
모각코 모집하기
+
+ {editMode === "create" ? "모각코 모집하기" : "모각코 수정하기"}
+
@@ -413,3 +134,17 @@ const CreateCodingMeetingPage = ({
}
export default CreateCodingMeetingPage
+
+const buttonText = ({
+ editMode,
+ loading,
+}: {
+ editMode: CodingMeetingPageMode
+ loading: boolean
+}) => {
+ if (editMode === "create") {
+ return loading ? "모각코 개설하는 중" : "모각코 개설하기"
+ }
+
+ return loading ? "모각코 수정하는 중" : "모각코 수정하기"
+}
diff --git a/src/page/coding-meetings/create/CreateCodingMeetingPage.types.ts b/src/page/coding-meetings/create/CreateCodingMeetingPage.types.ts
deleted file mode 100644
index 27296cb8..00000000
--- a/src/page/coding-meetings/create/CreateCodingMeetingPage.types.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import type {
- CodingMeetingDateTime,
- CodingMeetingHashTags,
- CodingMeetingLocation,
-} from "@/interfaces/coding-meetings"
-import type { Time } from "@/recoil/atoms/coding-meeting/dateTime"
-import type { LabelHTMLAttributes, PropsWithChildren } from "react"
-import { SetterOrUpdater } from "recoil"
-
-export interface CodingMeetingFormData {
- title: string
-}
-
-export type SubmitAskQuestionData = CodingMeetingFormData
-
-export type SubmitUpdateCodingMeetingData = Omit<
- SubmitAskQuestionData,
- "member_id"
-> & { post_id: number }
-
-export const enum TimeZone {
- AM = "AM",
- PM = "PM",
-}
-
-export interface CodingMeetingSectionProps
- extends NonNullable
{
- className?: string
-}
-
-export interface CodingMeetingSectionLabelProps
- extends LabelHTMLAttributes {}
-
-export type TimeOptionsProps = { date: string[] }
-
-export type HeadCountSectionProps = {
- initialCnt?: string
-}
-
-export type HashTagsSectionProps = {
- initialHashTags?: CodingMeetingHashTags
-}
-
-export type DateTimeSectionProps = {
- initialDateTime?: CodingMeetingDateTime
-}
-
-export type LocationSectionProps = {
- initialLocation?: CodingMeetingLocation
-}
-
-export type TimeBoxProps = {
- timeState: Time
- setTimeState: SetterOrUpdater
+
+
+ {place.place_name}
+
+ {place.address_name}
+
+
+
+
+
+
+
+ )
+ })}
+
+ )
+}
+
+export default SearchResultPlaceMaps
diff --git a/src/page/coding-meetings/create/components/section/location/search/AutoComplete.tsx b/src/page/coding-meetings/create/components/section/location/search/AutoComplete.tsx
new file mode 100644
index 00000000..00ec0936
--- /dev/null
+++ b/src/page/coding-meetings/create/components/section/location/search/AutoComplete.tsx
@@ -0,0 +1,76 @@
+"use client"
+
+import { PlaceAutoComplete } from "@/recoil/atoms/coding-meeting/mapData"
+import { useRecoilValue } from "recoil"
+import HighlightPlaceName from "./HighlightPlaceName"
+import React from "react"
+
+interface AutoCompleteProps {
+ onPlaceClick?: (payload: {
+ event: React.MouseEvent
+ place: kakao.maps.services.PlacesSearchResultItem
+ }) => void
+}
+
+function AutoComplete({ onPlaceClick }: AutoCompleteProps) {
+ const { keyword, loading, placeList } = useRecoilValue(PlaceAutoComplete)
+
+ if (!keyword) {
+ return 검색어를 입력해 주세요
+ }
+
+ if (loading) return 잠시만 기다려주세요
+
+ if (!placeList?.length)
+ return (
+
+
+ {keyword}
+ {" "}
+ 검색어와 일치하는 장소를 찾지 못했어요.
+
+ )
+
+ return (
+
+ {placeList.map((place) => {
+ const onClick = (e: React.MouseEvent) => {
+ onPlaceClick && onPlaceClick({ event: e, place })
+ }
+
+ return (
+ -
+
+ {place.address_name}
+
+ )
+ })}
+
+ )
+}
+
+export default AutoComplete
+
+AutoComplete.Wrapper = function AutoCompleteWrapper({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ const placeAutoComplete = useRecoilValue(PlaceAutoComplete)
+
+ return placeAutoComplete.open ? (
+
+ {children}
+
+ ) : null
+}
diff --git a/src/page/coding-meetings/create/components/section/location/search/HighlightPlaceName.tsx b/src/page/coding-meetings/create/components/section/location/search/HighlightPlaceName.tsx
new file mode 100644
index 00000000..6954e405
--- /dev/null
+++ b/src/page/coding-meetings/create/components/section/location/search/HighlightPlaceName.tsx
@@ -0,0 +1,69 @@
+import Highlight from "react-highlight-words"
+
+interface HighlightPlaceNameProps {
+ placeName: string
+ typedKeyword: string
+}
+
+function HighlightPlaceName({
+ placeName,
+ typedKeyword,
+}: HighlightPlaceNameProps) {
+ const searchWords = getSearchWords({
+ keyword: typedKeyword,
+ text: placeName,
+ })
+
+ return (
+
+ )
+}
+
+export default HighlightPlaceName
+
+function getSearchWords({
+ keyword,
+ text,
+}: {
+ keyword: string
+ text: string
+}): string[] {
+ return keyword
+ .split(" ")
+ .map((keyword) => {
+ if (text.search(keyword) > -1) {
+ return [keyword]
+ }
+
+ const matchedWords = []
+
+ let startIndex = 0
+
+ for (let i = 0; i < keyword.length; i++) {
+ const subKeyword = keyword.slice(startIndex, i + 1)
+
+ if (text.search(subKeyword) === -1) {
+ const prev = keyword.slice(startIndex, i)
+
+ if (prev && text.search(prev) > -1) {
+ matchedWords.push(prev)
+
+ startIndex = i
+ i = startIndex - 1
+ }
+ continue
+ }
+
+ if (i === keyword.length - 1) {
+ matchedWords.push(keyword.slice(startIndex, i + 1))
+ }
+ }
+
+ return matchedWords?.length ? matchedWords : []
+ })
+ .flatMap((value) => value)
+}
diff --git a/src/page/coding-meetings/create/components/section/location/search/SearchController.tsx b/src/page/coding-meetings/create/components/section/location/search/SearchController.tsx
new file mode 100644
index 00000000..7e9aabf0
--- /dev/null
+++ b/src/page/coding-meetings/create/components/section/location/search/SearchController.tsx
@@ -0,0 +1,278 @@
+"use client"
+
+import { Icons } from "@/components/icons/Icons"
+import Button from "@/components/shared/button/Button"
+import RowInput from "@/components/shared/input/RowInput"
+import { CodingMeetingLocationSearchFormData } from "@/interfaces/form"
+import { debounce } from "lodash-es"
+import React, { useEffect, useMemo, useRef } from "react"
+import { FieldErrors, useController, useForm } from "react-hook-form"
+import { IoIosCloseCircle } from "react-icons/io"
+import AutoComplete from "./AutoComplete"
+import { twMerge } from "tailwind-merge"
+import { useKakaoMapPlaceApi } from "@/page/coding-meetings/create/hooks/useKakaoMapPlaceApi"
+import {
+ PlaceAutoComplete,
+ SearchPlaceResultData,
+} from "@/recoil/atoms/coding-meeting/mapData"
+import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil"
+import SearchResultPlaceMaps from "../SearchResultPlaceMaps"
+import { locationSearchRules } from "@/page/coding-meetings/create/controls/rules/location/location-search-rules"
+
+function SearchController() {
+ const { control } = useForm()
+ const { field: searchField } = useController({
+ control,
+ name: "keyword",
+ rules: locationSearchRules,
+ defaultValue: "",
+ })
+
+ const { kakaoPlaceApi } = useKakaoMapPlaceApi()
+
+ const [placeAutoComplete, setPlaceAutoComplete] =
+ useRecoilState(PlaceAutoComplete)
+ const resetPlaceAutoComplete = useResetRecoilState(PlaceAutoComplete)
+ const setSearchPlaceResultData = useSetRecoilState(SearchPlaceResultData)
+ const resetSearchPlaceResultData = useResetRecoilState(SearchPlaceResultData)
+
+ const containerRef = useRef(null) // clickAway에서 활용
+ const inputFieldRef = useRef(null) // 인풋요소를 blur, focus 하기 위해
+ const isComposingRef = useRef(false) // IME 이슈 해결에서 활용
+
+ const inputFoucsedClassnames = twMerge([
+ "p-0 focus-within:border-colorsGray overflow-hidden",
+ placeAutoComplete.open &&
+ "focus-within:border-transparent border-transparent rounded-tl-2xl rounded-tr-2xl rounded-bl-none rounded-br-none shadow-[0_4px_8px_0_rgba(0,0,0,.13)]",
+ ])
+
+ // debounce handler
+ const debounceSearchCallback = (value: string) => {
+ if (isComposingRef.current === true) {
+ inputFieldRef.current!.blur()
+ inputFieldRef.current!.focus()
+ }
+
+ if (!kakaoPlaceApi) {
+ setPlaceAutoComplete((prev) => ({
+ ...prev,
+ open: false,
+ loading: false,
+ keyword: value,
+ placeList: [],
+ }))
+
+ return
+ }
+
+ if (!value) {
+ resetPlaceAutoComplete()
+
+ return
+ }
+
+ if (placeAutoComplete.loading) {
+ return
+ }
+
+ setPlaceAutoComplete((prev) => ({
+ ...prev,
+ open: true,
+ loading: true,
+ keyword: value,
+ }))
+
+ kakaoPlaceApi.keywordSearch(value, (data, status, _pagination) => {
+ if (status === kakao.maps.services.Status.OK) {
+ setPlaceAutoComplete((prev) => ({
+ ...prev,
+ loading: false,
+ placeList: data,
+ }))
+
+ return
+ }
+
+ if (status === kakao.maps.services.Status.ZERO_RESULT) {
+ setPlaceAutoComplete((prev) => ({
+ ...prev,
+ loading: false,
+ placeList: [],
+ }))
+
+ return
+ }
+
+ // error
+ console.log("place search error(debounce)", { value })
+
+ setPlaceAutoComplete((prev) => ({
+ ...prev,
+ loading: false,
+ placeList: [],
+ }))
+ })
+ }
+
+ const debounceKeyword = useMemo(
+ () => debounce(debounceSearchCallback, 400),
+ [kakaoPlaceApi] /* eslint-disable-line */,
+ ) // kakaoApi 외 나머지는 내부에서 참조(ref)로 값을 사용할 수 있음
+
+ // change handler
+ const handleChange = (e: React.ChangeEvent) => {
+ setPlaceAutoComplete((prev) => ({ ...prev, open: false }))
+ searchField.onChange(e.target.value)
+
+ debounceKeyword(e.target.value)
+ }
+
+ // search submit handler
+ const onSearchSubmit = ({ keyword }: CodingMeetingLocationSearchFormData) => {
+ setPlaceAutoComplete((prev) => ({ ...prev, open: false }))
+ inputFieldRef.current!.blur()
+
+ kakaoPlaceApi?.keywordSearch(keyword, (data, status, _pagination) => {
+ setTimeout(() => {
+ setPlaceAutoComplete((prev) => ({
+ ...prev,
+ open: false,
+ }))
+ }, 400) // delay시간을 debounce 시간으로 설정
+
+ if (status === kakao.maps.services.Status.OK) {
+ setSearchPlaceResultData({
+ keyword,
+ placeList: data,
+ })
+
+ return
+ }
+
+ if (status === kakao.maps.services.Status.ZERO_RESULT) {
+ setSearchPlaceResultData({
+ keyword,
+ placeList: [],
+ })
+
+ return
+ }
+
+ // error
+ console.log("place search error", { keyword })
+
+ setSearchPlaceResultData({
+ keyword,
+ placeList: [],
+ })
+ })
+ }
+
+ const onSearchInvalid = (
+ errors: FieldErrors,
+ ) => {
+ // console.log("searchErrors", { errors })
+ }
+
+ // effect: add clickAway listener
+ // cleanup
+ useEffect(() => {
+ const handleClickAway = (e: MouseEvent) => {
+ if (!containerRef.current) return
+
+ const target = e.target as HTMLElement
+ if (containerRef.current.contains(target)) {
+ return
+ }
+
+ setPlaceAutoComplete((prev) => ({ ...prev, open: false }))
+ }
+
+ window.addEventListener("click", handleClickAway)
+
+ return () => {
+ window.removeEventListener("click", handleClickAway)
+
+ resetSearchPlaceResultData()
+ resetPlaceAutoComplete()
+ }
+ }, []) /* eslint-disable-line */
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default SearchController
diff --git a/src/page/coding-meetings/create/components/section/member-count/MemberCountSection.tsx b/src/page/coding-meetings/create/components/section/member-count/MemberCountSection.tsx
new file mode 100644
index 00000000..c90afc76
--- /dev/null
+++ b/src/page/coding-meetings/create/components/section/member-count/MemberCountSection.tsx
@@ -0,0 +1,20 @@
+import { CodingMeetingFormData } from "@/interfaces/form"
+import MemberCountController from "../../../controls/MemberCountController"
+import CodingMeetingSection from "../../CodingMeetingSection"
+
+interface MemberCountSectionProps {
+ initialMemberCount?: CodingMeetingFormData["member_upper_limit"]
+}
+
+function MemberCountSection({ initialMemberCount }: MemberCountSectionProps) {
+ return (
+
+
+ 모집인원
+
+
+
+ )
+}
+
+export default MemberCountSection
diff --git a/src/page/coding-meetings/create/components/section/title/TitleSection.tsx b/src/page/coding-meetings/create/components/section/title/TitleSection.tsx
new file mode 100644
index 00000000..45bcf041
--- /dev/null
+++ b/src/page/coding-meetings/create/components/section/title/TitleSection.tsx
@@ -0,0 +1,12 @@
+import { CodingMeetingFormData } from "@/interfaces/form"
+import TitleController from "../../../controls/TitleController"
+
+interface TitleSectionProps {
+ initialTitle?: CodingMeetingFormData["title"]
+}
+
+function TitleSection({ initialTitle }: TitleSectionProps) {
+ return
+}
+
+export default TitleSection
diff --git a/src/page/coding-meetings/create/controls/ContentController.tsx b/src/page/coding-meetings/create/controls/ContentController.tsx
new file mode 100644
index 00000000..daf35915
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/ContentController.tsx
@@ -0,0 +1,45 @@
+"use client"
+
+import TextCounter from "@/components/shared/TextCounter"
+import Textarea from "@/components/shared/textarea/Textarea"
+import { CODING_MEETING_LIMITS } from "@/constants/limitation"
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { useController, useFormContext } from "react-hook-form"
+import { codingMeetingContentRules } from "./rules/content/coding-meeting-content-rules"
+
+interface ContentControllerProps {
+ initialContent?: CodingMeetingFormData["content"]
+}
+
+function ContentController({ initialContent }: ContentControllerProps) {
+ const { control } = useFormContext()
+ const { field } = useController({
+ control,
+ name: "content",
+ rules: codingMeetingContentRules,
+ defaultValue: initialContent ?? "",
+ })
+
+ return (
+
+ )
+}
+
+export default ContentController
diff --git a/src/page/coding-meetings/create/controls/LocationController.tsx b/src/page/coding-meetings/create/controls/LocationController.tsx
new file mode 100644
index 00000000..6ed7fa0f
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/LocationController.tsx
@@ -0,0 +1,112 @@
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { useController, useFormContext } from "react-hook-form"
+import LocationDialog from "../components/section/location/LocationDialog"
+import Button from "@/components/shared/button/Button"
+import { Icons } from "@/components/icons/Icons"
+import useModal from "@/hooks/useModal"
+import { useEffect } from "react"
+import { locationRules } from "./rules/location/location-rules"
+import { FaExternalLinkAlt } from "react-icons/fa"
+
+interface LocationControllerProps {
+ initialLocation?: CodingMeetingFormData["location"]
+}
+
+function LocationController({ initialLocation }: LocationControllerProps) {
+ const { control } = useFormContext()
+ const { field } = useController({
+ control,
+ name: "location",
+ rules: locationRules,
+ defaultValue: initialLocation,
+ })
+
+ const { closeModal } = useModal()
+
+ useEffect(() => {
+ return () => {
+ closeModal()
+ }
+ }, []) /* eslint-disable-line */
+
+ return (
+
+ {field.value ? (
+
+ ) : (
+
+ )}
+
+ )
+}
+
+export default LocationController
+
+const LocationSelector = () => {
+ const { openModal } = useModal()
+
+ return (
+
+ )
+}
+
+const SelectedLocation = ({
+ location,
+}: {
+ location: CodingMeetingFormData["location"]
+}) => {
+ const { openModal } = useModal()
+
+ return (
+
+
+ {location.place_name}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/page/coding-meetings/create/controls/MemberCountController.tsx b/src/page/coding-meetings/create/controls/MemberCountController.tsx
new file mode 100644
index 00000000..3c4bf256
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/MemberCountController.tsx
@@ -0,0 +1,78 @@
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { useController, useFormContext } from "react-hook-form"
+import { memberCountRules } from "./rules/member-count/member-count-rules"
+import { CODING_MEETING_LIMITS } from "@/constants/limitation"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/Select"
+import { useId } from "react"
+
+interface MemberCountControllerProps {
+ initialMemberCount?: CodingMeetingFormData["member_upper_limit"]
+}
+
+function MemberCountController({
+ initialMemberCount,
+}: MemberCountControllerProps) {
+ const { control } = useFormContext()
+ const { field } = useController({
+ control,
+ name: "member_upper_limit",
+ rules: memberCountRules,
+ defaultValue: initialMemberCount,
+ })
+
+ const id = useId()
+
+ const onValueChange = (countFormat: string) => {
+ field.onChange(Number(countFormat))
+ }
+
+ return (
+
+
+ 본인 포함 최대 6명까지 가능합니다.
+
+
+
+ )
+}
+
+export default MemberCountController
+
+const memberCountOptions = Array.from({
+ length:
+ CODING_MEETING_LIMITS.memberCount.max -
+ CODING_MEETING_LIMITS.memberCount.min +
+ 1,
+}).map((_, index) => {
+ return CODING_MEETING_LIMITS.memberCount.min + index
+})
diff --git a/src/page/coding-meetings/create/controls/TitleController.tsx b/src/page/coding-meetings/create/controls/TitleController.tsx
new file mode 100644
index 00000000..4093df20
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/TitleController.tsx
@@ -0,0 +1,57 @@
+import TextCounter from "@/components/shared/TextCounter"
+import { Input } from "@/components/shared/input/Input"
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { useController, useFormContext } from "react-hook-form"
+import CodingMeetingSection from "../components/CodingMeetingSection"
+import Limitation from "@/constants/limitation"
+import { codingMeetingTitleRules } from "./rules/title/title-rules"
+
+interface TitleControllerProps {
+ initialTitle?: CodingMeetingFormData["title"]
+}
+
+function TitleController({ initialTitle }: TitleControllerProps) {
+ const { control } = useFormContext()
+ const { field, fieldState } = useController({
+ control,
+ name: "title",
+ rules: codingMeetingTitleRules,
+ defaultValue: initialTitle ?? "",
+ })
+
+ return (
+
+
+ 제목
+
+
+
+
+
+
+ )
+}
+
+export default TitleController
diff --git a/src/page/coding-meetings/create/controls/end-time/CodingMeetingEndHourController.tsx b/src/page/coding-meetings/create/controls/end-time/CodingMeetingEndHourController.tsx
new file mode 100644
index 00000000..839d78ff
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/end-time/CodingMeetingEndHourController.tsx
@@ -0,0 +1,77 @@
+"use client"
+
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectTrigger,
+} from "@/components/ui/Select"
+import { CODING_MEETING_HOURS } from "@/constants/timeOptions"
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { SelectValue } from "@radix-ui/react-select"
+import { useId } from "react"
+import { useController, useFormContext } from "react-hook-form"
+import { codingMeetingEndHourRules } from "../rules/date/end-time/coding-meeting-end-hour-rules"
+
+interface CodingMeetingEndHourControllerProps {
+ initialEndHour?: CodingMeetingFormData["date"]["end_time"][0]
+}
+
+function CodingMeetingEndHourController({
+ initialEndHour,
+}: CodingMeetingEndHourControllerProps) {
+ const { control } = useFormContext()
+ const { field } = useController({
+ control,
+ name: "date.end_time.0",
+ rules: codingMeetingEndHourRules,
+ defaultValue: initialEndHour,
+ })
+
+ const seperatorId = useId()
+
+ return (
+
+ )
+}
+
+export default CodingMeetingEndHourController
diff --git a/src/page/coding-meetings/create/controls/end-time/CodingMeetingEndMinuteController.tsx b/src/page/coding-meetings/create/controls/end-time/CodingMeetingEndMinuteController.tsx
new file mode 100644
index 00000000..999f59ca
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/end-time/CodingMeetingEndMinuteController.tsx
@@ -0,0 +1,59 @@
+"use client"
+
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { useController, useFormContext } from "react-hook-form"
+import { useId } from "react"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/Select"
+import { CODING_MEETING_MINUTES } from "@/constants/timeOptions"
+import { codingMeetingEndMinuteRules } from "../rules/date/end-time/coding-meeting-end-minute-rules"
+
+interface CodingMeetingEndMinuteControllerProps {
+ initialEndMinute?: CodingMeetingFormData["date"]["end_time"][1]
+}
+
+function CodingMeetingEndMinuteController({
+ initialEndMinute,
+}: CodingMeetingEndMinuteControllerProps) {
+ const { control } = useFormContext()
+ const { field } = useController({
+ control,
+ name: "date.end_time.1",
+ rules: codingMeetingEndMinuteRules,
+ defaultValue: initialEndMinute,
+ })
+
+ const seperatorId = useId()
+
+ return (
+
+ )
+}
+
+export default CodingMeetingEndMinuteController
diff --git a/src/page/coding-meetings/create/controls/rules/content/coding-meeting-content-rules.ts b/src/page/coding-meetings/create/controls/rules/content/coding-meeting-content-rules.ts
new file mode 100644
index 00000000..ff5c550c
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/rules/content/coding-meeting-content-rules.ts
@@ -0,0 +1,20 @@
+import { CODING_MEETING_LIMITS } from "@/constants/limitation"
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { RegisterOptions } from "react-hook-form"
+
+type CodingMeetingContentRules = Omit<
+ RegisterOptions,
+ "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
+>
+
+export const codingMeetingContentRules: CodingMeetingContentRules = {
+ required: "모집글의 내용을 작성해주세요",
+ minLength: {
+ value: CODING_MEETING_LIMITS.content.minLength,
+ message: `모집글은 최소 ${CODING_MEETING_LIMITS.content.minLength}자 이상이어야 합니다`,
+ },
+ maxLength: {
+ value: CODING_MEETING_LIMITS.content.maxLength,
+ message: `모집글은 최대 ${CODING_MEETING_LIMITS.content.maxLength}자 이하이어야 합니다`,
+ },
+}
diff --git a/src/page/coding-meetings/create/controls/rules/date/day/coding-meeting-day-rules.ts b/src/page/coding-meetings/create/controls/rules/date/day/coding-meeting-day-rules.ts
new file mode 100644
index 00000000..a4cc4683
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/rules/date/day/coding-meeting-day-rules.ts
@@ -0,0 +1,21 @@
+import { CodingMeetingFormData } from "@/interfaces/form"
+import dayjs from "dayjs"
+import { RegisterOptions } from "react-hook-form"
+
+type CodingMeetingDayRules = Omit<
+ RegisterOptions,
+ "setValueAs" | "disabled" | "valueAsNumber" | "valueAsDate"
+>
+
+export const codingMeetingDayRules: CodingMeetingDayRules = {
+ validate: (day) => {
+ const meetingDay = dayjs(day).startOf("days")
+ const now = dayjs().startOf("days")
+
+ if (meetingDay.isBefore(now)) {
+ return `모임 날짜는 ${now.format("YYYY-MM-DD")} 부터 가능합니다`
+ }
+
+ return true
+ },
+}
diff --git a/src/page/coding-meetings/create/controls/rules/date/end-time/coding-meeting-end-hour-rules.ts b/src/page/coding-meetings/create/controls/rules/date/end-time/coding-meeting-end-hour-rules.ts
new file mode 100644
index 00000000..058e479f
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/rules/date/end-time/coding-meeting-end-hour-rules.ts
@@ -0,0 +1,24 @@
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { RegisterOptions } from "react-hook-form"
+
+type CodingMeetingEndHourRules = Omit<
+ RegisterOptions,
+ "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
+>
+
+export const codingMeetingEndHourRules: CodingMeetingEndHourRules = {
+ required: "모임 종료시간의 시(hour)를 선택해주세요",
+ validate: (endHour, values) => {
+ if (!values.date.start_time) return true
+
+ const [startHour, _] = values.date.start_time
+
+ if (!startHour) return true
+
+ if (Number(endHour) < Number(startHour)) {
+ return "모임 종료 시간은 시작 시간 이후여야 합니다"
+ }
+
+ return true
+ },
+}
diff --git a/src/page/coding-meetings/create/controls/rules/date/end-time/coding-meeting-end-minute-rules.ts b/src/page/coding-meetings/create/controls/rules/date/end-time/coding-meeting-end-minute-rules.ts
new file mode 100644
index 00000000..4528b51f
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/rules/date/end-time/coding-meeting-end-minute-rules.ts
@@ -0,0 +1,43 @@
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { RegisterOptions } from "react-hook-form"
+
+type CodingMeetingEndMinuteRules = Omit<
+ RegisterOptions,
+ "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
+>
+
+export const codingMeetingEndMinuteRules: CodingMeetingEndMinuteRules = {
+ required: "모임 종료시간의 분(minute)을 선택해주세요",
+ validate: (endMinute, values) => {
+ if (!values.date.day || !values.date.start_time || !values.date.end_time)
+ return true
+
+ const [startHour, startMinute] = values.date.start_time
+ const [endHour, _] = values.date.end_time
+
+ if (!startHour || !startMinute || !endHour) return true
+
+ const [startTimeNumberFormat, endTimeNumberFormat] = [
+ Number(`${startHour}${startMinute}`),
+ Number(`${endHour}${endMinute}`),
+ ]
+
+ if (endTimeNumberFormat < startTimeNumberFormat) {
+ /*
+ 11:30 => Number('1130') => 1130
+ 00:30 => Number('0030') => 30
+ - 시작시간, 종료시간 문자 포멧을 숫자로 변환하여
+ 대소를 비교해도 된다고 판단
+ (일은 동일하기 때문에 dayjs 연산이 필요하지 않다고 판단)
+ */
+
+ return "모임 종료 시간은 시작 시간 이후여야 합니다"
+ }
+
+ if (startTimeNumberFormat === endTimeNumberFormat) {
+ return "모임 시작 시간과 종료 시간은 서로 다르게 설정해야 합니다"
+ }
+
+ return true
+ },
+}
diff --git a/src/page/coding-meetings/create/controls/rules/date/start-time/coding-meeting-start-hour-rules.ts b/src/page/coding-meetings/create/controls/rules/date/start-time/coding-meeting-start-hour-rules.ts
new file mode 100644
index 00000000..b1a4bac9
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/rules/date/start-time/coding-meeting-start-hour-rules.ts
@@ -0,0 +1,24 @@
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { RegisterOptions } from "react-hook-form"
+
+type CodingMeetingStartHourRules = Omit<
+ RegisterOptions,
+ "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
+>
+
+export const codingMeetingStartHourRules: CodingMeetingStartHourRules = {
+ required: "모임 시작시간의 시(hour)를 선택해주세요",
+ validate: (startHour, values) => {
+ if (!values.date.end_time) return true
+
+ const [endHour, _] = values.date.end_time
+
+ if (!endHour) return true
+
+ if (Number(startHour) > Number(endHour)) {
+ return "모임 시작 시간은 종료 시간 이전이어야 합니다"
+ }
+
+ return true
+ },
+}
diff --git a/src/page/coding-meetings/create/controls/rules/date/start-time/coding-meeting-start-minute-rules.ts b/src/page/coding-meetings/create/controls/rules/date/start-time/coding-meeting-start-minute-rules.ts
new file mode 100644
index 00000000..cbe6a904
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/rules/date/start-time/coding-meeting-start-minute-rules.ts
@@ -0,0 +1,43 @@
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { RegisterOptions } from "react-hook-form"
+
+type CodingMeetingStartMinuteRules = Omit<
+ RegisterOptions,
+ "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
+>
+
+export const codingMeetingStartMinuteRules: CodingMeetingStartMinuteRules = {
+ required: "모임 시작시간의 분(minute)을 선택해주세요",
+ validate: (startMinute, values) => {
+ if (!values.date.day || !values.date.start_time || !values.date.end_time)
+ return true
+
+ const [startHour, _] = values.date.start_time
+ const [endHour, endMinute] = values.date.end_time
+
+ if (!endHour || !endMinute || !startHour) return true
+
+ const [startTimeNumberFormat, endTimeNumberFormat] = [
+ Number(`${startHour}${startMinute}`),
+ Number(`${endHour}${endMinute}`),
+ ]
+
+ if (startTimeNumberFormat > endTimeNumberFormat) {
+ /*
+ 11:30 => Number('1130') => 1130
+ 00:30 => Number('0030') => 30
+ - 시작시간, 종료시간 문자 포멧을 숫자로 변환하여
+ 대소를 비교해도 된다고 판단
+ (일은 동일하기 때문에 dayjs 연산이 필요하지 않다고 판단)
+ */
+
+ return "모임 시작 시간은 종료 시간 이전이어야 합니다"
+ }
+
+ if (startTimeNumberFormat === endTimeNumberFormat) {
+ return "모임 시작 시간과 종료 시간은 서로 다르게 설정해야 합니다"
+ }
+
+ return true
+ },
+}
diff --git a/src/page/coding-meetings/create/controls/rules/location/location-rules.ts b/src/page/coding-meetings/create/controls/rules/location/location-rules.ts
new file mode 100644
index 00000000..093077de
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/rules/location/location-rules.ts
@@ -0,0 +1,9 @@
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { RegisterOptions } from "react-hook-form"
+
+export const locationRules: Omit<
+ RegisterOptions,
+ "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
+> = {
+ required: "모임 위치를 설정해주세요",
+}
diff --git a/src/page/coding-meetings/create/controls/rules/location/location-search-rules.ts b/src/page/coding-meetings/create/controls/rules/location/location-search-rules.ts
new file mode 100644
index 00000000..726ec5a1
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/rules/location/location-search-rules.ts
@@ -0,0 +1,11 @@
+import { CodingMeetingLocationSearchFormData } from "@/interfaces/form"
+import { RegisterOptions } from "react-hook-form"
+
+type LocationSearchRules = Omit<
+ RegisterOptions,
+ "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
+>
+
+export const locationSearchRules: LocationSearchRules = {
+ required: true,
+}
diff --git a/src/page/coding-meetings/create/controls/rules/member-count/member-count-rules.ts b/src/page/coding-meetings/create/controls/rules/member-count/member-count-rules.ts
new file mode 100644
index 00000000..33b75ccb
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/rules/member-count/member-count-rules.ts
@@ -0,0 +1,20 @@
+import { CODING_MEETING_LIMITS } from "@/constants/limitation"
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { RegisterOptions } from "react-hook-form"
+
+type MemberCountRules = Omit<
+ RegisterOptions,
+ "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
+>
+
+export const memberCountRules: MemberCountRules = {
+ required: "모임 인원을 설정해 주세요",
+ min: {
+ value: CODING_MEETING_LIMITS.memberCount.min,
+ message: "모임 인원은 본인 포함 최소 3명부터 가능합니다",
+ },
+ max: {
+ value: CODING_MEETING_LIMITS.memberCount.max,
+ message: "모임 인원은 본인 포함 최대 6명까지 가능합니다",
+ },
+}
diff --git a/src/page/coding-meetings/create/controls/rules/title/title-rules.ts b/src/page/coding-meetings/create/controls/rules/title/title-rules.ts
new file mode 100644
index 00000000..93aa9927
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/rules/title/title-rules.ts
@@ -0,0 +1,21 @@
+import { CODING_MEETING_LIMITS } from "@/constants/limitation"
+import { validationMessage } from "@/constants/message/validation"
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { RegisterOptions } from "react-hook-form"
+
+type CodingMeetingTitleRules = Omit<
+ RegisterOptions,
+ "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
+>
+
+export const codingMeetingTitleRules: CodingMeetingTitleRules = {
+ required: validationMessage.notitle,
+ minLength: {
+ value: CODING_MEETING_LIMITS.title.minLength,
+ message: validationMessage.underTitleLimit,
+ },
+ maxLength: {
+ value: CODING_MEETING_LIMITS.title.maxLength,
+ message: validationMessage.overTitleLimit,
+ },
+}
diff --git a/src/page/coding-meetings/create/controls/start-time/CodingMeetingStartHourController.tsx b/src/page/coding-meetings/create/controls/start-time/CodingMeetingStartHourController.tsx
new file mode 100644
index 00000000..9c5761e1
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/start-time/CodingMeetingStartHourController.tsx
@@ -0,0 +1,77 @@
+"use client"
+
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectTrigger,
+} from "@/components/ui/Select"
+import { CODING_MEETING_HOURS } from "@/constants/timeOptions"
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { SelectValue } from "@radix-ui/react-select"
+import { useId } from "react"
+import { useController, useFormContext } from "react-hook-form"
+import { codingMeetingStartHourRules } from "../rules/date/start-time/coding-meeting-start-hour-rules"
+
+interface CodingMeetingStartHourControllerProps {
+ initialStartHour?: CodingMeetingFormData["date"]["start_time"][0]
+}
+
+function CodingMeetingStartHourController({
+ initialStartHour,
+}: CodingMeetingStartHourControllerProps) {
+ const { control } = useFormContext()
+ const { field } = useController({
+ control,
+ name: "date.start_time.0",
+ rules: codingMeetingStartHourRules,
+ defaultValue: initialStartHour,
+ })
+
+ const seperatorId = useId()
+
+ return (
+
+ )
+}
+
+export default CodingMeetingStartHourController
diff --git a/src/page/coding-meetings/create/controls/start-time/CodingMeetingStartMinuteController.tsx b/src/page/coding-meetings/create/controls/start-time/CodingMeetingStartMinuteController.tsx
new file mode 100644
index 00000000..5410491a
--- /dev/null
+++ b/src/page/coding-meetings/create/controls/start-time/CodingMeetingStartMinuteController.tsx
@@ -0,0 +1,59 @@
+"use client"
+
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { useController, useFormContext } from "react-hook-form"
+import { codingMeetingStartMinuteRules } from "../rules/date/start-time/coding-meeting-start-minute-rules"
+import { useId } from "react"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/Select"
+import { CODING_MEETING_MINUTES } from "@/constants/timeOptions"
+
+interface CodingMeetingStartMinuteControllerProps {
+ initialStartMinute?: CodingMeetingFormData["date"]["start_time"][1]
+}
+
+function CodingMeetingStartMinuteController({
+ initialStartMinute,
+}: CodingMeetingStartMinuteControllerProps) {
+ const { control } = useFormContext()
+ const { field } = useController({
+ control,
+ name: "date.start_time.1",
+ rules: codingMeetingStartMinuteRules,
+ defaultValue: initialStartMinute,
+ })
+
+ const seperatorId = useId()
+
+ return (
+
+ )
+}
+
+export default CodingMeetingStartMinuteController
diff --git a/src/page/coding-meetings/create/hooks/useCreateCodingMeeting.tsx b/src/page/coding-meetings/create/hooks/useCreateCodingMeeting.tsx
new file mode 100644
index 00000000..97e1c4eb
--- /dev/null
+++ b/src/page/coding-meetings/create/hooks/useCreateCodingMeeting.tsx
@@ -0,0 +1,113 @@
+"use client"
+
+import { errorMessage } from "@/constants/message/error"
+import queryKey from "@/constants/queryKey"
+import { APIResponse } from "@/interfaces/dto/api-response"
+import {
+ CreateCodingMeetingRequest,
+ CreateCodingMeetingResponse,
+} from "@/interfaces/dto/coding-meeting/create-coding-meeting.dto"
+import { createCodingMeeting } from "@/service/coding-meetings"
+import { revalidatePage } from "@/util/actions/revalidatePage"
+import { useMutation, useQueryClient } from "@tanstack/react-query"
+import { AxiosError, AxiosResponse, HttpStatusCode } from "axios"
+import { useRouter } from "next/navigation"
+import { toast } from "react-toastify"
+import { serializeCmToken } from "../../util/cm-token"
+import { useFormContext } from "react-hook-form"
+import { CodingMeetingFormData } from "@/interfaces/form"
+
+interface UseCreateCodingMeeting {
+ handleManual?: boolean
+ onSuccess?: (
+ data: AxiosResponse,
+ variables: CreateCodingMeetingRequest,
+ context: unknown,
+ ) => void
+ onError?: (
+ error: Error | AxiosError,
+ variables: CreateCodingMeetingRequest,
+ context: unknown,
+ ) => void
+}
+
+export function useCreateCodingMeeting({
+ handleManual,
+ onSuccess,
+ onError,
+}: UseCreateCodingMeeting = {}) {
+ const queryClient = useQueryClient()
+ const { replace } = useRouter()
+
+ const { reset } = useFormContext()
+
+ const { mutate, status } = useMutation({
+ mutationKey: [queryKey.codingMeeting, "create"],
+ mutationFn: (request: CreateCodingMeetingRequest) =>
+ createCodingMeeting({ ...request }),
+ onSuccess(response, variables, context) {
+ if (handleManual) {
+ onSuccess && onSuccess(response, variables, context)
+
+ return
+ }
+
+ reset()
+
+ const payload = response.data.data
+
+ queryClient.invalidateQueries({
+ queryKey: [queryKey.codingMeeting],
+ })
+
+ replace(
+ `/coding-meetings/${
+ payload?.coding_meeting_token
+ ? serializeCmToken(payload.coding_meeting_token)
+ : ""
+ }`,
+ )
+ },
+ onError(error, variables, context) {
+ if (handleManual) {
+ onError && onError(error, variables, context)
+
+ return
+ }
+
+ const toastId = "codingMeetingCreateError"
+
+ if (error instanceof AxiosError) {
+ const { response } = error as AxiosError
+
+ if (response?.status === HttpStatusCode.Unauthorized) {
+ revalidatePage("/coding-meetings/create", "page")
+ setTimeout(() => {
+ toast.error("로그인 후 다시 작성해주세요", {
+ position: "top-center",
+ })
+ }, 0)
+
+ return
+ }
+
+ toast.error(response?.data.msg ?? errorMessage.createCodingMeeting, {
+ position: "top-center",
+ toastId,
+ })
+
+ return
+ }
+
+ toast.error(errorMessage.createCodingMeeting, {
+ position: "top-center",
+ toastId,
+ })
+ },
+ })
+
+ return {
+ createCodingMeetingApi: mutate,
+ createCodingMeetingApiStatus: status,
+ }
+}
diff --git a/src/page/coding-meetings/create/hooks/useHandleCreateCodingMeetingTime.tsx b/src/page/coding-meetings/create/hooks/useHandleCreateCodingMeetingTime.tsx
deleted file mode 100644
index cd62aa77..00000000
--- a/src/page/coding-meetings/create/hooks/useHandleCreateCodingMeetingTime.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-"use client"
-
-import {
- CodingMeetingDay,
- EndTime,
- StartTime,
- type Time,
-} from "@/recoil/atoms/coding-meeting/dateTime"
-import { formatDay } from "@/util/getDate"
-import dayjs from "dayjs"
-import utc from "dayjs/plugin/utc"
-
-import { useRecoilState } from "recoil"
-
-const useHandleCreateCodingMeetingTime = () => {
- const [day, setDay] = useRecoilState(CodingMeetingDay)
- const [startTime, setStartTime] = useRecoilState(StartTime)
- const [endTime, setEndTime] = useRecoilState(EndTime)
-
- const formattedDay = formatDay(day + "")
- const formatMinute = (minute: number | string) =>
- String(minute).length === 1 ? "0" + String(minute) : minute + ""
-
- const formatTime = ({ hour, minute }: Time) => `${hour}:${minute}`
-
- const formatByUTC = (time: string) => {
- dayjs.extend(utc)
- return dayjs(`${formattedDay} ${time}`).utc().format().slice(0, -1)
- }
-
- const resetTimes = () => {
- setStartTime({
- hour: "",
- minute: "",
- })
- setEndTime({
- hour: "",
- minute: "",
- })
- }
-
- const formattedStartTime = dayjs(`${formattedDay} ${formatTime(startTime)}`)
- const formattedEndTime = dayjs(`${formattedDay} ${formatTime(endTime)}`)
-
- const resetDateTimes = () => {
- setDay(new Date())
- setStartTime({
- hour: "",
- minute: "",
- })
- setEndTime({
- hour: "",
- minute: "",
- })
- }
-
- return {
- startTime,
- endTime,
- setStartTime,
- setEndTime,
- formatMinute,
- formatTime,
- formatByUTC,
- resetTimes,
- resetDateTimes,
- formattedStartTime,
- formattedEndTime,
- }
-}
-
-export default useHandleCreateCodingMeetingTime
diff --git a/src/page/coding-meetings/create/hooks/useKakaoMapPlaceApi.tsx b/src/page/coding-meetings/create/hooks/useKakaoMapPlaceApi.tsx
new file mode 100644
index 00000000..681f4064
--- /dev/null
+++ b/src/page/coding-meetings/create/hooks/useKakaoMapPlaceApi.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { useKakaoLoader } from "react-kakao-maps-sdk"
+
+export function useKakaoMapPlaceApi() {
+ const [loading, error] = useKakaoLoader({
+ appkey: process.env.NEXT_PUBLIC_KAKAO_MAP!,
+ libraries: ["services"],
+ })
+
+ const [kakaoPlaceApi, setKakaoPlaceApi] =
+ useState(null)
+
+ useEffect(() => {
+ if (loading || error) {
+ return
+ }
+
+ if (kakaoPlaceApi) return
+
+ setKakaoPlaceApi(new kakao.maps.services.Places())
+ }, [loading, error, kakaoPlaceApi])
+
+ return {
+ loading,
+ error,
+ kakaoPlaceApi,
+ }
+}
diff --git a/src/page/coding-meetings/create/hooks/useUpdateCodingMeeting.tsx b/src/page/coding-meetings/create/hooks/useUpdateCodingMeeting.tsx
new file mode 100644
index 00000000..a0a991e5
--- /dev/null
+++ b/src/page/coding-meetings/create/hooks/useUpdateCodingMeeting.tsx
@@ -0,0 +1,112 @@
+"use client"
+
+import { errorMessage } from "@/constants/message/error"
+import queryKey from "@/constants/queryKey"
+import { APIResponse } from "@/interfaces/dto/api-response"
+import {
+ UpdateCodingMeetingRequest,
+ UpdateCodingMeetingResponse,
+} from "@/interfaces/dto/coding-meeting/update-coding-meeting.dto"
+import { updateCodingMeeting } from "@/service/coding-meetings"
+import { revalidatePage } from "@/util/actions/revalidatePage"
+import { useMutation, useQueryClient } from "@tanstack/react-query"
+import { AxiosError, AxiosResponse, HttpStatusCode } from "axios"
+import { useRouter } from "next/navigation"
+import { toast } from "react-toastify"
+import { serializeCmToken } from "../../util/cm-token"
+import { useFormContext } from "react-hook-form"
+import { CodingMeetingFormData } from "@/interfaces/form"
+
+interface UseUpdateCodingMeeting {
+ handleManual?: boolean
+ onSuccess?: (
+ data: AxiosResponse,
+ variables: UpdateCodingMeetingRequest,
+ context: unknown,
+ ) => void
+ onError?: (
+ error: Error | AxiosError,
+ variables: UpdateCodingMeetingRequest,
+ context: unknown,
+ ) => void
+}
+
+export function useUpdateCodingMeeting({
+ handleManual,
+ onSuccess,
+ onError,
+}: UseUpdateCodingMeeting = {}) {
+ const queryClient = useQueryClient()
+ const { replace } = useRouter()
+
+ const { reset } = useFormContext()
+
+ const { mutate, status } = useMutation({
+ mutationKey: [queryKey.codingMeeting, "update"],
+ mutationFn: (request: UpdateCodingMeetingRequest) =>
+ updateCodingMeeting({ ...request }),
+ async onSuccess(response, variables, context) {
+ if (handleManual) {
+ onSuccess && onSuccess(response, variables, context)
+ return
+ }
+
+ reset()
+
+ queryClient.resetQueries({
+ queryKey: [queryKey.codingMeeting],
+ })
+
+ await revalidatePage("/coding-meetings/[token]", "page")
+
+ setTimeout(() => {
+ replace(
+ `/coding-meetings/${serializeCmToken(
+ variables.coding_meeting_token,
+ )}`,
+ )
+ }, 0)
+ },
+ onError(error, variables, context) {
+ if (handleManual) {
+ onError && onError(error, variables, context)
+ return
+ }
+
+ const toastId = "codingMeetingUpdateError"
+
+ if (error instanceof AxiosError) {
+ const { response } = error as AxiosError
+
+ if (response?.status === HttpStatusCode.Unauthorized) {
+ revalidatePage("/coding-meetings/post/[token]", "page")
+
+ setTimeout(() => {
+ toast.error("로그인 후 다시 작성해주세요", {
+ position: "top-center",
+ })
+ }, 0)
+
+ return
+ }
+
+ toast.error(response?.data.msg ?? errorMessage.updateCodingMeeting, {
+ position: "top-center",
+ toastId,
+ })
+
+ return
+ }
+
+ toast.error(errorMessage.createCodingMeeting, {
+ position: "top-center",
+ toastId,
+ })
+ },
+ })
+
+ return {
+ updateCodingMeetingApi: mutate,
+ updateCodingMeetingApiStatus: status,
+ }
+}
diff --git a/src/page/coding-meetings/util/parser.ts b/src/page/coding-meetings/util/parser.ts
new file mode 100644
index 00000000..5b6cc568
--- /dev/null
+++ b/src/page/coding-meetings/util/parser.ts
@@ -0,0 +1,112 @@
+import { CreateCodingMeetingRequest } from "@/interfaces/dto/coding-meeting/create-coding-meeting.dto"
+import { CodingMeetingDetailPayload } from "@/interfaces/dto/coding-meeting/get-coding-meeting-detail.dto"
+import { UpdateCodingMeetingRequest } from "@/interfaces/dto/coding-meeting/update-coding-meeting.dto"
+import { CodingMeetingFormData } from "@/interfaces/form"
+import { getKorDayjs } from "@/util/getDate"
+
+type Action = "create" | "update"
+
+export function formDataToPayload(
+ action: "create",
+ formData: CodingMeetingFormData,
+): CreateCodingMeetingRequest
+export function formDataToPayload(
+ action: "update",
+ formData: CodingMeetingFormData,
+ codingMeetingToken: string,
+): UpdateCodingMeetingRequest
+export function formDataToPayload(
+ action: Action,
+ formData: CodingMeetingFormData,
+ codingMeetingToken?: string,
+) {
+ const { title, location, member_upper_limit, date, hashtags, content } =
+ formData
+
+ const dayInstance = getKorDayjs(date.day)
+ const [startHour, startMinute] = date.start_time
+ const [endHour, endMinute] = date.end_time
+
+ const basePayload = {
+ coding_meeting_title: title,
+ coding_meeting_location_id: location.id,
+ coding_meeting_location_place_name: location.place_name,
+ coding_meeting_location_longitude: location.longitude,
+ coding_meeting_location_latitude: location.latitude,
+ coding_meeting_member_upper_limit: member_upper_limit,
+ coding_meeting_start_time: dayInstance
+ .clone()
+ .set("hours", Number(startHour))
+ .set("minutes", Number(startMinute))
+ .toISOString()
+ .replace(/Z$/, ""),
+ coding_meeting_end_time: dayInstance
+ .clone()
+ .set("hours", Number(endHour))
+ .set("minutes", Number(endMinute))
+ .toISOString()
+ .replace(/Z$/, ""),
+ coding_meeting_hashtags: hashtags.map(({ tag }) => tag),
+ coding_meeting_content: content,
+ }
+
+ if (action === "create") {
+ return {
+ ...basePayload,
+ } as CreateCodingMeetingRequest
+ }
+
+ return {
+ ...basePayload,
+ coding_meeting_token: codingMeetingToken,
+ } as UpdateCodingMeetingRequest
+}
+
+export function payloadToFormData(
+ payload: CodingMeetingDetailPayload,
+): CodingMeetingFormData {
+ return {
+ title: payload.coding_meeting_title,
+ content: payload.coding_meeting_content,
+ member_upper_limit: payload.coding_meeting_member_upper_limit,
+ date: {
+ ...parseCodingMeetingDate({
+ startTime: payload.coding_meeting_start_time,
+ endTime: payload.coding_meeting_end_time,
+ }),
+ },
+ location: {
+ id: payload.coding_meeting_location_id,
+ longitude: payload.coding_meeting_location_longitude,
+ latitude: payload.coding_meeting_location_latitude,
+ place_name: payload.coding_meeting_location_place_name,
+ },
+ hashtags: payload.coding_meeting_hashtags.map((hashtag) => ({
+ tag: hashtag,
+ })),
+ }
+}
+
+const parseCodingMeetingDate = ({
+ startTime,
+ endTime,
+}: {
+ startTime: string
+ endTime: string
+}) => {
+ const day = getKorDayjs(startTime).startOf("days").toDate()
+ const start_time = [
+ getKorDayjs(startTime).format("HH"),
+ getKorDayjs(startTime).format("mm"),
+ ]
+ const end_time = [
+ getKorDayjs(endTime).format("HH"),
+ getKorDayjs(endTime).format("mm"),
+ ]
+
+ return {
+ day,
+ start_time,
+ end_time,
+ } as CodingMeetingFormData["date"]
+}
diff --git a/src/page/coffee-chat/create/CreateCoffeeChatReservationPage.tsx b/src/page/coffee-chat/create/CreateCoffeeChatReservationPage.tsx
index 0eb72b8f..42c2ae89 100644
--- a/src/page/coffee-chat/create/CreateCoffeeChatReservationPage.tsx
+++ b/src/page/coffee-chat/create/CreateCoffeeChatReservationPage.tsx
@@ -1,7 +1,6 @@
"use client"
import { useForm } from "react-hook-form"
-import Spacing from "@/components/shared/Spacing"
import { useEffect } from "react"
import { toast } from "react-toastify"
import { useClientSession } from "@/hooks/useClientSession"
@@ -20,9 +19,9 @@ import {
import TitleSection from "./components/sections/title/TitleSection"
import IntroductionSection from "./components/sections/introduction/IntroductionSection"
import ContentSection from "./components/sections/content/ContentSection"
-import { pickError } from "./controls/util/form"
import { useCreateCoffeeChat } from "./hooks/useCreateCoffeeChat"
import LinkToListPage from "@/components/LinkToListPage"
+import { pickFirstError } from "@/util/hook-form/error"
export interface CoffeeChatFormProps {
initialValues?: CoffeeChatEditorInitialValues
@@ -91,7 +90,7 @@ function CreateCoffeeChatReservationPage({
각 필드(title, introduction...)에 대한 리액트 훅 폼 에러메시지를 설정했기 때문에,
에러 객체의 message를 그대로 활용
*/
- const { type, message } = pickError(errors)
+ const { type, message } = pickFirstError(errors)
toast.error(message, {
toastId: `${type}-${message}`,
diff --git a/src/page/coffee-chat/create/components/sections/content/ContentSection.tsx b/src/page/coffee-chat/create/components/sections/content/ContentSection.tsx
index 905a1fb9..60386a92 100644
--- a/src/page/coffee-chat/create/components/sections/content/ContentSection.tsx
+++ b/src/page/coffee-chat/create/components/sections/content/ContentSection.tsx
@@ -33,8 +33,8 @@ function CoffeeChatContentSection({ control }: CoffeeChatContentSectionProps) {
diff --git a/src/page/coffee-chat/create/components/sections/date/TimeOptions.tsx b/src/page/coffee-chat/create/components/sections/date/TimeOptions.tsx
index 3de01a6b..2cf61e40 100644
--- a/src/page/coffee-chat/create/components/sections/date/TimeOptions.tsx
+++ b/src/page/coffee-chat/create/components/sections/date/TimeOptions.tsx
@@ -1,6 +1,10 @@
import TimeZoneSection from "./TimeZoneSection"
import { useState } from "react"
-import { AM, PM, TimeZone } from "@/constants/timeOptions"
+import {
+ CODING_MEETING_AM_OPTIONS,
+ CODING_MEETING_PM_OPTIONS,
+ TimeZone,
+} from "@/constants/timeOptions"
import TimeOptionButton from "../../TimeOptionButton"
import Spacing from "@/components/shared/Spacing"
import dayjs from "dayjs"
@@ -12,7 +16,10 @@ interface TimeOptionsProps {
function TimeOptions({ day }: TimeOptionsProps) {
const [timeZone, setTimeZone] = useState(TimeZone.AM)
- const targetOptions = timeZone === TimeZone.AM ? AM : PM
+ const targetOptions =
+ timeZone === TimeZone.AM
+ ? CODING_MEETING_AM_OPTIONS
+ : CODING_MEETING_PM_OPTIONS
return (
diff --git a/src/page/coffee-chat/create/controls/rules/chat-content-rules.ts b/src/page/coffee-chat/create/controls/rules/chat-content-rules.ts
index 698c21ab..9780c7a5 100644
--- a/src/page/coffee-chat/create/controls/rules/chat-content-rules.ts
+++ b/src/page/coffee-chat/create/controls/rules/chat-content-rules.ts
@@ -11,11 +11,11 @@ type ChatContentRules = Omit<
export const chatContentRules: ChatContentRules = {
required: validationMessage.noContent,
minLength: {
- value: Limitation.content_limit_under,
- message: validationMessage.underContentLimit,
+ value: Limitation.chat_content_min_length,
+ message: validationMessage.chatContentLength,
},
maxLength: {
- value: Limitation.content_limit_over,
- message: validationMessage.overContentLimit,
+ value: Limitation.chat_content_max_length,
+ message: validationMessage.chatContentLength,
},
}
diff --git a/src/page/coffee-chat/create/controls/rules/hashtag-rules.ts b/src/page/coffee-chat/create/controls/rules/hashtag-rules.ts
index bb2e18d7..75f86d89 100644
--- a/src/page/coffee-chat/create/controls/rules/hashtag-rules.ts
+++ b/src/page/coffee-chat/create/controls/rules/hashtag-rules.ts
@@ -16,6 +16,7 @@ export type HashTagListField = ControllerRenderProps<
export const enum HashTagRuleValidateType {
"InvalidFormat" = "invalidFormat",
+ "WhiteSpace" = "invalidWhiteSpace",
}
export const enum HashTagListRuleValidateType {
@@ -42,6 +43,13 @@ export const hashTagRules: ({
return validationMessage.preventSpecialCharacter
},
+ [HashTagRuleValidateType.WhiteSpace]: (hashTag) => {
+ if (validatorInstance.validateHashTag(hashTag).whiteSpace()) {
+ return true
+ }
+
+ return "해시태그는 공백만 포함할 수 없습니다."
+ },
[HashTagListRuleValidateType.Duplicate]: (hashTag) => {
if (
validatorInstance.validateHashTagList(hashTagList).duplicate(hashTag)
diff --git a/src/page/coffee-chat/create/controls/util/form.ts b/src/page/coffee-chat/create/controls/util/form.ts
deleted file mode 100644
index 5fff0a98..00000000
--- a/src/page/coffee-chat/create/controls/util/form.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { CoffeeChatFormData } from "@/interfaces/form"
-import { FieldError, FieldErrors, Merge } from "react-hook-form"
-
-type ErrorType =
- | FieldError
- | Merge
- | (Record<
- string,
- Partial<{
- type: string | number
- message: string
- }>
- > &
- Partial<{
- type: string | number
- message: string
- }>)
-
-export function pickError(errors: FieldErrors): ErrorType {
- return (errors?.title ??
- errors?.introduction ??
- errors?.content ??
- errors?.hashTags ??
- errors?.dateTimes ??
- errors?.root)!
-}
diff --git a/src/page/coffee-chat/detail/reservation/calendar-base/ReservationCalendarBase.tsx b/src/page/coffee-chat/detail/reservation/calendar-base/ReservationCalendarBase.tsx
index 18eb9514..ad3662c6 100644
--- a/src/page/coffee-chat/detail/reservation/calendar-base/ReservationCalendarBase.tsx
+++ b/src/page/coffee-chat/detail/reservation/calendar-base/ReservationCalendarBase.tsx
@@ -6,28 +6,39 @@ import { getDay, getHoliday } from "@/util/getDate"
import dayjs from "dayjs"
import { Value } from "./Calendar.types"
import ReactCalendarTileContent from "@/components/react-calendar/ReactCalendarTileContent"
+import { ForwardedRef, forwardRef } from "react"
+import { twMerge } from "tailwind-merge"
type ReservationCalendarBaseProps = {
- start: Date
- limit: number
+ start?: Date
+ limit?: number
date: Value
onDateChange?: CalendarProps["onChange"]
tileClassName?: CalendarProps["tileClassName"]
minDate?: Date
maxDate?: Date
+ tileDisabled?: CalendarProps["tileDisabled"]
+ wrapperClassName?: string
}
-const ReservationCalendarBase = ({
- start,
- limit,
- date,
- onDateChange,
- tileClassName,
- minDate,
- maxDate,
-}: ReservationCalendarBaseProps) => {
+const ReservationCalendarBase = (
+ {
+ start,
+ limit,
+ date,
+ onDateChange,
+ tileClassName,
+ tileDisabled,
+ minDate,
+ maxDate,
+ wrapperClassName,
+ }: ReservationCalendarBaseProps,
+ ref: ForwardedRef,
+) => {
// 종료 날짜
- const calendarValue = new Date(dayjs(start).add(limit, "day").format())
+ const calendarValue = limit
+ ? new Date(dayjs(start).add(limit, "day").format())
+ : undefined
const tileClassNames = ({ date, activeStartDate, view }: TileArgs) => {
const holiday = getHoliday(date)
@@ -55,8 +66,14 @@ const ReservationCalendarBase = ({
return holiday ? "holiday" : undefined
}
+ const wrapperClassNames = twMerge([
+ "focus:outline focus:outline-1 focus:outline-blue-400/40",
+ wrapperClassName,
+ "react-calendar",
+ ])
+
return (
-
+
)
}
-export default ReservationCalendarBase
+export default forwardRef
(
+ ReservationCalendarBase,
+)
diff --git a/src/react-query/coding-meeting.ts b/src/react-query/coding-meeting.ts
deleted file mode 100644
index 62f05ee4..00000000
--- a/src/react-query/coding-meeting.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import queryKey from "@/constants/queryKey"
-import type { CreateCodingMeetingRequest } from "@/interfaces/dto/coding-meeting/create-coding-meeting.dto"
-import { UpdateCodingMeetingRequest } from "@/interfaces/dto/coding-meeting/update-coding-meeting.dto"
-import {
- createCodingMeeting,
- updateCodingMeeting,
-} from "@/service/coding-meetings"
-import { useMutation } from "@tanstack/react-query"
-
-// 모각코 등록글 생성
-const useCreateCodingMeeting = () => {
- const {
- data,
- mutate: createCodingMeetingMutate,
- isPending: isCreateCodingMeeting,
- isError: isCreateCodingMeetingPostError,
- isSuccess: isCreateCodingMeetingPostSuccess,
- } = useMutation({
- mutationKey: [queryKey.codingMeeting],
- mutationFn: (createPayload: CreateCodingMeetingRequest) =>
- createCodingMeeting({
- ...createPayload,
- }),
- })
-
- return {
- createCodingMeetingPostResponse: data,
- createCodingMeetingPost: createCodingMeetingMutate,
- createCodingMeetingPostStatus: {
- isCreateCodingMeeting,
- isCreateCodingMeetingPostError,
- isCreateCodingMeetingPostSuccess,
- },
- }
-}
-
-// 모각코 등록글 수정
-const useUpdateCodingMeeting = () => {
- const {
- data,
- mutate: updateCodingMeetingMutate,
- isPending: isUpdateCodingMeeting,
- isError: isUpdateCodingMeetingError,
- isSuccess: isUpdateCodingMeetingSuccess,
- } = useMutation({
- mutationKey: [queryKey.codingMeeting],
- mutationFn: (updatePayload: UpdateCodingMeetingRequest) =>
- updateCodingMeeting({ ...updatePayload }),
- })
-
- return {
- updateCodingMeetingResponse: data,
- updateCodingMeeting: updateCodingMeetingMutate,
- updateCodingMeetingStatus: {
- isUpdateCodingMeeting,
- isUpdateCodingMeetingError,
- isUpdateCodingMeetingSuccess,
- },
- }
-}
-
-export const CodingMeetingQueries = {
- useCreateCodingMeeting,
- useUpdateCodingMeeting,
-}
diff --git a/src/recoil/atoms/coding-meeting/dateTime.tsx b/src/recoil/atoms/coding-meeting/dateTime.tsx
deleted file mode 100644
index 4bb59bf1..00000000
--- a/src/recoil/atoms/coding-meeting/dateTime.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Value } from "@/interfaces/calendar"
-import { atom } from "recoil"
-
-export type Time = {
- hour: string
- minute: string
-}
-
-export const StartTime = atom