Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

➕ [WV-25] feat : 스케줄 페이지 구현 #34

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a10963d
✏️ WV-25 rename : 케밥케이스로 수정정
Jinviz Dec 27, 2024
c271e08
Merge branch 'dev' of github.com:KTwizviz/wizviz into WV-25-game-sche…
Jinviz Dec 27, 2024
6e75ffa
➕ WV-25 feat : 캘린더 컴포넌트 구현
Jinviz Dec 27, 2024
fe6013c
💄 WV25 design : 오늘 날짜의 셀 하이라이트
Jinviz Dec 27, 2024
6b317e6
➕ WV-25 feat : 월 스케줄 API 호출출
Jinviz Dec 27, 2024
c4c73ef
Merge branch 'dev' of https://github.com/KTwizviz/wizviz into WV-25-c…
Jinviz Dec 30, 2024
adfa49c
💄 WV-25 style : 캘린더 셀 색상 변경경
Jinviz Dec 30, 2024
0747d44
➕ WV-25 feat : 월 스케줄 API 데이터 패칭
Jinviz Dec 30, 2024
af41684
📌 WV-25 setting : next/imge 외부 도메인 이미지 허용 설정
Jinviz Dec 30, 2024
9f84094
➕ WV-25 feat : 스케줄이 있는 날 배경 적용용
Jinviz Dec 30, 2024
9ad1166
➕ WV-25 feat : 스케줄 중 홈구장일 때 마크 추가
Jinviz Dec 30, 2024
590d5cb
➕ WV-25 feat : 캘린더 셀 승패 표시 및 홈그라운드는 텍스트 강조
Jinviz Dec 31, 2024
43eb8cf
✏️ WV-25 rename : 사용하지 않는 임포트 제거
Jinviz Dec 31, 2024
41ea0c2
♻ WV-25 refactor : 날짜 객체 부모 컴포넌트 내에서 관리
Jinviz Jan 2, 2025
09ee90e
➕ WV-25 feat : 일정 Carousel 컴포넌트 생성 (미구현)
Jinviz Jan 2, 2025
9b9fa29
♻️ WV-25 refactor : Page에서 상태 관리 및 분리
Jinviz Jan 3, 2025
83c322f
✏️ WV-25 rename : 컴포넌트 이름 컨벤션 적용
Jinviz Jan 3, 2025
ef39a54
Merge branch 'dev' into WV-25-create-game-schedule-page
Jinviz Jan 3, 2025
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
70 changes: 68 additions & 2 deletions app/(menu)/game/[league]/schedule/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,69 @@
export default function page() {
return <div>page입니다</div>;
'use client'

import ScheduleCalendar from "@/components/game/schedule/schedule-calendar";
import ScheduleCarousel from "@/components/game/schedule/schedule-carousel";
import { useEffect, useState } from "react";

export default function Schedule() {
const date = new Date()
const year = date.getFullYear() //년
const month = date.getMonth() + 1 //월
const today = date.getDate() //일

const [currentDate, setCurrentDate] = useState<CalendarDate>({ year: year, month: month, today: today }) // 현재 날짜 (년,월)
const [schedules, setSchedules] = useState<GameSchedule[]>([]) // 스케줄 API 데이터

const stringDate = `${currentDate.year}${String(currentDate.month).padStart(2, '0')}` // 현재 선택된 날짜 스트링(API Params) e.g. '202409'

useEffect(() => {
getMonthSchedules(stringDate);
}, [stringDate])

const getMonthSchedules = async (params: string) => {
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_KEY}/game/monthschedule?yearMonth=${params}`,
{
next: { revalidate: 3600 }, // 1시간마다 캐싱
}
);

if (!res.ok) {
throw new Error('Network response was not ok');
}

const data = await res.json();
setSchedules(data.data.list);

} catch (error) {
console.error('API 요청 에러:', error);
}
}

const handleCurrentMonth = (month: string) => {
if (month === 'next') {
setCurrentDate(prev => {
if (prev.month === 12) {
return { year: prev.year + 1, month: 1 }
}
return { ...prev, month: prev.month + 1 }
})
}

if (month === 'prev') {
setCurrentDate(prev => {
if (prev.month === 1) {
return { year: prev.year - 1, month: 12 }
}
return { ...prev, month: prev.month - 1 }
})
}
}

return (
<div>
<ScheduleCarousel date={currentDate} />
<ScheduleCalendar date={currentDate} schedules={schedules} monthHandler={handleCurrentMonth} />
</div>
)
}
3 changes: 2 additions & 1 deletion app/(menu)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import BreadCrumb from "@/components/common/breadcrumb/Breadcrumb";
import BreadCrumb from "@/components/common/breadcrumb/breadcrumb"
import SubBannerMenu from "@/components/common/sub-menu-banner";
import CategoryMenu from "@/components/common/CategoryMenu";
import type { Metadata } from "next";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Breadcrumb, BreadcrumbList } from "@/components/ui";
import { usePathname } from "next/navigation";
import BreadcrumbSegment from "./BreadcrumbSegment";
import BreadcrumbSegment from "./breadcrumb-segment";

const BreadCrumb = () => {
const pathName: string = usePathname();
Expand Down
84 changes: 84 additions & 0 deletions components/common/sub-menu-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";

import submenuBg from "@/assets/images/submenu_bg.png";
import Image from "next/image";
import { SubMenuInfo } from "@/constants/header-menu";

interface SubMenu {
id: string;
title: string;
descript: string;
}

const SubBannerMenu = () => {
const router = useRouter();
const pathname: string = usePathname();
const currentPath: string = pathname.split("/").pop() || ""; // 현재 메뉴 이름
const [subMenuList, setSubMenuList] = useState<SubMenu[]>([]); // 해당 메뉴의 하위 메뉴 리스트
const [title, setTitle] = useState<string>("");
const [descript, setDescript] = useState<string>("");

// 현재 진입한 메뉴와 하위메뉴 정보들을 상태로 관리
useEffect(() => {
const findMenuInfo = () => {
for (const menu in SubMenuInfo) {
const subMenuAll = SubMenuInfo[
menu as keyof typeof SubMenuInfo
] as SubMenu[];
const matchedMenu = subMenuAll.find((item) => item.id === currentPath);

if (matchedMenu) {
setSubMenuList(subMenuAll);
setTitle(matchedMenu.title);
setDescript(matchedMenu.descript);
break;
}
}
};

findMenuInfo();
}, [currentPath]);

return (
<div className="w-full">
<div className="relative">
<div className="relative w-full h-[253px] flex items-center justify-center bg-cover bg-center bg-blend-overlay">
<Image
src={submenuBg}
alt="Submenu background"
fill
className="absolute inset-0 object-cover"
/>
<div className="text-center text-white p-8 max-w-3xl relative z-10">
<h1 className="text-4xl md:text-5xl font-bold mb-4">{title}</h1>
<p className="text-lg md:text-xl opacity-90">{descript}</p>
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 transform translate-y-1/2">
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg">
<div className="flex justify-around p-4">
{subMenuList.map((tab) => (
<button
key={tab.id}
onClick={() => router.push(tab.id)}
className={`flex flex-col items-center space-y-1 px-4 py-2 rounded-lg transition-all duration-300
${
currentPath === tab.id
? "bg-SYSTEM-white text-ELSE-CC6" // 적절한 색상 클래스 사용
: "text-ELSE-49 hover:bg-ELSE-DE"
}`}
>
<span className="text-sm font-medium">{tab.title}</span>
</button>
))}
</div>
</div>
</div>
</div>
</div>
);
};

export default SubBannerMenu;
101 changes: 101 additions & 0 deletions components/game/schedule/schedule-calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use client'

import { ChevronLeft, ChevronRight } from 'lucide-react'
import Image from 'next/image';

const ScheduleCalendar = ({ date, schedules, monthHandler }: ScheduleCalendarProps) => {
const daysInMonth = new Date(date.year, date.month, 0).getDate() // 현재 선택된 月의 마지막 날짜
const firstDayOfMonth = new Date(date.year, date.month - 1, 1).getDay() // 현재 선택된 月의 시작 요일

const days = Array.from({ length: daysInMonth }, (_, i) => i + 1)
const weekdays = ['일', '월', '화', '수', '목', '금', '토']

return (
<div className="w-full mx-auto mt-6">
<div className="flex items-center justify-between mb-4 px-4">
<button onClick={() => monthHandler('prev')} className="p-2">
<ChevronLeft className="w-8 h-8" />
</button>
<div className="flex text-xl items-center gap-2">
<span>{date.year}년</span>
<span>{date.month}월</span>
</div>
<button onClick={() => monthHandler('next')} className="p-2">
<ChevronRight className="w-8 h-8" />
</button>
</div>

<div className="grid grid-cols-7">
{weekdays.map((day) => (
<div key={day} className="p-4 text-center text-m font-medium">
{day}
</div>
))}

{Array(firstDayOfMonth).fill(null).map((_, index) => (
<div key={`empty - ${index}`} className="p-2" />
))}

{days.map((day) => {
const keyDate = `${date.year}${String(date.month).padStart(2, '0')}` + String(day).padStart(2, '0');
const todaySchedule = schedules.find((schedule) => schedule.displayDate === keyDate);

return (
<div
key={keyDate}
className={`p-2 min-h-[140px] border text-sm
${day === date.today && date.year === date.year && date.month === date.month
? 'border-SYSTEM-main'
: 'border-gray-100'}
${todaySchedule && 'bg-ELSE-FF5'}` // 스케줄이 있는 날 배경 색상 적용
}
>
<div className="flex justify-between items-center font-medium mb-1">
<span className="flex-1">
{day}
</span>
{todaySchedule?.outcome === '승' ?
<div className='flex items-center text-white bg-SYSTEM-main rounded-xl px-1.5'>승</div>
: todaySchedule?.outcome === '패' &&
<div className='flex items-center text-white bg-ELSE-D9 rounded-xl px-1.5'>패</div>
}
<div className="flex-1"></div>
</div>

{
todaySchedule && (
<div className="w-full flex flex-col justify-center items-center text-m">
<div className="flex items-center gap-1 mb-1">
<Image
src={todaySchedule.visitLogo}
alt={todaySchedule.visit}
width={48}
height={48}
/>
<span className='text-l'>vs</span>
<Image
src={todaySchedule.homeLogo}
alt={todaySchedule.home}
width={48}
height={48}
/>
</div>
<p>{todaySchedule.gtime}</p>
{todaySchedule.stadium === '수원' ?
<p className='text-SYSTEM-main font-bold'>
{todaySchedule.stadium}
</p>
: <p> {todaySchedule.stadium}</p>
}
</div>
)
}
</div>
)
})}
</div>
</div >
)
}

export default ScheduleCalendar;
67 changes: 67 additions & 0 deletions components/game/schedule/schedule-carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client'

import { useEffect, useState } from 'react'
import { Card, CardContent } from "@/components/ui/card"
import {
Carousel,
CarouselApi,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"

const ScheduleCarousel = ({ date }: { date: CalendarDate }) => {
const [api, setApi] = useState<CarouselApi>()
const [center, setCenter] = useState<number>(0)

useEffect(() => {
if (!api) {
return
}
setCenter(api.selectedScrollSnap() + 2)

const onSelect = () => {
setCenter(api.selectedScrollSnap() + 2)
}

// 이벤트 등록
api.on("select", onSelect)

// 언마운트 시 이벤트 제거 (cleanup)
return () => {
api.off("select", onSelect)
}
}, [api])

return (
<div className='flex justify-center pb-8 border-b'>
<Carousel
opts={{
align: "start",
}}
className="w-[80%]"
setApi={setApi}
>
<CarouselContent>
{Array.from({ length: 10 }).map((_, index) => (
<CarouselItem key={index} className="md:basis-1/2 lg:basis-1/3">
<div className="p-1">
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="text-3xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>

</div>
)
}

export default ScheduleCarousel
33 changes: 33 additions & 0 deletions components/game/schedule/type.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
type GameSchedule = {
broadcast: string
displayDate: string
gameDate: number
gmkey: string
gtime: string
home: string
homeKey: string
homeLogo: string
homeScore: number
matchTeamCode: string
matchTeamName: string
outcome: string
stadium: string
stadiumKey: string
status: string
visit: string
visitKey: string
visitLogo: string
visitScore: number
}

type CalendarDate = {
year: number
month: number
today?: number
}

type ScheduleCalendarProps = {
date: CalendarDate
schedules: GameSchedule[]
monthHandler: (increment: string) => void
}
Loading
Loading