From 6baf9bef20ab3a5851781d57a566fab0f6a04bf4 Mon Sep 17 00:00:00 2001 From: Sara han Date: Thu, 26 Jun 2025 20:40:21 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=83=80=EC=9D=BC?= =?UTF-8?q?=EB=93=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/ui/user-profile/ProjectTabPanel.tsx | 26 ++++++++++-------- .../user/ui/user-profile/TapWithBadge.tsx | 27 ++++++++++--------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/entities/user/ui/user-profile/ProjectTabPanel.tsx b/src/entities/user/ui/user-profile/ProjectTabPanel.tsx index 211e191..0db471f 100644 --- a/src/entities/user/ui/user-profile/ProjectTabPanel.tsx +++ b/src/entities/user/ui/user-profile/ProjectTabPanel.tsx @@ -1,4 +1,4 @@ -import { Box } from "@mui/material"; +import { Box, styled } from "@mui/material"; import type { JSX } from "react"; import type { ProjectListRes } from "@shared/types/project"; @@ -16,21 +16,25 @@ const ProjectTabPanel = ({ emptyMessage, }: ProjectTabPanelProps): JSX.Element => projects && projects.length > 0 ? ( - + {projects.slice(0, 3).map((project) => ( ))} - + ) : ( ); export default ProjectTabPanel; + +const StyledGridContainer = styled(Box)(({ theme }) => ({ + display: "grid", + gridTemplateColumns: "1fr", + gap: theme.spacing(2), + [theme.breakpoints.up("sm")]: { + gridTemplateColumns: "repeat(2, 1fr)", + }, + [theme.breakpoints.up("md")]: { + gridTemplateColumns: "repeat(3, 1fr)", + }, +})); diff --git a/src/entities/user/ui/user-profile/TapWithBadge.tsx b/src/entities/user/ui/user-profile/TapWithBadge.tsx index c43cbfe..63cc5bc 100644 --- a/src/entities/user/ui/user-profile/TapWithBadge.tsx +++ b/src/entities/user/ui/user-profile/TapWithBadge.tsx @@ -1,12 +1,12 @@ -import { Badge } from "@mui/material"; -import type { JSX } from "react"; +import { Badge, styled } from "@mui/material"; +import type { JSX, ComponentType } from "react"; interface TabWithBadgeProps { label: string; count: number; active: boolean; onClick: () => void; - ProfileTabChip: any; + ProfileTabChip: ComponentType; } const TabWithBadge = ({ @@ -16,22 +16,23 @@ const TabWithBadge = ({ onClick, ProfileTabChip, }: TabWithBadgeProps): JSX.Element => ( - - + ); export default TabWithBadge; + +const StyledBadge = styled(Badge)((_theme) => ({ + "& .MuiBadge-badge": { + fontSize: "1.1rem", + fontWeight: 700, + minWidth: 24, + height: 24, + }, +})); From 33031e1f261f6c517cac6e18e209949c65db2dc4 Mon Sep 17 00:00:00 2001 From: Sara han Date: Thu, 26 Jun 2025 22:23:39 +0900 Subject: [PATCH 2/5] =?UTF-8?q?style:=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/ui/user-profile/UserProfileCard.tsx | 69 +++++++++++++------ src/pages/user-profile/ui/UserProfilePage.tsx | 10 ++- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/src/entities/user/ui/user-profile/UserProfileCard.tsx b/src/entities/user/ui/user-profile/UserProfileCard.tsx index 71ec1b7..1576097 100644 --- a/src/entities/user/ui/user-profile/UserProfileCard.tsx +++ b/src/entities/user/ui/user-profile/UserProfileCard.tsx @@ -9,20 +9,23 @@ import { Divider, } from "@mui/material"; import { styled as muiStyled } from "@mui/material/styles"; -import type { JSX } from "react"; +import type { ComponentType, JSX } from "react"; + +import type { ProjectListRes } from "@shared/types/project"; +import type { User } from "@shared/types/user"; import TabWithBadge from "./TapWithBadge"; // Chip 컴포넌트는 상위에서 import해서 prop으로 넘겨야 함 interface UserProfileCardProps { - userProfile: any; + userProfile: User; PROFILE_TABS: { label: string; color: string }[]; - likeProjects: any[]; - appliedProjects: any[]; + likeProjects: ProjectListRes[]; + appliedProjects: ProjectListRes[]; tab: number; setTab: (idx: number) => void; - ProfileTabChip: any; + ProfileTabChip: ComponentType; } const userRoleMap: Record = { @@ -33,9 +36,9 @@ const userRoleMap: Record = { pm: "PM", }; const experienceMap: Record = { - junior: "주니어 (3년 이하)", - mid: "미들 (3년 이상 10년 이하)", - senior: "시니어 (10년 이상)", + junior: "주니어 (3년 이하) 🌱", + mid: "미들 (3년 이상 10년 이하) 🌿", + senior: "시니어 (10년 이상) 🌳", }; const UserProfileCard = ({ @@ -51,30 +54,34 @@ const UserProfileCard = ({ - - + + - + {userProfile.name} - + {userRoleMap[userProfile.userRole] || userProfile.userRole} - + {experienceMap[userProfile.experience] || userProfile.experience} - + {userProfile.introduceMyself} - {userProfile.email} - + 💌 • {userProfile.email} + {PROFILE_TABS.map((tabInfo, idx) => ( ))} - + ); @@ -100,15 +107,16 @@ export default UserProfileCard; // 스타일 컴포넌트 재사용 const ProfileCard = muiStyled(Card)(({ theme }) => ({ minWidth: 280, - maxWidth: 320, + maxWidth: "100%", borderRadius: 12, boxShadow: theme.shadows[2], position: "relative", - padding: 0, + padding: "0 2rem", })); const ProfileCardContent = muiStyled(CardContent)(({ theme }) => ({ padding: theme.spacing(3), paddingBottom: "16px", + position: "relative", "&:last-child": { paddingBottom: "16px", }, @@ -119,12 +127,18 @@ const ProfileCardHeader = muiStyled(Box)({ justifyContent: "flex-end", marginBottom: 8, minHeight: 32, + position: "absolute", + top: 10, + right: -10, }); const ProfileMainRow = muiStyled(Box)({ display: "flex", - flexDirection: "row", alignItems: "center", gap: 16, + justifyContent: "space-between", + flexDirection: "row-reverse", + marginTop: "3rem", + padding: "0 1rem", }); const ProfileAvatar = muiStyled(Avatar)({ width: 100, @@ -134,12 +148,23 @@ const ProfileAvatar = muiStyled(Avatar)({ const ProfileInfoCol = muiStyled(Box)({ display: "flex", flexDirection: "column", - gap: 4, + gap: 2, }); const ProfileEmail = muiStyled(Typography)(({ theme }) => ({ color: theme.palette.text.disabled, - fontSize: "0.95rem", + fontSize: "1.1rem", fontStyle: "italic", fontWeight: 400, marginTop: theme.spacing(1), + textAlign: "end", +})); + +const TabBadgeContainer = muiStyled(Box)(({ theme }) => ({ + marginTop: theme.spacing(2), + gap: theme.spacing(1), + justifyContent: "center", + display: "none", + [theme.breakpoints.up("sm")]: { + display: "flex", + }, })); diff --git a/src/pages/user-profile/ui/UserProfilePage.tsx b/src/pages/user-profile/ui/UserProfilePage.tsx index 232e6b7..a8e62e5 100644 --- a/src/pages/user-profile/ui/UserProfilePage.tsx +++ b/src/pages/user-profile/ui/UserProfilePage.tsx @@ -11,6 +11,7 @@ import UserProfileHeader from "@entities/user/ui/user-profile/UserProfileHeader" import UserProfileProjectList from "@entities/user/ui/user-profile/UserProfileProjectList"; import { useAuthStore } from "@shared/stores/authStore"; +import LoadingSpinner from "@shared/ui/loading-spinner/LoadingSpinner"; // 탭 이름 상수 배열 const PROFILE_TABS = [ @@ -35,13 +36,18 @@ const UserProfilePage = (): JSX.Element => { const [tab, setTab] = useState(0); if (!userProfile) { - return
UserProfilePage
; + return ; } return ( - + {/* 왼쪽 프로필 사이드바 */} Date: Thu, 26 Jun 2025 22:34:44 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/user/ui/user-profile/UserProfileCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entities/user/ui/user-profile/UserProfileCard.tsx b/src/entities/user/ui/user-profile/UserProfileCard.tsx index 1576097..b187ac1 100644 --- a/src/entities/user/ui/user-profile/UserProfileCard.tsx +++ b/src/entities/user/ui/user-profile/UserProfileCard.tsx @@ -80,7 +80,6 @@ const UserProfileCard = ({ {userProfile.introduceMyself} - 💌 • {userProfile.email} {PROFILE_TABS.map((tabInfo, idx) => ( ))} + 💌 • {userProfile.email} ); From aac353925e47ae7146fc51757871fea9e2933eb8 Mon Sep 17 00:00:00 2001 From: Sara han Date: Fri, 27 Jun 2025 00:27:45 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B0=8F=20=EC=8A=A4=EB=82=B5=EB=B0=94=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/ui/user-profile/ProjectTabPanel.tsx | 17 +- .../user/ui/user-profile/UserProfileCard.tsx | 10 +- .../user-profile/UserProfileProjectList.tsx | 151 ++++++++++++++++-- src/features/projects/api/projectsApi.ts | 25 +++ src/pages/user-profile/ui/UserProfilePage.tsx | 34 ++-- src/shared/stores/projectStore.ts | 27 ++++ src/shared/ui/DeleteButton.tsx | 41 +++++ src/shared/ui/ProjectCard.tsx | 12 +- src/shared/ui/SnackbarAlert.tsx | 38 +++++ 9 files changed, 327 insertions(+), 28 deletions(-) create mode 100644 src/shared/stores/projectStore.ts create mode 100644 src/shared/ui/DeleteButton.tsx create mode 100644 src/shared/ui/SnackbarAlert.tsx diff --git a/src/entities/user/ui/user-profile/ProjectTabPanel.tsx b/src/entities/user/ui/user-profile/ProjectTabPanel.tsx index 0db471f..7124fae 100644 --- a/src/entities/user/ui/user-profile/ProjectTabPanel.tsx +++ b/src/entities/user/ui/user-profile/ProjectTabPanel.tsx @@ -9,16 +9,29 @@ import EmptyProjectCard from "./EmptyProjectCard"; interface ProjectTabPanelProps { projects: ProjectListRes[]; emptyMessage: string; + editMode?: boolean; + selectedIds?: string[]; + onSelectProject?: (id: string) => void; } const ProjectTabPanel = ({ projects, emptyMessage, + editMode = false, + selectedIds = [], + onSelectProject, }: ProjectTabPanelProps): JSX.Element => projects && projects.length > 0 ? ( - {projects.slice(0, 3).map((project) => ( - + {projects.map((project) => ( + onSelectProject && onSelectProject(project.id)} + /> ))} ) : ( diff --git a/src/entities/user/ui/user-profile/UserProfileCard.tsx b/src/entities/user/ui/user-profile/UserProfileCard.tsx index b187ac1..a366c09 100644 --- a/src/entities/user/ui/user-profile/UserProfileCard.tsx +++ b/src/entities/user/ui/user-profile/UserProfileCard.tsx @@ -11,7 +11,7 @@ import { import { styled as muiStyled } from "@mui/material/styles"; import type { ComponentType, JSX } from "react"; -import type { ProjectListRes } from "@shared/types/project"; +import { useProjectStore } from "@shared/stores/projectStore"; import type { User } from "@shared/types/user"; import TabWithBadge from "./TapWithBadge"; @@ -21,8 +21,6 @@ import TabWithBadge from "./TapWithBadge"; interface UserProfileCardProps { userProfile: User; PROFILE_TABS: { label: string; color: string }[]; - likeProjects: ProjectListRes[]; - appliedProjects: ProjectListRes[]; tab: number; setTab: (idx: number) => void; ProfileTabChip: ComponentType; @@ -44,12 +42,12 @@ const experienceMap: Record = { const UserProfileCard = ({ userProfile, PROFILE_TABS, - likeProjects, - appliedProjects, tab, setTab, ProfileTabChip, }: UserProfileCardProps): JSX.Element => { + const { likeProjects, appliedProjects } = useProjectStore(); + return ( @@ -112,6 +110,8 @@ const ProfileCard = muiStyled(Card)(({ theme }) => ({ boxShadow: theme.shadows[2], position: "relative", padding: "0 2rem", + maxHeight: "350px", + overflow: "auto", })); const ProfileCardContent = muiStyled(CardContent)(({ theme }) => ({ padding: theme.spacing(3), diff --git a/src/entities/user/ui/user-profile/UserProfileProjectList.tsx b/src/entities/user/ui/user-profile/UserProfileProjectList.tsx index 2fad7f8..f43edf5 100644 --- a/src/entities/user/ui/user-profile/UserProfileProjectList.tsx +++ b/src/entities/user/ui/user-profile/UserProfileProjectList.tsx @@ -1,44 +1,173 @@ -import { Box, Tabs, Tab } from "@mui/material"; +import { + Box, + Tabs, + Tab, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Typography, +} from "@mui/material"; import type { JSX } from "react"; +import { useState, useCallback } from "react"; import ProjectTabPanel from "@entities/user/ui/user-profile/ProjectTabPanel"; -import type { ProjectListRes } from "@shared/types/project"; +import { useProjectStore } from "@shared/stores/projectStore"; +import DeleteButton from "@shared/ui/DeleteButton"; +import SnackbarAlert from "@shared/ui/SnackbarAlert"; interface UserProfileProjectListProps { PROFILE_TABS: { label: string; color: string }[]; tab: number; setTab: (idx: number) => void; - likeProjects: ProjectListRes[]; - appliedProjects: ProjectListRes[]; + onDeleteProjects: ( + type: "likeProjects" | "appliedProjects", + ids: string[] + ) => Promise; } const UserProfileProjectList = ({ PROFILE_TABS, tab, setTab, - likeProjects, - appliedProjects, + onDeleteProjects, }: UserProfileProjectListProps): JSX.Element => { + const [editMode, setEditMode] = useState(false); + const [selectedIds, setSelectedIds] = useState([]); + const [openDialog, setOpenDialog] = useState(false); + const [snackbarOpen, setSnackbarOpen] = useState(false); + + const { + likeProjects, + appliedProjects, + removeLikeProjects, + removeAppliedProjects, + } = useProjectStore(); + + const currentProjects = tab === 0 ? likeProjects : appliedProjects; + const allIds = currentProjects.map((p) => p.id); + const isAllSelected = + selectedIds.length === allIds.length && allIds.length > 0; + + const handleToggleAll = useCallback(() => { + setSelectedIds(isAllSelected ? [] : allIds); + }, [isAllSelected, allIds]); + + const handleSelectProject = useCallback((id: string) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((pid) => pid !== id) : [...prev, id] + ); + }, []); + + const handleDelete = useCallback(() => setOpenDialog(true), []); + const handleCancelDelete = useCallback(() => setOpenDialog(false), []); + + const handleConfirmDelete = useCallback(async () => { + if (tab === 0) { + await onDeleteProjects("likeProjects", selectedIds); + removeLikeProjects(selectedIds); + } else { + await onDeleteProjects("appliedProjects", selectedIds); + removeAppliedProjects(selectedIds); + } + setOpenDialog(false); + setSelectedIds([]); + setSnackbarOpen(true); + }, [ + tab, + selectedIds, + onDeleteProjects, + removeLikeProjects, + removeAppliedProjects, + ]); + return ( - setTab(v)} sx={{ mb: 3 }}> - {PROFILE_TABS.map((tabInfo, _idx) => ( - - ))} - + {/* Tabs와 버튼을 한 줄에 배치 */} + + setTab(v)}> + {PROFILE_TABS.map((tabInfo, _idx) => ( + + ))} + + + {editMode ? ( + <> + + + 삭제 + + + + ) : ( + + )} + + {tab === 0 && ( )} {tab === 1 && ( )} + {/* 삭제 확인 다이얼로그 */} + + 정말로 삭제하시겠습니까? + + + 선택한 프로젝트를 삭제하면 되돌릴 수 없습니다. + + + + + 삭제 + + + setSnackbarOpen(false)} + message="선택한 프로젝트가 성공적으로 삭제되었습니다." + severity="success" + duration={2500} + anchorOrigin={{ vertical: "top", horizontal: "center" }} + /> ); }; diff --git a/src/features/projects/api/projectsApi.ts b/src/features/projects/api/projectsApi.ts index 5c2c9b6..32139b2 100644 --- a/src/features/projects/api/projectsApi.ts +++ b/src/features/projects/api/projectsApi.ts @@ -156,3 +156,28 @@ export const updateUnLike = async ( }; } }; + +export type UserProjectField = "likeProjects" | "appliedProjects"; + +export const removeProjectsFromUser = async ( + uid: string, + type: UserProjectField, + projectIds: string[] +): Promise<{ success: boolean; error?: string }> => { + if (!uid || projectIds.length === 0) { + return { + success: false, + error: "유저 ID 또는 삭제할 프로젝트가 없습니다.", + }; + } + try { + const userRef = doc(db, "users", uid); + await updateDoc(userRef, { + [type]: arrayRemove(...projectIds), + }); + return { success: true }; + } catch (err) { + console.error(err); + return { success: false, error: "파이어베이스 업데이트 실패" }; + } +}; diff --git a/src/pages/user-profile/ui/UserProfilePage.tsx b/src/pages/user-profile/ui/UserProfilePage.tsx index a8e62e5..a4c0abf 100644 --- a/src/pages/user-profile/ui/UserProfilePage.tsx +++ b/src/pages/user-profile/ui/UserProfilePage.tsx @@ -1,9 +1,10 @@ import { Box, Container, Chip as MuiChip } from "@mui/material"; import { styled as muiStyled } from "@mui/material/styles"; import type { JSX } from "react"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useUserProfile } from "@features/auth/hooks/useUserProfile"; +import { removeProjectsFromUser } from "@features/projects/api/projectsApi"; import { useProjectsByIds } from "@entities/projects/hook/useProjectsByIds"; import UserProfileCard from "@entities/user/ui/user-profile/UserProfileCard"; @@ -11,6 +12,7 @@ import UserProfileHeader from "@entities/user/ui/user-profile/UserProfileHeader" import UserProfileProjectList from "@entities/user/ui/user-profile/UserProfileProjectList"; import { useAuthStore } from "@shared/stores/authStore"; +import { useProjectStore } from "@shared/stores/projectStore"; import LoadingSpinner from "@shared/ui/loading-spinner/LoadingSpinner"; // 탭 이름 상수 배열 @@ -24,17 +26,34 @@ const UserProfilePage = (): JSX.Element => { const uid = user?.uid; const { data: userProfile } = useUserProfile(uid ?? ""); + // zustand store 사용 + const { setLikeProjects, setAppliedProjects } = useProjectStore(); + // 관심있는/지원한 프로젝트 id 배열 const likeIds = userProfile?.likeProjects ?? []; const appliedIds = userProfile?.appliedProjects ?? []; // 프로젝트 데이터 가져오기 - const { data: likeProjects } = useProjectsByIds(likeIds); - const { data: appliedProjects } = useProjectsByIds(appliedIds); - console.log(likeProjects); - console.log(appliedProjects); + const { data: likeProjectsData } = useProjectsByIds(likeIds); + const { data: appliedProjectsData } = useProjectsByIds(appliedIds); + + // zustand store에 동기화 + useEffect(() => { + if (likeProjectsData) setLikeProjects(likeProjectsData); + }, [likeProjectsData, setLikeProjects]); + useEffect(() => { + if (appliedProjectsData) setAppliedProjects(appliedProjectsData); + }, [appliedProjectsData, setAppliedProjects]); const [tab, setTab] = useState(0); + const handleDeleteProjects = async ( + type: "likeProjects" | "appliedProjects", + ids: string[] + ): Promise => { + if (!user) return; + await removeProjectsFromUser(user.uid, type, ids); + }; + if (!userProfile) { return ; } @@ -52,8 +71,6 @@ const UserProfilePage = (): JSX.Element => { { PROFILE_TABS={PROFILE_TABS} tab={tab} setTab={setTab} - likeProjects={likeProjects ?? []} - appliedProjects={appliedProjects ?? []} + onDeleteProjects={handleDeleteProjects} /> diff --git a/src/shared/stores/projectStore.ts b/src/shared/stores/projectStore.ts new file mode 100644 index 0000000..5ff6c2a --- /dev/null +++ b/src/shared/stores/projectStore.ts @@ -0,0 +1,27 @@ +import { create } from "zustand"; + +import type { ProjectListRes } from "@shared/types/project"; + +interface ProjectStoreState { + likeProjects: ProjectListRes[]; + appliedProjects: ProjectListRes[]; + setLikeProjects: (projects: ProjectListRes[]) => void; + setAppliedProjects: (projects: ProjectListRes[]) => void; + removeLikeProjects: (ids: string[]) => void; + removeAppliedProjects: (ids: string[]) => void; +} + +export const useProjectStore = create((set) => ({ + likeProjects: [], + appliedProjects: [], + setLikeProjects: (projects) => set({ likeProjects: projects }), + setAppliedProjects: (projects) => set({ appliedProjects: projects }), + removeLikeProjects: (ids) => + set((state) => ({ + likeProjects: state.likeProjects.filter((p) => !ids.includes(p.id)), + })), + removeAppliedProjects: (ids) => + set((state) => ({ + appliedProjects: state.appliedProjects.filter((p) => !ids.includes(p.id)), + })), +})); diff --git a/src/shared/ui/DeleteButton.tsx b/src/shared/ui/DeleteButton.tsx new file mode 100644 index 0000000..cb23fad --- /dev/null +++ b/src/shared/ui/DeleteButton.tsx @@ -0,0 +1,41 @@ +import DeleteIcon from "@mui/icons-material/Delete"; +import { Button, styled } from "@mui/material"; +import type { JSX } from "react"; + +interface DeleteButtonProps { + onClick: () => void; + children?: React.ReactNode; + sx?: any; + disabled?: boolean; +} + +const DeleteButton = ({ + onClick, + children = "삭제", + sx, + disabled = false, +}: DeleteButtonProps): JSX.Element => ( + } + onClick={onClick} + sx={sx} + disabled={disabled} + > + {children} + +); + +export default DeleteButton; + +const StyledButton = styled(Button)(({ theme }) => ({ + fontWeight: 600, + borderRadius: theme.spacing(0.8), + boxShadow: "none", + color: theme.palette.error.main, + borderColor: theme.palette.error.main, + "&:hover": { + backgroundColor: theme.palette.error.light, + color: theme.palette.error.contrastText, + }, +})); diff --git a/src/shared/ui/ProjectCard.tsx b/src/shared/ui/ProjectCard.tsx index 5b68c63..dc5118d 100644 --- a/src/shared/ui/ProjectCard.tsx +++ b/src/shared/ui/ProjectCard.tsx @@ -14,6 +14,7 @@ import { useMediaQuery, useTheme, } from "@mui/material"; +import Checkbox from "@mui/material/Checkbox"; import type { JSX } from "react"; import { memo } from "react"; import { Link } from "react-router-dom"; @@ -31,12 +32,18 @@ interface ProjectCardProps { project: ProjectListRes; simple?: boolean; sx?: any; + editMode?: boolean; + selected?: boolean; + onSelect?: () => void; } const ProjectCard = ({ project, simple = false, sx, + editMode = false, + selected = false, + onSelect, }: ProjectCardProps): JSX.Element => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.up("sm")); @@ -46,6 +53,9 @@ const ProjectCard = ({ + {simple && editMode && ( + + )} @@ -172,7 +182,7 @@ const StyledCardContent = styled(CardContent)(({ theme }) => ({ const ProjectHeader = styled(Box)(() => ({ display: "flex", - justifyContent: "flex-start", + justifyContent: "space-between", alignItems: "center", })); diff --git a/src/shared/ui/SnackbarAlert.tsx b/src/shared/ui/SnackbarAlert.tsx new file mode 100644 index 0000000..80d174b --- /dev/null +++ b/src/shared/ui/SnackbarAlert.tsx @@ -0,0 +1,38 @@ +import { + Snackbar, + Alert, + type AlertColor, + type SnackbarOrigin, +} from "@mui/material"; +import type { JSX } from "react"; + +interface SnackbarAlertProps { + open: boolean; + onClose: () => void; + message: string; + severity?: AlertColor; + duration?: number; + anchorOrigin?: SnackbarOrigin; +} + +const SnackbarAlert = ({ + open, + onClose, + message, + severity = "success", + duration = 2500, + anchorOrigin = { vertical: "top", horizontal: "center" }, +}: SnackbarAlertProps): JSX.Element => ( + + + {message} + + +); + +export default SnackbarAlert; From 1aa7731547a95dd526dfe781edcf336f1db2e2cc Mon Sep 17 00:00:00 2001 From: Sara han Date: Fri, 27 Jun 2025 01:09:10 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20useCountdown=20=ED=9B=85=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20=EB=B0=8F=20UserNotFound=20=EC=B9=B4?= =?UTF-8?q?=EC=9A=B4=ED=8A=B8=EB=8B=A4=EC=9A=B4=20UX=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/user-profile/ui/UserNotFound.tsx | 24 +++++++++++++++++++ src/pages/user-profile/ui/UserProfilePage.tsx | 9 +++++-- src/shared/hooks/useCountdown.ts | 18 ++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/pages/user-profile/ui/UserNotFound.tsx create mode 100644 src/shared/hooks/useCountdown.ts diff --git a/src/pages/user-profile/ui/UserNotFound.tsx b/src/pages/user-profile/ui/UserNotFound.tsx new file mode 100644 index 0000000..aec59d1 --- /dev/null +++ b/src/pages/user-profile/ui/UserNotFound.tsx @@ -0,0 +1,24 @@ +import { Box, Typography, Button } from "@mui/material"; +import type { JSX } from "react"; +import { useNavigate } from "react-router-dom"; + +import useCountdown from "@shared/hooks/useCountdown"; + +const UserNotFound = (): JSX.Element => { + const navigate = useNavigate(); + const count = useCountdown(3, () => navigate("/")); + + return ( + + + 😕 유저 정보를 찾을 수 없습니다. + + {count}초 후 홈으로 이동합니다. + + + ); +}; + +export default UserNotFound; diff --git a/src/pages/user-profile/ui/UserProfilePage.tsx b/src/pages/user-profile/ui/UserProfilePage.tsx index a4c0abf..99e2b46 100644 --- a/src/pages/user-profile/ui/UserProfilePage.tsx +++ b/src/pages/user-profile/ui/UserProfilePage.tsx @@ -15,6 +15,8 @@ import { useAuthStore } from "@shared/stores/authStore"; import { useProjectStore } from "@shared/stores/projectStore"; import LoadingSpinner from "@shared/ui/loading-spinner/LoadingSpinner"; +import UserNotFound from "./UserNotFound"; + // 탭 이름 상수 배열 const PROFILE_TABS = [ { label: "관심있는 프로젝트", color: "primary" }, @@ -24,7 +26,7 @@ const PROFILE_TABS = [ const UserProfilePage = (): JSX.Element => { const { user } = useAuthStore(); const uid = user?.uid; - const { data: userProfile } = useUserProfile(uid ?? ""); + const { data: userProfile, isLoading } = useUserProfile(uid ?? ""); // zustand store 사용 const { setLikeProjects, setAppliedProjects } = useProjectStore(); @@ -54,9 +56,12 @@ const UserProfilePage = (): JSX.Element => { await removeProjectsFromUser(user.uid, type, ids); }; - if (!userProfile) { + if (isLoading) { return ; } + if (!userProfile) { + return ; + } return ( diff --git a/src/shared/hooks/useCountdown.ts b/src/shared/hooks/useCountdown.ts new file mode 100644 index 0000000..86a206f --- /dev/null +++ b/src/shared/hooks/useCountdown.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from "react"; + +const useCountdown = (start: number, onEnd?: () => void): number => { + const [count, setCount] = useState(start); + + useEffect(() => { + if (count === 0) { + if (onEnd) onEnd(); + return; + } + const timer = setTimeout(() => setCount((prev) => prev - 1), 1000); + return () => clearTimeout(timer); + }, [count, onEnd]); + + return count; +}; + +export default useCountdown;