Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 마이페이지 개발 #107

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion src/app/(user)/mypage/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { MypageView } from "@/components/pages/MyPage/MypageView";

export default function MyPage() {
return <main>Hello, world!</main>;
return <MypageView />;
}
10 changes: 10 additions & 0 deletions src/components/common/Dropdown/Dropdown.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,13 @@
.dropdownList .dropdownItem:not(:last-child) {
border-radius: 0;
}

/* fullWidth 관련 스타일 */

.fullWidthToggle {
width: 100%;
}

.fullWidthItem {
width: 100%;
}
19 changes: 15 additions & 4 deletions src/components/common/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React from "react";
import React, { useEffect, useRef, useState } from "react";
import { Button, Menu, MenuTarget, MenuDropdown, MenuItem } from "@mantine/core";
import classes from "./Dropdown.module.css";

Expand All @@ -9,6 +9,7 @@ interface DropdownProps {
placeholder: string;
selectedOption?: string | null;
onOptionClick?: (option: string) => void;
fullWidth?: boolean;
}

export function Dropdown({
Expand All @@ -18,8 +19,17 @@ export function Dropdown({
onOptionClick = function (): void {
throw new Error("Function not implemented.");
},
fullWidth = false,
}: DropdownProps) {
const [opened, setOpened] = React.useState<boolean>(false);
const [dropdownWidth, setDropdownWidth] = useState<number | undefined>(undefined);
const menuTargetRef = useRef<HTMLButtonElement>(null);

useEffect(() => {
if (fullWidth && menuTargetRef.current) {
setDropdownWidth(menuTargetRef.current.offsetWidth - 2);
}
}, [fullWidth, opened]);

const handleOptionClick = (option: string) => {
onOptionClick(option);
Expand All @@ -30,8 +40,9 @@ export function Dropdown({
<Menu offset={0} opened={opened} onChange={setOpened}>
<MenuTarget>
<Button
ref={menuTargetRef}
justify="space-between"
className={`${classes.dropdownToggle} ${opened ? classes.opened : ""}`}
className={`${classes.dropdownToggle} ${opened ? classes.opened : ""} ${fullWidth && classes.fullWidthToggle}`}
onClick={() => setOpened(!opened)}
leftSection={<span className={classes.buttonLabel}>{selectedOption || placeholder}</span>}
rightSection={
Expand Down Expand Up @@ -63,11 +74,11 @@ export function Dropdown({
}
></Button>
</MenuTarget>
<MenuDropdown className={classes.dropdownList}>
<MenuDropdown className={classes.dropdownList} style={{ width: dropdownWidth }}>
{options.map((option) => (
<MenuItem
key={option}
className={classes.dropdownItem}
className={`${classes.dropdownItem} ${fullWidth && classes.fullWidthItem}`}
onClick={() => handleOptionClick(option)}
>
{option}
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/ProjectCard/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function ProjectCard({
}: ProjectCardProps) {
const studentsString = data.studentNames.join(", ");
const professorString = data.professorNames.join(", ");

return (
<Card className={classes.card} w={width} h={height}>
<CardSection className={classes["img-section"]}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/VideoCard/VideoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { QuizModal } from "./QuizModal";

export interface VideoCardProps {
title: string;
subtitle: string;
subtitle?: string;
videoUrl: string;
bookmarked: boolean;
onBookmarkToggle: () => void;
Expand Down
31 changes: 31 additions & 0 deletions src/components/pages/MyPage/MypageForm.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.formContainer {
padding: 15px;
}

.text {
width: 100%;
}

.inputWrapper {
flex-grow: 1;
height: 12%;
}

.rowGroup {
width: 100%;
}

.rowGroup > .row {
display: flex;
width: 100%;
line-height: 22px;
font-size: 16px;
margin-top: 5px;
align-items: center;
}

.rowGroup > .row > .firstCol {
font-weight: 700;
width: 25%;
flex-shrink: 0;
}
176 changes: 176 additions & 0 deletions src/components/pages/MyPage/MypageForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"use client";

import { PrimaryButton } from "@/components/common/Buttons";
import { useForm } from "@mantine/form";
import { useEffect, useState } from "react";
import { Group, Stack } from "@mantine/core";
import { TextInput } from "@/components/common/TextInput";
import classes from "./MypageForm.module.css";
import { Dropdown } from "@/components/common/Dropdown/Dropdown";

import { IDepartment, IUser } from "@/types/user";
import { USER_TYPE_LOOKUP_TABLE } from "@/constants/LookupTables";
import { fetcher } from "@/utils/fetcher";
import { CommonAxios } from "@/utils/CommonAxios";

interface Props {
initialData: IUser | null;
onUserUpdate: () => void;
}
export function MypageForm({ initialData, onUserUpdate }: Props) {
const [selectedDept, setSelectedDept] = useState(
initialData?.userType === "STUDENT" ? initialData?.departmentName : null
);
const [departments, setDepartments] = useState<IDepartment[]>([]);

const form = useForm({
initialValues: {
name: initialData?.name,
phone: initialData?.phone,
email: initialData?.email ?? "",
departmentName: initialData?.departmentName,
divison: initialData?.division,
position: initialData?.position,
studentNumber: initialData?.studentNumber ?? "",
},

validate: {
email: (value: string) => (/^\S+@\S+$/.test(value) ? null : "잘못된 이메일 형식입니다."),
studentNumber: (value: string) => {
// userType이 STUDENT인 경우만 유효성 검사 수행
if (initialData?.userType === "STUDENT") {
return /^\d{10}$/.test(value) ? null : "유효한 학번을 입력해주세요.";
}
return null;
},
},
});

/**
* 학과 리스트 가져오기
*/
useEffect(() => {
const fetchDepartments = async () => {
try {
const data = await fetcher({ url: "/departments" });
setDepartments(data);
} catch (error) {
console.error("Failed to fetch departments:", error);
} finally {
}
};
fetchDepartments();
}, []);

/**
* 유저 정보 수정 폼 제출
*/
const handleSubmit = async (values: typeof form.values) => {
console.log("handleSubmit 실행");
try {
// form에서 받은 값을 서버로 전송
const updatedUser = {
name: values.name,
phoneNumber: values.phone,
email: values.email,
divison: values.divison ?? null,
position: values.position ?? null,
studentNumber: values.studentNumber ?? null,
department: selectedDept ?? null,
};
// 유저 정보 수정 API 요청
await CommonAxios.put("/users/me", updatedUser);
} catch (error) {
console.error("Failed to update user", error);
} finally {
onUserUpdate();
}
};

return (
<form className={classes.formContainer} onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap={"lg"} className={classes.rowGroup}>
<div className={classes.row}>
<div className={classes.firstCol}>이름</div>
<div className={classes.inputWrapper}>
<TextInput {...form.getInputProps("name")} className={classes.text} />
</div>
</div>
<div className={classes.row}>
<div className={classes.firstCol}>휴대전화</div>
<div className={classes.inputWrapper}>
<TextInput {...form.getInputProps("phone")} className={classes.text} />
</div>
</div>
<div className={classes.row}>
<div className={classes.firstCol}>이메일</div>
<div className={classes.inputWrapper}>
<TextInput {...form.getInputProps("email")} className={classes.text} />
</div>
</div>
<div className={classes.row}>
<div className={classes.firstCol}>회원 유형</div>
{/* 유저 타입 변경하지 못하도록 수정 */}
<div className={classes.secondCol}>
{USER_TYPE_LOOKUP_TABLE[initialData?.userType as keyof typeof USER_TYPE_LOOKUP_TABLE] ??
"학생"}
</div>
{/* <div className={classes.inputWrapper}>
<Dropdown
options={userTypesList}
placeholder={"선택"}
onOptionClick={setSelectedUserType}
selectedOption={selectedUserType}
fullWidth
/>
</div> */}
</div>
{initialData?.userType === "STUDENT" && (
<>
<div className={classes.row}>
<div className={classes.firstCol}>학과</div>
<div className={classes.inputWrapper}>
<Dropdown
options={departments.map((department) => department.name)}
placeholder="학과 선택"
onOptionClick={setSelectedDept}
selectedOption={selectedDept}
fullWidth
/>
</div>
</div>
<div className={classes.row}>
<div className={classes.firstCol}>학번</div>
<div className={classes.inputWrapper}>
<TextInput {...form.getInputProps("studentNumber")} className={classes.text} />
</div>
</div>
</>
)}
{(initialData?.userType === "PROFESSOR" ||
initialData?.userType === "INACTIVE_PROFESSOR" ||
initialData?.userType === "COMPANY" ||
initialData?.userType === "INACTIVE_COMPANY") && (
<>
<div className={classes.row}>
<div className={classes.firstCol}>소속</div>
<div className={classes.inputWrapper}>
<TextInput {...form.getInputProps("division")} className={classes.text} />
</div>
</div>
<div className={classes.row}>
<div className={classes.firstCol}>직책</div>
<div className={classes.inputWrapper}>
<TextInput {...form.getInputProps("position")} className={classes.text} />
</div>
</div>
</>
)}
</Stack>

<Group justify="flex-end" mt="md">
<PrimaryButton type="submit">수정 완료</PrimaryButton>
</Group>
</form>
);
}
Loading
Loading