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" ? "모각코 모집하기" : "모각코 수정하기"} +

- - - 제목 - -
- -
- {watch("title") && - (watch("title")?.length < Limitation.title_limit_under || - watch("title")?.length > Limitation.title_limit_over) && ( - - {"제목은 5자 이상 100자 이하여야 합니다."} - - )} -
-
-
- - - - - - - - - - - - 소개글 - -
-