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
5 changes: 4 additions & 1 deletion src/entities/user/hooks/useSignUpForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from "react";

import { useSignUp } from "@entities/user/hooks/useSignUp";

import { useAuthStore } from "@shared/stores/authStore";
import type { UserExperience, UserRole } from "@shared/types/user";

// 반환 타입 정의
Expand All @@ -19,8 +20,10 @@ interface UseSignUpFormReturn {

export function useSignUpForm(): UseSignUpFormReturn {
const { signUp } = useSignUp();
const { user } = useAuthStore();
const defaultName = user?.displayName || "";

const [name, setName] = useState("");
const [name, setName] = useState(defaultName);
const [userRole, setUserRole] = useState("");
const [experience, setExperience] = useState("");
const [introduceMyself, setIntroduceMyself] = useState("");
Expand Down
5 changes: 3 additions & 2 deletions src/entities/user/ui/UserInfoForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { type JSX } from "react";
import { useSignUpForm } from "@entities/user/hooks/useSignUpForm";
import SubmitButton from "@entities/user/ui/SubmitButton";

// import { useAuthStore } from "@shared/stores/authStore";

const UserInfoForm = (): JSX.Element => {
const {
name,
Expand All @@ -31,13 +33,12 @@ const UserInfoForm = (): JSX.Element => {
{/* 이름 입력 */}
<FormControl error={errors.name} variant="outlined" fullWidth>
<StyledTextField
label="🙋 이름 *"
label="🙋 닉네임 *"
variant="outlined"
value={name}
onChange={(e) => handleChange("name")(e.target.value)}
error={errors.name}
onFocus={() => errors.name && handleChange("name")(name)}
placeholder="이름"
InputLabelProps={{ shrink: true }}
/>
{errors.name && <ErrorText>이름을 입력해주세요.</ErrorText>}
Expand Down
98 changes: 98 additions & 0 deletions src/entities/user/ui/user-profile/EmptyProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
Typography,
Box,
Card,
CardContent,
Divider,
styled,
} from "@mui/material";
import type { JSX } from "react";

import NavigateButton from "@shared/ui/NavigateButton";

interface EmptyProjectCardProps {
message: string;
}

const EmptyProjectCard = ({ message }: EmptyProjectCardProps): JSX.Element => (
<StyledCard>
<StyledCardContent>
<Box
flex={1}
display="flex"
alignItems="center"
justifyContent="center"
minHeight={60}
>
<Typography variant="body1" color="text.secondary" align="center">
{message}
</Typography>
</Box>
<StyledDivider />
<Box
flex={1}
display="flex"
alignItems="center"
justifyContent="center"
minHeight={80}
gap={2}
>
<NavigateButton to="/project">프로젝트 찾기</NavigateButton>
<NavigateButton to="/project/insert">프로젝트 등록</NavigateButton>
</Box>
</StyledCardContent>
</StyledCard>
);

export default EmptyProjectCard;

const StyledCard = styled(Card)(({ theme }) => ({
height: "auto",
minHeight: 180,
maxWidth: 320,
width: "100%",
display: "flex",
flexDirection: "column",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
cursor: "pointer",
border: `1px solid ${theme.palette.divider}`,
padding: theme.spacing(1.5, 1.5, 1.5, 1.5),

"&:hover": {
transform: "translateY(-0.4rem)",
boxShadow:
"0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)",
borderColor: theme.palette.primary.light,
},

[theme.breakpoints.up("sm")]: {
flex: 1,
maxWidth: 340,
"&:hover": {
transform: "translateY(-0.6rem)",
},
},

[theme.breakpoints.up("md")]: {
maxWidth: 360,
maxHeight: 320,
},
}));

const StyledCardContent = styled(CardContent)(({ theme }) => ({
height: "100%",
display: "flex",
flexDirection: "column",
gap: theme.spacing(1),
padding: theme.spacing(2),

[theme.breakpoints.up("sm")]: {
gap: theme.spacing(2),
padding: theme.spacing(2.5),
},
}));

const StyledDivider = styled(Divider)(({ theme }) => ({
margin: `${theme.spacing(0.8)} 0`,
backgroundColor: theme.palette.divider,
}));
36 changes: 36 additions & 0 deletions src/entities/user/ui/user-profile/ProjectTabPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Box } from "@mui/material";
import type { JSX } from "react";

import type { ProjectListRes } from "@shared/types/project";
import ProjectCard from "@shared/ui/ProjectCard";

import EmptyProjectCard from "./EmptyProjectCard";

interface ProjectTabPanelProps {
projects: ProjectListRes[];
emptyMessage: string;
}

const ProjectTabPanel = ({
projects,
emptyMessage,
}: ProjectTabPanelProps): JSX.Element =>
projects && projects.length > 0 ? (
<Box
display="grid"
gridTemplateColumns={{
xs: "1fr",
sm: "repeat(2, 1fr)",
md: "repeat(3, 1fr)",
}}
gap={2}
>
{projects.slice(0, 3).map((project) => (
<ProjectCard key={project.id} project={project} simple />
))}
</Box>
) : (
<EmptyProjectCard message={emptyMessage} />
);

export default ProjectTabPanel;
37 changes: 37 additions & 0 deletions src/entities/user/ui/user-profile/TapWithBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Badge } from "@mui/material";
import type { JSX } from "react";

interface TabWithBadgeProps {
label: string;
count: number;
active: boolean;
onClick: () => void;
ProfileTabChip: any;
}

const TabWithBadge = ({
label,
count,
active,
onClick,
ProfileTabChip,
}: TabWithBadgeProps): JSX.Element => (
<Badge
badgeContent={count}
color={active ? "primary" : "secondary"}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
overlap="rectangular"
sx={{
"& .MuiBadge-badge": {
fontSize: "1.1rem",
fontWeight: 700,
minWidth: 24,
height: 24,
},
}}
>
<ProfileTabChip label={label} active={active} clickable onClick={onClick} />
</Badge>
);

export default TabWithBadge;
145 changes: 145 additions & 0 deletions src/entities/user/ui/user-profile/UserProfileCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import SettingsIcon from "@mui/icons-material/Settings";
import {
Box,
Avatar,
Typography,
Card,
CardContent,
IconButton,
Divider,
} from "@mui/material";
import { styled as muiStyled } from "@mui/material/styles";
import type { JSX } from "react";

import TabWithBadge from "./TapWithBadge";

// Chip 컴포넌트는 상위에서 import해서 prop으로 넘겨야 함

interface UserProfileCardProps {
userProfile: any;
PROFILE_TABS: { label: string; color: string }[];
likeProjects: any[];
appliedProjects: any[];
tab: number;
setTab: (idx: number) => void;
ProfileTabChip: any;
}

const userRoleMap: Record<string, string> = {
frontend: "프론트엔드",
backend: "백엔드",
fullstack: "풀스택",
designer: "디자이너",
pm: "PM",
};
const experienceMap: Record<string, string> = {
junior: "주니어 (3년 이하)",
mid: "미들 (3년 이상 10년 이하)",
senior: "시니어 (10년 이상)",
};

const UserProfileCard = ({
userProfile,
PROFILE_TABS,
likeProjects,
appliedProjects,
tab,
setTab,
ProfileTabChip,
}: UserProfileCardProps): JSX.Element => {
return (
<ProfileCard>
<ProfileCardContent>
<ProfileCardHeader>
<IconButton size="small" aria-label="프로필 수정">
<SettingsIcon />
</IconButton>
</ProfileCardHeader>
<ProfileMainRow>
<ProfileAvatar src={userProfile.avatar} />
<ProfileInfoCol>
<Typography variant="h5" fontWeight={700}>
{userProfile.name}
</Typography>
<Typography>
{userRoleMap[userProfile.userRole] || userProfile.userRole}
</Typography>
<Typography>
{experienceMap[userProfile.experience] || userProfile.experience}
</Typography>
</ProfileInfoCol>
</ProfileMainRow>
<Box mt={2} width="100%">
<Typography>{userProfile.introduceMyself}</Typography>
</Box>
<Divider sx={{ my: 2 }} />
<ProfileEmail>{userProfile.email}</ProfileEmail>
<Box mt={2} display="flex" gap={1} justifyContent="center">
{PROFILE_TABS.map((tabInfo, idx) => (
<TabWithBadge
key={tabInfo.label}
label={tabInfo.label}
count={
idx === 0
? likeProjects?.length || 0
: appliedProjects?.length || 0
}
active={tab === idx}
onClick={() => setTab(idx)}
ProfileTabChip={ProfileTabChip}
/>
))}
</Box>
</ProfileCardContent>
</ProfileCard>
);
};

export default UserProfileCard;

// 스타일 컴포넌트 재사용
const ProfileCard = muiStyled(Card)(({ theme }) => ({
minWidth: 280,
maxWidth: 320,
borderRadius: 12,
boxShadow: theme.shadows[2],
position: "relative",
padding: 0,
}));
const ProfileCardContent = muiStyled(CardContent)(({ theme }) => ({
padding: theme.spacing(3),
paddingBottom: "16px",
"&:last-child": {
paddingBottom: "16px",
},
}));
const ProfileCardHeader = muiStyled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginBottom: 8,
minHeight: 32,
});
const ProfileMainRow = muiStyled(Box)({
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 16,
});
const ProfileAvatar = muiStyled(Avatar)({
width: 100,
height: 100,
marginBottom: 0,
});
const ProfileInfoCol = muiStyled(Box)({
display: "flex",
flexDirection: "column",
gap: 4,
});
const ProfileEmail = muiStyled(Typography)(({ theme }) => ({
color: theme.palette.text.disabled,
fontSize: "0.95rem",
fontStyle: "italic",
fontWeight: 400,
marginTop: theme.spacing(1),
}));
Loading