Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/app/routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -54,6 +55,7 @@ export const Router = () => {
<Route path="/mentorship" element={<MentorshipLayout />}>
<Route path="" element={<MentorList />} />
<Route path="info" element={<MentorInfoView />} />
<Route path="suggestion" element={<Suggestion />} />
</Route>
</Routes>
);
Expand Down
2 changes: 1 addition & 1 deletion src/global/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
96 changes: 96 additions & 0 deletions src/global/ui/calendar.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof DayPicker>

const koreanWeekdays = ["일", "월", "화", "수", "목", "금", "토"]

function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
const [currentMonth, setCurrentMonth] = useState(new Date());

return (
<DayPicker
showOutsideDays={showOutsideDays}
locale={ko}
month={currentMonth}
onMonthChange={setCurrentMonth}
className={cn("w-full", className)}
disabled={(date) => 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 }) => (
<ChevronLeft
className={cn("h-6 w-6", className)} {...props}
strokeWidth={1}
/>
),
IconRight: ({ className, ...props }) => (
<ChevronRight
className={cn("h-6 w-6", className)} {...props}
strokeWidth={1}
/>
),
}}
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 }
76 changes: 76 additions & 0 deletions src/mentorship/components/suggestion/DateTimeSelectModal.tsx
Original file line number Diff line number Diff line change
@@ -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<DateTimeSelectModalProps> = ({ isOpen, onClose, onSelect }) => {
const [selectedDate, setSelectedDate] = useState<Date | undefined>(new Date());
const [selectedTime, setSelectedTime] = useState<string | null>(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 ? (
<div className="fixed inset-0 bg-black/75 flex items-end justify-center z-50">
<div className="bg-white w-full max-w-[600px] h-[90%] rounded-t-[30px] p-8 flex flex-col relative">

{/* 헤더 */}
<div className="flex justify-between items-center mb-2">
<h2 className="font-bold text-[16px]">희망 날짜</h2>
<button onClick={onClose}>
<img src="/assets/exit.png" alt="close" className="w-6 h-6" />
</button>
</div>

{/* 📅 캘린더 적용 */}
<Calendar
mode="single"
selected={selectedDate}
onSelect={setSelectedDate}
className="my-2"
/>

{/* 시간 선택 */}
<div className="flex-grow overflow-y-auto px-1 pt-1 no-scrollbar">
<div className="grid grid-cols-2 gap-2">
{timeSlots.map((slot) => (
<button
key={slot}
className={`py-2 px-4 border rounded-[10px] ${
selectedTime === slot ? "bg-[#F2EFFF] text-primary-1 border-primary-1" : "text-[#8C8B93] border-[#D9D7E0]"
}`}
onClick={() => setSelectedTime(slot)}
>
{slot}
</button>
))}
</div>
</div>

{/* 선택 완료 버튼 */}
<div className="w-full pt-5 bg-white">
<GlobalButton
onClick={handleConfirm}
disabled={!selectedDate || !selectedTime}
>
선택 완료
</GlobalButton>
</div>
</div>
</div>
) : null;
};

export default DateTimeSelectModal;
18 changes: 18 additions & 0 deletions src/mentorship/components/suggestion/SuggestInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const SuggestInfo = () => {
return (
<div className="px-4 py-4">
<div className="bg-[#F2EFFF] rounded-[16px] p-4">
<div className="mb-4">
<div className="text-[22px] font-semibold">바이 님께</div>
<div className="text-[22px] font-semibold">멘토링 제안하기</div>
</div>
<div className="text-[14px] text-[#7F7D85]">
링어스 멘토링은 30분을 기본으로 진행됩니다.
동문 기반의 멘토링 서비스를 통해 더욱 깊이 있는 조언과 네트워킹 기회를 얻어보세요!
</div>
</div>
</div>
);
};

export default SuggestInfo;
54 changes: 54 additions & 0 deletions src/mentorship/components/suggestion/SuggestSubject.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useState } from "react";

const SuggestSubject = () => {
const [selectedSubjects, setSelectedSubjects] = useState<string[]>([]);
const [text, setText] = useState("");

const subjects = ["학업 관련", "업계 동향", "면접 대비", "취업 준비"];

const toggleSubject = (subject: string) => {
setSelectedSubjects((prev: string[]) =>
prev.includes(subject)
? prev.filter((t) => t !== subject)
: [...prev, subject]
);
};

return (
<div className="px-4 py-2">
<div className="font-semibold text-[18px] my-4">이야기 나누고 싶은 주제</div>

{/* 주제 선택 */}
<div className="grid grid-cols-2 gap-2">
{subjects.map((subject) => (
<button
key={subject}
className={`text-[16px] border-[1px] rounded-[10px] flex flex-col items-center justify-center p-3
${selectedSubjects.includes(subject) ? "bg-[#F2EFFF] text-primary-1 border-primary-1" : "border-[#D9D7E0] text-[#8C8B93]"}`}
onClick={() => toggleSubject(subject)}
>
{subject}
</button>
))}
</div>

{/* 주제 작성 */}
<textarea
className="w-full min-h-[350px] px-5 py-4 mt-4 border-[1px] border-[#D9D7E0] rounded-[10px] text-[14px] resize-none outline-none focus:border-primary-1"
value={text}
onChange={(e) => {
if (e.target.value.length <= 500) {
setText(e.target.value);
}
}}
placeholder="하고 싶은 이야기가 별도로 있다면 직접 자유롭게 작성해 주세요."
rows={4}
/>
<div className="text-right text-[#94939B] text-[14px] mt-1">
{text.length} / 500
</div>
</div>
);
};

export default SuggestSubject;
Loading