diff --git a/package.json b/package.json index b5d68f4..e94b92d 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,12 @@ "axios": "^1.7.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "framer-motion": "^12.4.4", "lucide-react": "^0.473.0", "path-browserify": "^1.0.1", "react": "^18.3.1", + "react-day-picker": "8.10.1", "react-dom": "^18.3.1", "react-icons": "^5.5.0", "react-router-dom": "^6.28.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 854cade..5823d79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 framer-motion: specifier: ^12.4.4 version: 12.4.4(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -47,6 +50,9 @@ importers: react: specifier: ^18.3.1 version: 18.3.1 + react-day-picker: + specifier: 8.10.1 + version: 8.10.1(date-fns@4.1.0)(react@18.3.1) react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) @@ -1727,6 +1733,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -2906,6 +2915,12 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-day-picker@8.10.1: + resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -5382,6 +5397,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + date-fns@4.1.0: {} + debug@4.4.0: dependencies: ms: 2.1.3 @@ -6632,6 +6649,11 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-day-picker@8.10.1(date-fns@4.1.0)(react@18.3.1): + dependencies: + date-fns: 4.1.0 + react: 18.3.1 + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 diff --git a/src/app/routes/Router.tsx b/src/app/routes/Router.tsx index c5c4eec..32ccdd7 100644 --- a/src/app/routes/Router.tsx +++ b/src/app/routes/Router.tsx @@ -12,6 +12,7 @@ import SignInPage from '@/auth/pages/SignInPage'; import SignUpPage from '@/auth/pages/SignUpPage'; import SignupFinish from '@/auth/components/signup/SignupFinish'; import MentorInfoView from '@/mentorship/pages/MentorInfoView'; +import Suggestion from '@/mentorship/pages/Suggestion'; import MentorInfo from '@/user/pages/ProfileEdit/MentorInfo'; import MentorProfileEdit from '@/user/pages/ProfileEdit/MentorProfileEdit'; import MenteeInfo from '@/user/pages/ProfileEdit/MenteeInfo'; @@ -54,6 +55,7 @@ export const Router = () => { }> } /> } /> + } /> ); diff --git a/src/global/ui/Button.tsx b/src/global/ui/Button.tsx index b4007fd..ac206da 100644 --- a/src/global/ui/Button.tsx +++ b/src/global/ui/Button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/global/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:bg-primary-2 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { diff --git a/src/global/ui/calendar.tsx b/src/global/ui/calendar.tsx new file mode 100644 index 0000000..b3db30b --- /dev/null +++ b/src/global/ui/calendar.tsx @@ -0,0 +1,96 @@ +import * as React from "react" +import { useState } from "react" +import { ChevronLeft, ChevronRight } from "lucide-react" +import { DayPicker } from "react-day-picker" +import { ko } from "date-fns/locale" + +import { cn } from "@/global/lib/utils" +import { buttonVariants } from "@/global/ui/button" + +export type CalendarProps = React.ComponentProps + +const koreanWeekdays = ["일", "월", "화", "수", "목", "금", "토"] + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + const [currentMonth, setCurrentMonth] = useState(new Date()); + + return ( + date.getMonth() !== currentMonth.getMonth()} + classNames={{ + months: "w-full flex flex-col space-y-4", + month: "space-y-4", + caption: "flex justify-between py-0 relative items-center", + caption_label: "text-[16px] font-medium", + nav: "ml-auto space-x-2 flex items-center", + nav_button: cn( + "h-6 w-6 bg-transparent text-[#94939B]" + ), + nav_button_previous: "", + nav_button_next: "", + table: "w-full border-collapse space-y-1", + head_row: "flex justify-between pb-2 px-2", + head_cell: "w-10 text-[12px] text-[#878787] font-medium", + row: "flex justify-between mt-0 px-2", + cell: cn( + "relative text-center focus-within:relative focus-within:z-20", + props.mode === "range" + ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" + : "[&:has([aria-selected])]:rounded-md" + ), + day: cn( + buttonVariants({ variant: "ghost" }), + "h-10 w-10 rounded-[50px] p-0 text-[16px] font-normal" + ), + day_range_start: "day-range-start", + day_range_end: "day-range-end", + day_selected: + "bg-[#512DF1] text-white focus:bg-[#512DF1] focus:text-white", + day_today: "", + day_outside: + "day-outside opacity-40", + day_disabled: "opacity-40 text-[#878787]", + day_range_middle: + "aria-selected:bg-accent aria-selected:text-accent-foreground", + day_hidden: "invisible", + ...classNames, + }} + components={{ + IconLeft: ({ className, ...props }) => ( + + ), + IconRight: ({ className, ...props }) => ( + + ), + }} + formatters={{ + formatCaption: (date) => { + const year = date.getFullYear() + const month = date.getMonth() + 1 + return `${year}년 ${month.toString().padStart(2, "0")}월` + }, + formatWeekdayName: (weekday) => koreanWeekdays[weekday.getDay()] + }} + {...props} + /> + ) +} +Calendar.displayName = "Calendar" + +export { Calendar } diff --git a/src/mentorship/components/suggestion/DateTimeSelectModal.tsx b/src/mentorship/components/suggestion/DateTimeSelectModal.tsx new file mode 100644 index 0000000..3040d53 --- /dev/null +++ b/src/mentorship/components/suggestion/DateTimeSelectModal.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import { Calendar } from "@/global/ui/calendar"; +import { GlobalButton } from "@/global/ui/GlobalButton"; + +interface DateTimeSelectModalProps { + isOpen: boolean; + onClose: () => void; + onSelect: (dateTime: string) => void; +} + +const DateTimeSelectModal: React.FC = ({ isOpen, onClose, onSelect }) => { + const [selectedDate, setSelectedDate] = useState(new Date()); + const [selectedTime, setSelectedTime] = useState(null); + + const timeSlots = ["17:00 ~ 18:30", "18:30 ~ 19:00", "19:00 ~ 19:30", "19:30 ~ 20:00","20:00 ~ 20:30", "20:30 ~ 21:00", "21:00 ~ 21:30", "21:30 ~ 22:00", "22:30 ~ 23:00", "23:00 ~ 23:30"]; + + const handleConfirm = () => { + if (selectedDate && selectedTime) { + const formattedDate = selectedDate.toISOString().split("T")[0]; + onSelect(`${formattedDate} ${selectedTime}`); + onClose(); + } + }; + + return isOpen ? ( +
+
+ + {/* 헤더 */} +
+

희망 날짜

+ +
+ + {/* 📅 캘린더 적용 */} + + + {/* 시간 선택 */} +
+
+ {timeSlots.map((slot) => ( + + ))} +
+
+ + {/* 선택 완료 버튼 */} +
+ + 선택 완료 + +
+
+
+ ) : null; +}; + +export default DateTimeSelectModal; \ No newline at end of file diff --git a/src/mentorship/components/suggestion/SuggestInfo.tsx b/src/mentorship/components/suggestion/SuggestInfo.tsx new file mode 100644 index 0000000..4de6acb --- /dev/null +++ b/src/mentorship/components/suggestion/SuggestInfo.tsx @@ -0,0 +1,18 @@ +const SuggestInfo = () => { + return ( +
+
+
+
바이 님께
+
멘토링 제안하기
+
+
+ 링어스 멘토링은 30분을 기본으로 진행됩니다. + 동문 기반의 멘토링 서비스를 통해 더욱 깊이 있는 조언과 네트워킹 기회를 얻어보세요! +
+
+
+ ); +}; + +export default SuggestInfo; \ No newline at end of file diff --git a/src/mentorship/components/suggestion/SuggestSubject.tsx b/src/mentorship/components/suggestion/SuggestSubject.tsx new file mode 100644 index 0000000..b3d192f --- /dev/null +++ b/src/mentorship/components/suggestion/SuggestSubject.tsx @@ -0,0 +1,54 @@ +import { useState } from "react"; + +const SuggestSubject = () => { + const [selectedSubjects, setSelectedSubjects] = useState([]); + const [text, setText] = useState(""); + + const subjects = ["학업 관련", "업계 동향", "면접 대비", "취업 준비"]; + + const toggleSubject = (subject: string) => { + setSelectedSubjects((prev: string[]) => + prev.includes(subject) + ? prev.filter((t) => t !== subject) + : [...prev, subject] + ); + }; + + return ( +
+
이야기 나누고 싶은 주제
+ + {/* 주제 선택 */} +
+ {subjects.map((subject) => ( + + ))} +
+ + {/* 주제 작성 */} +