diff --git a/public/assets/images/img-404.svg b/public/assets/images/img-404.svg new file mode 100644 index 00000000..10dcbe66 --- /dev/null +++ b/public/assets/images/img-404.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/landing/img-avatar-2.jpg b/public/assets/landing/img-avatar-2.jpg new file mode 100644 index 00000000..06aa35d2 Binary files /dev/null and b/public/assets/landing/img-avatar-2.jpg differ diff --git a/src/api/team/get-ssr-user-groups.ts b/src/api/team/get-ssr-user-groups.ts new file mode 100644 index 00000000..13261487 --- /dev/null +++ b/src/api/team/get-ssr-user-groups.ts @@ -0,0 +1,17 @@ +import { devConsoleError } from "@/lib/error"; +import { serverFetch } from "@/lib/server/server-fetch"; +import { GetUserGroup } from "@/types/group"; + +const getSSRUserGroups = async (): Promise => { + try { + return await serverFetch(`/user/groups`, { + method: "GET", + cache: "no-store", + }); + } catch (e) { + devConsoleError(e); + throw e; + } +}; + +export default getSSRUserGroups; diff --git a/src/app/(routes)/layout.tsx b/src/app/(routes)/layout.tsx index 49d1218f..6653e592 100644 --- a/src/app/(routes)/layout.tsx +++ b/src/app/(routes)/layout.tsx @@ -2,33 +2,24 @@ import { useState, useEffect } from "react"; import { useAuthStore } from "@/store/auth.store"; import { Header } from "@/components/layout"; -import { DropdownOption } from "@/types/option"; export default function RoutesLayout({ children }: { children: React.ReactNode }) { - const [group, setGroup] = useState(null); + const [isLogin, setIsLogin] = useState(false); const user = useAuthStore(state => state.user); + const initialized = useAuthStore(state => state.initialized); useEffect(() => { if (user) { - const userGroupInfo = - user.memberships?.map(mb => { - const groupInfo: DropdownOption = { - id: mb.group.id, - name: mb.group.name, - image: mb.group.image, - }; - return groupInfo; - }) || []; - setGroup(userGroupInfo); + setIsLogin(true); } else { - setGroup(null); + setIsLogin(false); } - }, [user]); + }, [initialized, user]); return ( <> -
+
{children} ); diff --git a/src/app/(routes)/team/[id]/edit/page.tsx b/src/app/(routes)/team/[id]/edit/page.tsx index 60ac4874..1d31fb1b 100644 --- a/src/app/(routes)/team/[id]/edit/page.tsx +++ b/src/app/(routes)/team/[id]/edit/page.tsx @@ -63,6 +63,7 @@ export default function TeamEditPagae() { queryKey: ["getGroups", groupId], }); sessionStorage.setItem("teamEditMessage", "팀이 수정되었습니다."); + queryClient.invalidateQueries({ queryKey: ["getUser"] }); router.replace(`/team/${groupId}`); }, onError: error => { diff --git a/src/app/(routes)/team/[id]/page.tsx b/src/app/(routes)/team/[id]/page.tsx index 26270422..95b07396 100644 --- a/src/app/(routes)/team/[id]/page.tsx +++ b/src/app/(routes)/team/[id]/page.tsx @@ -1,25 +1,36 @@ import { notFound } from "next/navigation"; import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query"; import { getGroupTaskListsforServer } from "@/api/tasklist/index-server"; +import getSSRUserGroups from "@/api/team/get-ssr-user-groups"; +import getSSRUser from "@/api/user/get-ssr-user"; import TeamClientPages from "./team-client"; -export const dynamic = "force-dynamic"; - export default async function TeamPages({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const groupId = Number(id); const queryClient = new QueryClient(); + const groupData = await getGroupTaskListsforServer(groupId); + const userGroup = await getSSRUserGroups(); + const user = await getSSRUser(); + + const isMember = userGroup.some(ug => ug.id === groupId); - if (!groupData) { + if (!groupData || !isMember) { notFound(); } - //getusergroups도 ssr api 만들어서 여기서 조회해서 넘겨. + const memberList = user?.memberships?.filter(mb => mb.groupId === Number(groupId)) || [ + { role: "ADMIN" }, + ]; + + queryClient.setQueryData(["getGroups", groupId], groupData); + + const userRole = memberList[0].role; return ( - + ); } diff --git a/src/app/(routes)/team/[id]/team-client.tsx b/src/app/(routes)/team/[id]/team-client.tsx index 26d4644e..955e3d22 100644 --- a/src/app/(routes)/team/[id]/team-client.tsx +++ b/src/app/(routes)/team/[id]/team-client.tsx @@ -1,5 +1,5 @@ "use client"; -import { redirect, notFound } from "next/navigation"; +import { redirect } from "next/navigation"; import { useState, useEffect } from "react"; import { Container } from "@/components/layout"; import { GetGroupsResponse, TodoListProps } from "@/types/group"; @@ -8,23 +8,29 @@ import { TeamTitle, TodoList, TeamMember, TeamReport } from "@/components/featur import { useAuthStore } from "@/store/auth.store"; import { useToast } from "@/providers/toast-provider"; import TeamSkeleton from "@/components/skeleton-ui/team-skeleton"; +import { useQuery } from "@tanstack/react-query"; +import getGroups from "@/api/team/get-groups"; export default function TeamClientPages({ groupId, - groups, + userRole, }: { groupId: number; - groups: GetGroupsResponse; + userRole: string; }) { const { showToast } = useToast(); const user = useAuthStore(state => state.user); const initialized = useAuthStore(state => state.initialized); - const [userRole, setUserRole] = useState("MEMBER"); const [members, setMembers] = useState([]); const [todoLists, setTodoLists] = useState(); + const { data: groups } = useQuery({ + queryKey: ["getGroups", groupId], + queryFn: () => getGroups(groupId), + }); + useEffect(() => { setTimeout(() => { const teamJoinMessage = sessionStorage.getItem("teamJoinMessage"); @@ -42,15 +48,6 @@ export default function TeamClientPages({ } }, [groups]); - useEffect(() => { - if (user?.memberships) { - const isBeing = user.memberships.filter(mb => mb.groupId === Number(groupId)); - if (isBeing[0]?.role) { - setUserRole(isBeing[0].role); - } - } - }, [user]); - if (!initialized) { return ; } @@ -59,20 +56,14 @@ export default function TeamClientPages({ redirect("/"); } - const isMember = user.memberships?.some(mb => mb.groupId === groupId); - - if (!isMember) { - notFound(); - } - const { id: userId } = user; return ( - + - + ); } diff --git a/src/app/(routes)/team/create/page.tsx b/src/app/(routes)/team/create/page.tsx index c042615e..300b9c97 100644 --- a/src/app/(routes)/team/create/page.tsx +++ b/src/app/(routes)/team/create/page.tsx @@ -11,10 +11,14 @@ import IcProfile from "@/assets/icons/ic-image-circle.svg"; import IcEdit from "@/assets/icons/ic-pencil-border.svg"; import { devConsoleError } from "@/lib/error"; import { useToast } from "@/providers/toast-provider"; +import { useQueryClient } from "@tanstack/react-query"; +import { useAlert } from "@/providers/alert-provider"; export default function TeamCreatePage() { const router = useRouter(); const { showToast } = useToast(); + const { showAlert } = useAlert(); + const queryClient = useQueryClient(); const [formData, setFormData] = useState({ name: "", @@ -59,7 +63,8 @@ export default function TeamCreatePage() { mutationFn: postGroups, onSuccess: res => { sessionStorage.setItem("teatCreateMessage", "팀이 생성되었습니다."); - router.push(`/team/${res.id}`); + queryClient.invalidateQueries({ queryKey: ["getUser"] }); + router.replace(`/team/${res.id}`); }, onError: error => { devConsoleError(error); @@ -69,6 +74,10 @@ export default function TeamCreatePage() { const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); + if (formData.name.length === 0) { + showAlert("팀 명은 공란일 수 없습니다."); + return; + } if (selectedImgFile) { uploadImageMutate.mutate({ url: selectedImgFile }); } else { diff --git a/src/app/(routes)/team/join/page.tsx b/src/app/(routes)/team/join/page.tsx index 2bb8bcf6..fd86040f 100644 --- a/src/app/(routes)/team/join/page.tsx +++ b/src/app/(routes)/team/join/page.tsx @@ -9,11 +9,15 @@ import postTeamJoin from "@/api/team/post-join-team"; import { useToast } from "@/providers/toast-provider"; import axios from "axios"; import { devConsoleError } from "@/lib/error"; +import { useQueryClient } from "@tanstack/react-query"; +import { useAlert } from "@/providers/alert-provider"; export default function TeamJoinPage() { const router = useRouter(); + const queryClient = useQueryClient(); const user = useAuthStore(state => state.user); const { showToast } = useToast(); + const { showAlert } = useAlert(); const [formData, setFormData] = useState({ userEmail: "", @@ -38,7 +42,9 @@ export default function TeamJoinPage() { mutationFn: postTeamJoin, onSuccess: res => { sessionStorage.setItem("teamJoinMessage", "팀에 합류했습니다."); + queryClient.invalidateQueries({ queryKey: ["getUser"] }); setFormData(fd => ({ ...fd, token: "" })); + router.replace(`/team/${res.groupId}`); }, onError: error => { @@ -60,6 +66,10 @@ export default function TeamJoinPage() { const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); + if (formData.token.length === 0) { + showAlert("유효한 토큰을 입력해주세요"); + return; + } joinMutation.mutate(formData); }; return ( diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 00000000..25763c90 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,31 @@ +import Image from "next/image"; +import { Container } from "@/components/layout"; +import { Button } from "@/components/ui"; +import { Header } from "@/components/layout"; +import ButtonNotFound from "@/components/features/not-found/button-404"; + +export default function NotFound() { + return ( + <> +
+ + +

+ 페이지를 찾을 수 없습니다. +

+
+ + +
+
+ + ); +} diff --git a/src/components/features/article/layout.tsx b/src/components/features/article/layout.tsx index ab2b0343..8145e64d 100644 --- a/src/components/features/article/layout.tsx +++ b/src/components/features/article/layout.tsx @@ -79,7 +79,7 @@ export function ArticleConfirmModal({ }: ArticleConfirmModalProps) { return ( -
+

{message}

diff --git a/src/components/features/auth/forgot-password.tsx b/src/components/features/auth/forgot-password.tsx index e3b64a73..6a39b889 100644 --- a/src/components/features/auth/forgot-password.tsx +++ b/src/components/features/auth/forgot-password.tsx @@ -36,7 +36,7 @@ export default function ForgotPassword({ isOpen, onClose }: ForgotPasswordProps) return ( -
+

비밀번호 재설정

비밀번호 재설정 링크를 보내드립니다.

diff --git a/src/components/features/auth/form-fields.tsx b/src/components/features/auth/form-fields.tsx index b6e3070f..13ecbd04 100644 --- a/src/components/features/auth/form-fields.tsx +++ b/src/components/features/auth/form-fields.tsx @@ -36,7 +36,11 @@ export function SignUpFormFields({ isPending }: { isPending: boolean }) { caption="(닉네임 중복 불가, 최대 10자)" errorMsg={errors?.nickname?.message} > - + generateFloatingHearts(35)); const sectionRef = useRef(null); - const isInView = useInView(sectionRef, { amount: 0.25, once: false }); + const isInView = useInView(sectionRef, { amount: 0.2, once: true }); useEffect(() => { if (isInView && !showFloatingHearts) { diff --git a/src/components/features/landing/product-demo.tsx b/src/components/features/landing/product-demo.tsx index aac4a305..c4346fbb 100644 --- a/src/components/features/landing/product-demo.tsx +++ b/src/components/features/landing/product-demo.tsx @@ -29,7 +29,7 @@ const mockTasks = [ }, { id: 3, - title: "빌표자료 준비하기", + title: "발표자료 준비하기", completed: false, frequencyType: "한 번", }, @@ -134,7 +134,7 @@ export default function ProductDemo() { {tasks.length}개 - +
@@ -143,7 +143,7 @@ export default function ProductDemo() { {tasks.filter(t => t.completed).length}개 - +
diff --git a/src/components/features/my/user-profile.tsx b/src/components/features/my/user-profile.tsx index 2171c5da..823e7b7e 100644 --- a/src/components/features/my/user-profile.tsx +++ b/src/components/features/my/user-profile.tsx @@ -138,7 +138,11 @@ export function ProfileUpdateFormField({ label="닉네임" caption="(닉네임 중복 불가, 최대 10자)" > - + diff --git a/src/components/features/not-found/button-404.tsx b/src/components/features/not-found/button-404.tsx new file mode 100644 index 00000000..107d7abd --- /dev/null +++ b/src/components/features/not-found/button-404.tsx @@ -0,0 +1,13 @@ +"use client"; +import { Button } from "@/components/ui"; +import { useRouter } from "next/navigation"; + +export default function ButtonNotFound() { + const router = useRouter(); + + return ( + + ); +} diff --git a/src/components/features/tasklist/task-recurring/task-recurring-update-modal.tsx b/src/components/features/tasklist/task-recurring/task-recurring-update-modal.tsx index dbca46fd..e0e5bf2a 100644 --- a/src/components/features/tasklist/task-recurring/task-recurring-update-modal.tsx +++ b/src/components/features/tasklist/task-recurring/task-recurring-update-modal.tsx @@ -102,25 +102,27 @@ function FormField({ type }: { type: FormFieldType }) { /> {type !== "nameOnly" && ( - - - ( - <> - - - - )} - /> - +
+ + + ( + <> + + + + )} + /> + +
)}
diff --git a/src/components/features/team/team-title.tsx b/src/components/features/team/team-title.tsx index 3a3d62f4..855be4cf 100644 --- a/src/components/features/team/team-title.tsx +++ b/src/components/features/team/team-title.tsx @@ -10,17 +10,20 @@ import { useMutation } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; import { useToast } from "@/providers/toast-provider"; import { devConsoleError } from "@/lib/error"; +import { useQueryClient } from "@tanstack/react-query"; export default function TeamTitle({ name, id, userRole }: teamTitleProps) { const router = useRouter(); + const queryClient = useQueryClient(); + const { showAlert } = useAlert(); const { showToast } = useToast(); const { mutate } = useMutation({ mutationFn: deleteTeam, onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["getUser"] }); router.replace("/"); - //TODO: 랜딩페이지에서도 토스트 받을 수 있도록 수정 필요 }, onError: error => { showToast("팀 삭제에 문제가 생겼습니다.", "error"); diff --git a/src/components/layout/header/dropdown/group-dropdown.tsx b/src/components/layout/header/dropdown/group-dropdown.tsx index 4c2b0132..c61eda10 100644 --- a/src/components/layout/header/dropdown/group-dropdown.tsx +++ b/src/components/layout/header/dropdown/group-dropdown.tsx @@ -1,18 +1,23 @@ +"use client"; import { useState, useEffect } from "react"; import { usePathname } from "next/navigation"; import { Dropdown, Avatar } from "@/components/ui"; import { DropdownOption } from "@/types/option"; +import { useQuery } from "@tanstack/react-query"; +import getUser from "@/api/user/get-user"; import IcArrow from "@/assets/icons/ic-arrow-down.svg"; import cn from "@/lib/cn"; interface dropdownProps { - groups: DropdownOption[]; className?: string; + isLogin: boolean; } -export function GroupDropdown({ groups, className }: dropdownProps) { +export function GroupDropdown({ className, isLogin = false }: dropdownProps) { const pathname = usePathname(); + const { data: user } = useQuery({ queryKey: ["getUser"], queryFn: getUser }); + const [selectedGroup, setSelectedGroup] = useState({ id: 0, name: "", @@ -20,36 +25,46 @@ export function GroupDropdown({ groups, className }: dropdownProps) { }); useEffect(() => { - if (groups.length !== 0) { - let groupIdString: string | undefined; + if (user?.memberships) { + if (user.memberships.length !== 0) { + { + let groupIdString: string | undefined; - const pathSegments = pathname.split("/"); + const pathSegments = pathname.split("/"); - if (pathSegments[1] === "team" && pathSegments[2]) { - groupIdString = pathSegments[2]; - } + if (pathSegments[1] === "team" && pathSegments[2]) { + groupIdString = pathSegments[2]; + } + + let initialGroup = user.memberships[0]; - let initialGroup = groups[0]; + if (groupIdString) { + const foundGroup = user.memberships.find(mb => String(mb.group.id) === groupIdString); - if (groupIdString) { - const foundGroup = groups.find(group => String(group.id) === groupIdString); + if (foundGroup) { + initialGroup = foundGroup; + } + } - if (foundGroup) { - initialGroup = foundGroup; + setSelectedGroup(prev => ({ + ...prev, + id: initialGroup.group.id, + image: initialGroup.group.image, + name: initialGroup.group.name, + })); } } - setSelectedGroup(prev => ({ - ...prev, - id: initialGroup.id, - image: initialGroup.image, - name: initialGroup.name, - })); } - }, [groups, pathname]); + }, [user, pathname]); const handleGroupSelect = ({ name, image }: DropdownOption) => { setSelectedGroup(prev => ({ ...prev, name: name, image: image })); }; + + if (!isLogin) { + return <>; + } + return ( - {groups.map(group => { - return ( - - - - {group.name} - - - ); - })} + {user?.memberships && + user.memberships.map(mb => { + return ( + + + + {mb.group.name} + + + ); + })} ); diff --git a/src/components/layout/header/header-sidebar.tsx b/src/components/layout/header/header-sidebar.tsx index 485ab7c0..5fc29e54 100644 --- a/src/components/layout/header/header-sidebar.tsx +++ b/src/components/layout/header/header-sidebar.tsx @@ -5,14 +5,17 @@ import IcCancel from "@/assets/icons/ic-cancel.svg"; import IcBoard from "@/assets/icons/ic-board.svg"; import Logo from "@/assets/icons/ic-logo.svg"; import { Avatar } from "@/components/ui"; -import { DropdownOption } from "@/types/option"; +import { useQuery } from "@tanstack/react-query"; +import getUser from "@/api/user/get-user"; interface sidebarProps { - groups: DropdownOption[] | null; onClick: (open: boolean) => void; + isLogin: boolean; } -export function HeaderSidebar({ groups, onClick }: sidebarProps) { +export function HeaderSidebar({ onClick, isLogin = false }: sidebarProps) { + const { data: user } = useQuery({ queryKey: ["getUser"], queryFn: getUser }); + return (
@@ -24,23 +27,27 @@ export function HeaderSidebar({ groups, onClick }: sidebarProps) {
- {groups && - groups.map(group => ( -
- + {isLogin ? ( + user?.memberships && + user.memberships.map(mb => ( +
+
- {group.name} + {mb.group.name}
- ))} + )) + ) : ( + <> + )} () => { setOpen(newOpen); }; + if (isLoginPage) { return (
- +
@@ -37,23 +39,31 @@ export default function Header({ isLoginPage, groups, user }: HeaderProps) { return (
- {open && } + {open && }
- +
- {groups?.length !== 0 && groups && } - 자유게시판 + {isLogin && } + + 자유게시판 +
- {user ? : 로그인} + {user ? ( + + ) : ( + + 로그인 + + )}
diff --git a/src/components/ui/input/input-password.tsx b/src/components/ui/input/input-password.tsx index 3a0a0447..37d19dc7 100644 --- a/src/components/ui/input/input-password.tsx +++ b/src/components/ui/input/input-password.tsx @@ -32,6 +32,7 @@ export default function InputPassword({ {...props} />