diff --git a/src/features/auth/lib/form.utils.ts b/src/features/auth/lib/form.utils.ts index 55230d7..1d2087c 100644 --- a/src/features/auth/lib/form.utils.ts +++ b/src/features/auth/lib/form.utils.ts @@ -154,7 +154,8 @@ export const profileFormValidation = formValidation.shape({ .matches( /^\s*$|^((ftp|http|https):\/\/)?(www.)?(?!.*(ftp|http|https|www.))[a-zA-Z0-9_-]+(\.[a-zA-Z]+)+((\/)[\w#]+)*(\/\w+\?[a-zA-Z0-9_]+=\w+(&[a-zA-Z0-9_]+=\w+)*)?$/gm, 'URL 형식에 맞게 입력해주세요.', - ), + ) + .required('포트폴리오 URL을 입력해주세요.'), }); export const profileFormConfig: FormConfigType = { diff --git a/src/features/portfolio/hooks/usePortfolioList.ts b/src/features/portfolio/hooks/usePortfolioList.ts index ac40733..c265688 100644 --- a/src/features/portfolio/hooks/usePortfolioList.ts +++ b/src/features/portfolio/hooks/usePortfolioList.ts @@ -23,15 +23,16 @@ export const usePortfolioList = ({ }, getNextPageParam: lastPage => { + console.log(lastPage); // 더 엄격한 조건 체크 - if (!lastPage || !lastPage.content || lastPage.last) { + if (!lastPage || !lastPage.content) { return undefined; } // 현재 페이지의 아이템이 size보다 적으면 마지막 페이지 if (lastPage.content.length < size) { return undefined; } - return lastPage.number + 1; + return lastPage.offset / size + 1; }, initialPageParam: 0, diff --git a/src/features/portfolio/model/types.ts b/src/features/portfolio/model/types.ts index 79b47b1..2d46c5c 100644 --- a/src/features/portfolio/model/types.ts +++ b/src/features/portfolio/model/types.ts @@ -119,14 +119,9 @@ export interface Sort { // API Response Interface export interface PortfolioResponse { content: Portfolio[]; - pageable: Pageable; - size: number; - number: number; - sort: Sort; - numberOfElements: number; - first: boolean; - last: boolean; - empty: boolean; + hasNext: boolean; + offset: number; + pageSize: number; } export type PortfolioListApiResponse = ApiResponse; diff --git a/src/features/user/model/user.store.ts b/src/features/user/model/user.store.ts index 9cbcdb9..7d98cc8 100644 --- a/src/features/user/model/user.store.ts +++ b/src/features/user/model/user.store.ts @@ -45,7 +45,10 @@ export const useUserStore = create( }); }, clearUserData: () => { - set(initialState); + set({ + userData: null, + loading: false, + }); }, load: () => { set({ loading: true }); diff --git a/src/pages/MyPage/MyPage.tsx b/src/pages/MyPage/MyPage.tsx index df9d576..b5e2461 100644 --- a/src/pages/MyPage/MyPage.tsx +++ b/src/pages/MyPage/MyPage.tsx @@ -1,20 +1,8 @@ -import { useNavigate } from 'react-router-dom'; - import styles from './MyPage.module.scss'; -import { useRoleGuard } from '@/shared/hook/useRoleGuard'; import { ContentLayout, SideTab, useMyTab } from '@/widgets'; export const MyPage = () => { - const navigate = useNavigate(); - // REAL_NEWBIE면 register page로 redirect - useRoleGuard({ - requiredRoles: ['JUST_NEWBIE', 'ADMIN', 'OLD_NEWBIE', 'USER'], - onAccessDenied: () => { - navigate('/register'); - }, - }); - const { activeTabItem, isActivePath } = useMyTab(); return (
diff --git a/src/shared/hook/useRegistAlarm.ts b/src/shared/hook/useRegistAlarm.ts index 0bb7b68..62ee480 100644 --- a/src/shared/hook/useRegistAlarm.ts +++ b/src/shared/hook/useRegistAlarm.ts @@ -7,6 +7,16 @@ import { useUserStore } from '@/features/user/model/user.store'; const excludePath = ['/register', '/my']; +const checkPath = (path: string, pathList: string[]) => { + for (const excludePath of pathList) { + if (path.startsWith(excludePath)) { + return false; + } + } + + return true; +}; + export const useRegistAlarm = () => { const navigate = useNavigate(); const location = useLocation(); @@ -14,7 +24,7 @@ export const useRegistAlarm = () => { useEffect(() => { if (!userData) return; - if (userData.role === 'REAL_NEWBIE' && !excludePath.includes(location.pathname)) { + if (userData.role === 'REAL_NEWBIE' && checkPath(location.pathname, excludePath)) { void customConfirm({ title: '유저 등록', text: '아직 등록된 유저 프로필이 없습니다!\n프로필을 등록해주세요.', diff --git a/src/shared/hook/useRoleGuard.ts b/src/shared/hook/useRoleGuard.ts index eb64e99..bfa146f 100644 --- a/src/shared/hook/useRoleGuard.ts +++ b/src/shared/hook/useRoleGuard.ts @@ -4,12 +4,13 @@ import { useShallow } from 'zustand/shallow'; import { customConfirm } from '../ui'; +import type { UserDataState } from '@/features/user/model/user.store'; import { useUserStore } from '@/features/user/model/user.store'; import type { UserRole } from '@/features/user/user.dto'; interface UseRoleGuardProps { requiredRoles: UserRole[]; // 허용된 역할 목록 - onAccessDenied?: () => void; // 접근 제한 시 실행할 이벤트 + onAccessDenied?: (userData: UserDataState | null) => void; // 접근 제한 시 실행할 이벤트 } export const useRoleGuard = ({ requiredRoles, onAccessDenied }: UseRoleGuardProps) => { @@ -37,7 +38,7 @@ export const useRoleGuard = ({ requiredRoles, onAccessDenied }: UseRoleGuardProp if (!userData || !requiredRoles.includes(userData.role)) { if (onAccessDenied) { - onAccessDenied(); + onAccessDenied(userData); } else { void defaultDeniedHandler(); } diff --git a/src/widgets/Layout/ui/Header/Header.module.scss b/src/widgets/Layout/ui/Header/Header.module.scss index b8d2fcd..b44b791 100644 --- a/src/widgets/Layout/ui/Header/Header.module.scss +++ b/src/widgets/Layout/ui/Header/Header.module.scss @@ -91,6 +91,7 @@ flex-basis: 1; column-gap: 3rem; align-items: center; + min-width: 15rem; } } diff --git a/src/widgets/Layout/ui/Header/Header.tsx b/src/widgets/Layout/ui/Header/Header.tsx index a6705a5..bc999ea 100644 --- a/src/widgets/Layout/ui/Header/Header.tsx +++ b/src/widgets/Layout/ui/Header/Header.tsx @@ -25,10 +25,10 @@ export const Header = () => { const { pathname } = useLocation(); const navigate = useNavigate(); const open = useModalStore(state => state.actions.open); - const { userData } = useUserStore( + const { userData, loading } = useUserStore( useShallow(state => ({ userData: state.userData, - actions: state.actions, + loading: state.loading, })), ); const [isMobile, setIsMobile] = useState(false); @@ -200,51 +200,55 @@ export const Header = () => { ))} -
- -
+ {loading ? ( +
+ ) : ( +
-
- { - if (userData) navigate('/like'); - else { - void customConfirm({ - text: '로그인이 필요합니다.', - title: '로그인', - icon: 'info', - }); - } - }} - /> - {userData ? ( - - ) : ( -
+ { - open('login'); + if (userData) navigate('/like'); + else { + void customConfirm({ + text: '로그인이 필요합니다.', + title: '로그인', + icon: 'info', + }); + } }} - > - 로그인 - - )} -
{' '} + /> + {userData ? ( + + ) : ( + + )} +
+ )} {isSearch && (
diff --git a/src/widgets/PortfolioGrid/PortFolioGrid.module.scss b/src/widgets/PortfolioGrid/PortFolioGrid.module.scss index 3c389fb..67cecd3 100644 --- a/src/widgets/PortfolioGrid/PortFolioGrid.module.scss +++ b/src/widgets/PortfolioGrid/PortFolioGrid.module.scss @@ -19,3 +19,7 @@ gap: 1.25rem; width: 100%; } + +.loading { + padding: 2.5rem 0; +} diff --git a/src/widgets/PortfolioGrid/PortFolioGrid.tsx b/src/widgets/PortfolioGrid/PortFolioGrid.tsx index 9633d2a..8fa4dbb 100644 --- a/src/widgets/PortfolioGrid/PortFolioGrid.tsx +++ b/src/widgets/PortfolioGrid/PortFolioGrid.tsx @@ -4,7 +4,7 @@ import { PortfolioCard } from '@/features'; import { usePortfolioList } from '@/features/portfolio/hooks/usePortfolioList'; import type { PortfolioParams } from '@/features/portfolio/model/types'; import { useIntersectionObserver } from '@/shared/hook/useIntersectionObserver'; -import { Loader } from '@/shared/ui'; +import { TripleDot } from '@/shared/ui'; export const PortFolioGrid = ({ params }: { params: PortfolioParams }) => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status, isError } = @@ -23,40 +23,19 @@ export const PortFolioGrid = ({ params }: { params: PortfolioParams }) => { if (isError) return
Error loading portfolios
; const portfolios = data?.pages?.flatMap(page => page?.content ?? []).filter(Boolean) ?? []; - if (portfolios.length === 0) { - return ( -
- - 검색된 내용이 없습니다. -
- ); - } return (
- {portfolios.map(portfolio => ( - - ))} + {portfolios.length > 0 ? ( + portfolios.map(portfolio => ) + ) : ( +
검색 결과가 없습니다.
+ )}
-
- {isFetchingNextPage ? ( -
- -
- ) : hasNextPage ? ( -
Intersection Observer Trigger
- ) : null} + +
+ {isFetchingNextPage && }
); diff --git a/src/widgets/SettingUser/ContentLayout.tsx b/src/widgets/SettingUser/ContentLayout.tsx index 8379ecf..be960c1 100644 --- a/src/widgets/SettingUser/ContentLayout.tsx +++ b/src/widgets/SettingUser/ContentLayout.tsx @@ -33,16 +33,33 @@ interface ContentLayoutProps { export const ContentLayout = ({ activeTab }: ContentLayoutProps) => { // 로그인한 유저만 접근 가능 const { userData } = useRoleGuard({ - requiredRoles: ['ADMIN', 'JUST_NEWBIE', 'OLD_NEWBIE', 'REAL_NEWBIE', 'USER'], - onAccessDenied: () => { + requiredRoles: ['ADMIN', 'JUST_NEWBIE', 'OLD_NEWBIE', 'USER'], + onAccessDenied: userData => { + if (!userData) { + void customConfirm({ + title: '잘못된 접근', + text: '유저 정보를 확인할 수 없습니다.\n로그인하고 다시 시도해주세요.', + icon: 'warning', + showCancelButton: false, + allowOutsideClick: false, + }).then(result => { + if (result.isConfirmed) { + navigate('/'); + return; + } + }); + return; + } + void customConfirm({ title: '잘못된 접근', - text: '유저 정보를 확인할 수 없습니다.\n로그인하고 다시 시도해주세요.', + text: '유저 정보를 확인할 수 없습니다.\n회원 정보를 먼저 등록해주세요.', icon: 'warning', showCancelButton: false, + allowOutsideClick: false, }).then(result => { if (result.isConfirmed) { - navigate('/'); + navigate('/register'); return; } }); diff --git a/src/widgets/SettingUser/SetProfile.tsx b/src/widgets/SettingUser/SetProfile.tsx index 2c6d153..18d095b 100644 --- a/src/widgets/SettingUser/SetProfile.tsx +++ b/src/widgets/SettingUser/SetProfile.tsx @@ -27,14 +27,14 @@ export const SetProfile = ({ userData }: SetProfileProps) => { const { socials, majorJobGroup, minorJobGroup, ...rest } = data; const majorOption = { - value: majorJobGroup, + value: majorJobGroup || '', label: JOB_CATEGORIES.find(majorCatergory => majorCatergory.value === majorJobGroup)?.label ?? '알 수 없음', }; const minorOption = { - value: minorJobGroup, + value: minorJobGroup || '', label: JOB_SUB_CATEGORY.find(minorCategory => minorCategory.value === minorJobGroup)?.label ?? '알 수 없음',