diff --git a/package-lock.json b/package-lock.json index 30534de..8f01b9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,7 @@ "imagemin-webp": "^8.0.0", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router-dom": "^7.7.0", - "tailwindcss": "^4.1.11" + "react-router-dom": "^7.7.0" }, "devDependencies": { "@eslint/js": "^9.30.1", @@ -27,10 +26,13 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.4.21", "eslint": "^9.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.12", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", "vite": "^7.0.4", @@ -3122,6 +3124,11 @@ "tailwindcss": "4.1.11" } }, + "node_modules/@tailwindcss/node/node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==" + }, "node_modules/@tailwindcss/oxide": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", @@ -3368,6 +3375,11 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==" + }, "node_modules/@tanstack/query-core": { "version": "5.85.3", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.3.tgz", @@ -3895,6 +3907,43 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -6113,6 +6162,19 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -7450,6 +7512,15 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", @@ -7909,7 +7980,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7919,6 +7989,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8675,9 +8751,10 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==" + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", + "dev": true }, "node_modules/tapable": { "version": "2.2.2", diff --git a/package.json b/package.json index 1de4c03..7aa4e32 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,7 @@ "imagemin-webp": "^8.0.0", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router-dom": "^7.7.0", - "tailwindcss": "^4.1.11" + "react-router-dom": "^7.7.0" }, "devDependencies": { "@eslint/js": "^9.30.1", @@ -29,10 +28,13 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.4.21", "eslint": "^9.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.12", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", "vite": "^7.0.4", diff --git a/src/api/PostOAuth.tsx b/src/api/PostOAuth.tsx index daf7354..8dc6ffd 100644 --- a/src/api/PostOAuth.tsx +++ b/src/api/PostOAuth.tsx @@ -36,7 +36,7 @@ export default function PostOAuth() { const data: { accessToken: string; isOnboarded: boolean } = await res.json(); - localStorage.setItem('accessToken', data.accessToken); + sessionStorage.setItem('accessToken', data.accessToken); if (data.isOnboarded) { navigate('/main', { replace: true }); diff --git a/src/api/fetchChapters.ts b/src/api/fetchChapters.ts index 00a5b7c..3f1e7c2 100644 --- a/src/api/fetchChapters.ts +++ b/src/api/fetchChapters.ts @@ -3,7 +3,7 @@ import type { Chapter } from '../types/@common/chapter'; import { transformChapters } from '../utils/transformChapter'; export default async function fetchChapters(): Promise { - const accessToken = localStorage.getItem('accessToken'); + const accessToken = sessionStorage.getItem('accessToken'); try { const response = await fetch(`https://grav-it.inuappcenter.kr/api/v1/learning/chapters`, { method: 'GET', diff --git a/src/api/fetchMainInfo.ts b/src/api/fetchMainInfo.ts index e2ac867..0ba53b9 100644 --- a/src/api/fetchMainInfo.ts +++ b/src/api/fetchMainInfo.ts @@ -3,7 +3,7 @@ import { ApiError } from '../types/@common/api'; import type MainPageResponse from '../types/api/main'; export default async function fetchMainInfo(): Promise { - const accessToken = localStorage.getItem('accessToken'); + const accessToken = sessionStorage.getItem('accessToken'); try { const response = await fetch(API_ENDPOINTS.main.base, { diff --git a/src/api/getMypage.ts b/src/api/getMypage.ts new file mode 100644 index 0000000..cdfc2b2 --- /dev/null +++ b/src/api/getMypage.ts @@ -0,0 +1,28 @@ +import { API_ENDPOINTS } from '../constants/api'; + +export const getMypage = async () => { + const accessToken = sessionStorage.getItem('accessToken'); + + try { + const response = await fetch(`${API_ENDPOINTS.users.base}/my-page`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + + if (!response.ok) { + const errorData = await response.json(); + console.error('Error response:', response.status, errorData); + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('Fetch error:', error); + throw error; + } +}; diff --git a/src/api/getMypage.tsx b/src/api/getMypage.tsx deleted file mode 100644 index 6c18973..0000000 --- a/src/api/getMypage.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import axios from 'axios'; -import type { AxiosResponse } from 'axios'; -import { API_ENDPOINTS } from '../constants/api'; - -export const GetMypage = async () => { - const accessToken = localStorage.getItem('accessToken'); - - try { - const response: AxiosResponse = await axios.get(`${API_ENDPOINTS.users.base}/my-page`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - withCredentials: true, - }); - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - if (error.response) { - const { status, data } = error.response; - console.error('Error response:', status, data); - } else if (error.request) { - console.error('No response received:', error.request); - } else { - console.error('Error setting up request:', error.message); - } - } - - throw error; - } -}; diff --git a/src/api/getTierLeagues.ts b/src/api/getTierLeagues.ts new file mode 100644 index 0000000..c5fd448 --- /dev/null +++ b/src/api/getTierLeagues.ts @@ -0,0 +1,29 @@ +import { API_ENDPOINTS } from '../constants/api'; + +export const getTierLeagues = async (id: number, num: number) => { + const accessToken = sessionStorage.getItem('accessToken'); + + try { + const response = await fetch(API_ENDPOINTS.leagues.tier(id, num), { + method: 'GET', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + + if (!response.ok) { + const errorData = await response.json(); + console.error('Error response:', response.status, errorData); + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log(data); + return data; + } catch (error) { + console.error('Fetch error:', error); + throw error; + } +}; diff --git a/src/api/getUserLeagues.ts b/src/api/getUserLeagues.ts new file mode 100644 index 0000000..df127fe --- /dev/null +++ b/src/api/getUserLeagues.ts @@ -0,0 +1,29 @@ +import { API_ENDPOINTS } from '../constants/api'; + +export const getUserLeagues = async (num: number) => { + const accessToken = sessionStorage.getItem('accessToken'); + + try { + const response = await fetch(API_ENDPOINTS.leagues.user(num), { + method: 'GET', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + + if (!response.ok) { + const errorData = await response.json(); + console.error('Error response:', response.status, errorData); + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log(data); + return data; + } catch (error) { + console.error('Fetch error:', error); + throw error; + } +}; diff --git a/src/api/patchOnBoarding.tsx b/src/api/patchOnBoarding.tsx index ac48e03..e4c8d82 100644 --- a/src/api/patchOnBoarding.tsx +++ b/src/api/patchOnBoarding.tsx @@ -3,7 +3,7 @@ import type { AxiosResponse } from 'axios'; import { API_ENDPOINTS } from '../constants/api'; export const PatchOnBoarding = async (nickname: string, profilePhotoNumber: number) => { - const accessToken = localStorage.getItem('accessToken'); + const accessToken = sessionStorage.getItem('accessToken'); try { const response: AxiosResponse = await axios.patch( diff --git a/src/components/@common/header/Header.tsx b/src/components/@common/header/Header.tsx index 073291b..db56e1d 100644 --- a/src/components/@common/header/Header.tsx +++ b/src/components/@common/header/Header.tsx @@ -1,27 +1,47 @@ +import { useEffect, useState } from 'react'; import LogoButton from './LogoButton'; import LoginButton from './LoginButton'; import HeaderNav from './HeaderNav'; import StartButton from './StartButton'; function Header() { - const accessToken = localStorage.getItem('accessToken'); - const isLoggedIn = Boolean(accessToken); - - return ( -
-
-
- - -
- -
- {isLoggedIn && } - {isLoggedIn && } -
-
-
- ); + const [isLoggedIn, setIsLoggedIn] = useState( + Boolean(sessionStorage.getItem('accessToken')) + ); + + useEffect(() => { + const handleStorageChange = () => { + setIsLoggedIn(Boolean(sessionStorage.getItem('accessToken'))); + }; + + window.addEventListener('storage', handleStorageChange); + + const interval = setInterval(() => { + const current = Boolean(sessionStorage.getItem('accessToken')); + setIsLoggedIn((prev) => (prev !== current ? current : prev)); + }, 200); + + return () => { + window.removeEventListener('storage', handleStorageChange); + clearInterval(interval); + }; + }, []); + + return ( +
+
+
+ + +
+ +
+ {isLoggedIn && } + {isLoggedIn && } +
+
+
+ ); } export default Header; diff --git a/src/components/@common/level-info/LevelProgressCircle.tsx b/src/components/@common/level-info/LevelProgressCircle.tsx new file mode 100644 index 0000000..5d13582 --- /dev/null +++ b/src/components/@common/level-info/LevelProgressCircle.tsx @@ -0,0 +1,60 @@ +interface LevelProgressCircleProps { + level: number; + maxLevel?: number; + children?: React.ReactNode; +} + +export default function LevelProgressCircle({ + level, + maxLevel = 10, + children, +}: LevelProgressCircleProps) { + const size = 46; + const strokeWidth = 4; + const radius = (size - strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + const progress = Math.min(level / maxLevel, 1); + const dashOffset = circumference * (1 - progress); + + return ( +
+
+ {children} +
+ + + + + + + + + + + + +
+ ); +} diff --git a/src/components/league-page/TierSelector.tsx b/src/components/league-page/TierSelector.tsx new file mode 100644 index 0000000..8551fd1 --- /dev/null +++ b/src/components/league-page/TierSelector.tsx @@ -0,0 +1,65 @@ +import { useRef } from 'react'; + +type Tier = { + id: number; + name: string; + icon: React.FC>; + exp: number; +}; + +interface TierSelectorProps { + tiers: Tier[]; + selectedTierId: number | null; + onSelectTier: (tierId: number) => void; +} + +export default function TierSelector({ tiers, selectedTierId, onSelectTier }: TierSelectorProps) { + const itemRefs = useRef<(HTMLDivElement | null)[]>([]); + + const handleClick = (tierId: number, index: number) => { + onSelectTier(tierId); + itemRefs.current[index]?.scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'nearest', + }); + }; + + return ( +
+
+ {tiers.map((tier, index) => { + const isSelected = tier.id === selectedTierId; + const TierIcon = tier.icon; + + return ( +
{ itemRefs.current[index] = el; }} + onClick={() => handleClick(tier.id, index)} + className={`flex flex-col items-center cursor-pointer transition-all duration-300 justify-center ${ + isSelected ? 'scale-110 px-4' : '' + } min-h-[140px]`} + > +
+ +
+ +
+ {tier.name} + EXP {tier.exp} +
+
+ ); + })} +
+
+ ); +} diff --git a/src/components/league-page/UserList.tsx b/src/components/league-page/UserList.tsx new file mode 100644 index 0000000..51d540b --- /dev/null +++ b/src/components/league-page/UserList.tsx @@ -0,0 +1,58 @@ +import Profile2 from '@/assets/images/profile2.svg?react'; +import LevelProgressCircle from '../@common/level-info/LevelProgressCircle'; +import { PROFILE_COLORS } from '../../constants/profile-colors'; + +interface User { + userId: number; + nickname: string; + level: number; + lp: number; + rank: number; + profileImgNumber: number; +} + +interface UserListProps { + users: User[]; + loading: boolean; +} + +export default function UserList({ users, loading }: UserListProps) { + return ( +
+ {users.map((user) => { + const profileBgColor = + PROFILE_COLORS[user.profileImgNumber as keyof typeof PROFILE_COLORS]; + + return ( +
+
+ + {String(user.rank).padStart(3, '0')} + +
+ + + +
+ {user.nickname} +
+
+
+ LV + {user.level} +
+
+ LP + {user.lp} +
+
+
+ ); + })} + {loading &&
Loading...
} +
+ ); +} diff --git a/src/constants/api.ts b/src/constants/api.ts index 28a78dd..ed6b889 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -24,4 +24,9 @@ export const API_ENDPOINTS = { list: `${API_BASE_URL}/friends/list`, add: `${API_BASE_URL}/friends/add`, }, + leagues: { + base: `${API_BASE_URL}/rank`, + user: (num: number) => `${API_BASE_URL}/rank/user-leagues/page/${num}`, + tier: (id: number, num: number) => `${API_BASE_URL}/rank/leagues/${id}/page/${num}`, + } } as const; diff --git a/src/constants/tiers.ts b/src/constants/tiers.ts new file mode 100644 index 0000000..df2b224 --- /dev/null +++ b/src/constants/tiers.ts @@ -0,0 +1,8 @@ +import TierCircle from '@/assets/icons/tier-circle.svg?react'; + +export const tiers = [ + { id: 1, name: '브론즈', icon: TierCircle, exp: 10 }, + { id: 2, name: '실버', icon: TierCircle, exp: 20 }, + { id: 3, name: '골드', icon: TierCircle, exp: 30 }, + { id: 4, name: '마스터', icon: TierCircle, exp: 40 }, +]; \ No newline at end of file diff --git a/src/pages/LeaguePage.tsx b/src/pages/LeaguePage.tsx index 301952d..11c38c3 100644 --- a/src/pages/LeaguePage.tsx +++ b/src/pages/LeaguePage.tsx @@ -1,147 +1,145 @@ -import { useRef, useState } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { useQuery } from '@tanstack/react-query'; import Banner from '../components/@common/banner/Banner'; -import TierCircle from '@/assets/icons/tier-circle.svg?react'; -import Profile from '@/assets/images/profile.svg'; -import ProfileSub from '@/assets/icons/profile-sub.svg?react'; import UserStats from '../components/@common/level-info/UserStats'; +import TierSelector from '../components/league-page/TierSelector'; +import UserList from '../components/league-page/UserList'; +import fetchMainInfo from '../api/fetchMainInfo'; +import { getUserLeagues } from '../api/getUserLeagues'; +import { getTierLeagues } from '../api/getTierLeagues'; +import { tiers } from '../constants/tiers'; -type Tier = { - id: number; - name: string; - icon: React.FC>; - exp: number; +type User = { + userId: number; + nickname: string; + level: number; + lp: number; + profileImgNumber: number; + rank: number; }; -const tiers: Tier[] = [ - { id: 1, name: '브론즈', icon: TierCircle, exp: 10 }, - { id: 2, name: '실버', icon: TierCircle, exp: 20 }, - { id: 3, name: '골드', icon: TierCircle, exp: 30 }, - { id: 4, name: '마스터', icon: TierCircle, exp: 40 }, -]; +function LeaguePage() { + const [selectedTierId, setSelectedTierId] = useState(null); + const [page, setPage] = useState(0); + const [users, setUsers] = useState([]); + const [hasMore, setHasMore] = useState(true); + const scrollContainerRef = useRef(null); + const fetchingRef = useRef(false); -type User = { - id: number; - nickname: string; - level: number; - lp: number; - profileIcon: string; -}; + const { data: mainInfo } = useQuery({ + queryKey: ['main-info'], + queryFn: fetchMainInfo, + select: (data) => ({ + nickname: data.nickname, + league: data.league, + level: data.level, + xp: data.xp, + }), + }); -const users: User[] = [ - { id: 1, nickname: '사용자 1', level: 10, lp: 7897, profileIcon: Profile }, - { id: 2, nickname: '사용자 2', level: 12, lp: 7897, profileIcon: Profile }, - { id: 3, nickname: '사용자 3', level: 8, lp: 7897, profileIcon: Profile }, - { id: 4, nickname: '사용자 4', level: 15, lp: 7897, profileIcon: Profile }, - { id: 5, nickname: '사용자 5', level: 200, lp: 7897, profileIcon: Profile }, - { id: 6, nickname: '사용자 6', level: 8, lp: 7897, profileIcon: Profile }, -]; + const getTierIdFromLeague = (league?: string | null) => { + if (!league) return 1; + const tier = tiers.find((t) => league.includes(t.name)); + return tier?.id ?? 1; + }; -function LeaguePage() { - const [selectedTierId, setSelectedTierId] = useState(1); - const itemRefs = useRef<(HTMLDivElement | null)[]>([]); - - const handleClick = (index: number, id: number) => { - setSelectedTierId(id); - itemRefs.current[index]?.scrollIntoView({ - behavior: 'smooth', - inline: 'center', - block: 'nearest', - }); + useEffect(() => { + if (mainInfo && selectedTierId === null) { + setSelectedTierId(getTierIdFromLeague(mainInfo.league)); + } + }, [mainInfo, selectedTierId]); + + + const fetchUsers = useCallback( + async (targetPage: number, targetHasMore: boolean) => { + if (!selectedTierId || fetchingRef.current || !targetHasMore) return; + + fetchingRef.current = true; + try { + let newUsers: User[] = []; + if (selectedTierId === getTierIdFromLeague(mainInfo?.league)) { + newUsers = await getUserLeagues(targetPage); + } else { + newUsers = await getTierLeagues(selectedTierId, targetPage); + } + + if (newUsers.length > 0) { + setUsers((prev) => { + const existingIds = new Set(prev.map((u) => u.userId)); + return [...prev, ...newUsers.filter((u) => !existingIds.has(u.userId))]; + }); + setPage(targetPage + 1); + } + + if (newUsers.length < 10) setHasMore(false); + } catch (error) { + console.error('유저 불러오기 실패:', error); + } finally { + fetchingRef.current = false; + } + }, + [selectedTierId, mainInfo] + ); + + useEffect(() => { + if (selectedTierId !== null) { + setUsers([]); + setPage(0); + setHasMore(true); + fetchUsers(0, true); + } + }, [selectedTierId, fetchUsers]); + + useEffect(() => { + const container = scrollContainerRef.current; + if (!container) return; + + const handleScroll = () => { + if (fetchingRef.current || !hasMore) return; + if (container.scrollTop + container.clientHeight >= container.scrollHeight - 50) { + fetchUsers(page, hasMore); + } }; - return ( -
- -
-
-
-

- 현재 땅콩님의 티어는 브론즈입니다! -

- -
- -
-
- - {tiers.map((tier, index) => { - const isSelected = tier.id === selectedTierId; - const TierIcon = tier.icon; - - return ( -
{ - itemRefs.current[index] = el; - }} - onClick={() => handleClick(index, tier.id)} - className={`flex flex-col items-center cursor-pointer transition-all duration-300 pl-auto justify-center ${ - isSelected ? 'scale-110 px-4' : '' - } min-h-[140px]`} - > -
- -
- -
- {tier.name} - EXP {tier.exp} -
-
- ); - })} -
-
-
- -
-
- {[...users] - .sort((a, b) => b.level - a.level) - .map((user, index) => { - const rank = String(index + 1).padStart(3, '0'); - - return ( -
-
- {rank} -
- - -
- {user.nickname} -
-
-
- LV - {user.level} -
-
- LP - {user.lp} -
-
-
- ); - })} -
-
-
-
- ); + container.addEventListener('scroll', handleScroll); + return () => container.removeEventListener('scroll', handleScroll); + }, [page, hasMore, fetchUsers]); + + return ( +
+ +
+
+
+

+ 현재 {mainInfo?.nickname}님의 티어는{' '} + {mainInfo?.league || '브론즈'} 입니다! +

+ +
+ + setSelectedTierId(id)} + /> +
+ +
+ + {!hasMore &&

더 이상 유저가 없습니다.

} +
+
+
+ ); } export default LeaguePage; diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 13ffae8..060fa5a 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -20,7 +20,7 @@ export default function LoginPage() { throw new Error('로그인 URL을 받아오지 못했습니다.'); } - localStorage.setItem('returnTo', window.location.pathname); + sessionStorage.setItem('returnTo', window.location.pathname); window.location.href = data.loginUrl; diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index d6f56ad..6e4faf7 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -1,5 +1,4 @@ import Banner from '../components/@common/banner/Banner'; - import { Link } from 'react-router-dom'; import Rocket from '@/assets/images/rocket.png'; import Fire from '@/assets/images/fire.png'; diff --git a/src/pages/UserPage.tsx b/src/pages/UserPage.tsx index c170b5f..5cdcce1 100644 --- a/src/pages/UserPage.tsx +++ b/src/pages/UserPage.tsx @@ -5,7 +5,7 @@ import People from '@/assets/icons/people.svg?react'; import RightArrow from '@/assets/icons/right-arrow.svg?react'; import GoldBadge from '@/assets/icons/gold-badge.svg?react'; import CopperBadge from '@/assets/icons/copper-badge.svg?react'; -import { GetMypage } from '../api/getMypage'; +import { getMypage } from '../api/getMypage'; import { PROFILE_COLORS } from '../constants/profile-colors'; type UserInfo = { @@ -35,7 +35,7 @@ export default function UserPage() { useEffect(() => { async function fetchUser() { try { - const res = await GetMypage(); + const res = await getMypage(); setUserinfo({ nickname: res.nickname, profileImgNumber: res.profileImgNumber, diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..8981364 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,14 @@ +export default { + content: [ + './index.html', + './src/**/*.{js,ts,jsx,tsx}', + ], + theme: { + extend: { + fontFamily: { + mbc: ['MBC1961GulimM', 'sans-serif'], + }, + }, + }, + plugins: [], +}