diff --git a/frontend/lecture/package.json b/frontend/lecture/package.json
index 4f31e4f..49e5bba 100644
--- a/frontend/lecture/package.json
+++ b/frontend/lecture/package.json
@@ -9,8 +9,7 @@
"check-types": "tsc",
"lint": "eslint .",
"format": "prettier . --check",
- "preview": "vite preview",
- "check-all": "yarn check-types && yarn lint && yarn format"
+ "check-all": "yarn check-types && yarn lint && yarn format && yarn knip"
},
"dependencies": {
"@radix-ui/react-icons": "1.3.0",
@@ -19,7 +18,6 @@
"@radix-ui/react-tabs": "1.1.0",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
- "embla-carousel-react": "8.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-markdown": "9.0.1",
diff --git a/frontend/lecture/src/components/Slides/index.tsx b/frontend/lecture/src/components/Slides/index.tsx
index 507ced2..2f14a0f 100644
--- a/frontend/lecture/src/components/Slides/index.tsx
+++ b/frontend/lecture/src/components/Slides/index.tsx
@@ -1,19 +1,18 @@
-import { ReactNode } from 'react';
+import { ReactNode, useCallback, useEffect } from 'react';
+import { useSearchParams } from 'react-router-dom';
import {
Card,
CardContent,
- CardFooter,
CardHeader,
CardTitle,
} from '@/designsystem/ui/card';
import {
- Carousel,
- CarouselContent,
- CarouselItem,
- CarouselNext,
- CarouselPrevious,
-} from '@/designsystem/ui/carousel';
+ Pagination,
+ PaginationContent,
+ PaginationItem,
+ PaginationLink,
+} from '@/designsystem/ui/pagination';
import { Separator } from '@/designsystem/ui/separator';
export const Slides = ({
@@ -21,33 +20,98 @@ export const Slides = ({
}: {
slides: { title: string; content: ReactNode }[];
}) => {
+ const { page: page, onChangePage: onChangePage } = usePage({
+ minPage: 1,
+ maxPage: slides.length,
+ });
+
+ const slide = slides[page - 1];
+
+ useEffect(() => {
+ const keydownHandler = (event: KeyboardEvent) => {
+ if (event.key === 'ArrowRight') onChangePage(page + 1);
+ else if (event.key === 'ArrowLeft') onChangePage(page - 1);
+ };
+
+ window.addEventListener('keydown', keydownHandler);
+ return () => {
+ window.removeEventListener('keydown', keydownHandler);
+ };
+ }, [page, onChangePage]);
+
+ if (slide === undefined) return null;
+
return (
-
-
- {slides.map((slide, index) => (
-
-
-
- {slide.title}
-
-
-
-
-
- {slide.content}
-
-
-
-
-
- {index + 1}/{slides.length}
-
-
-
- ))}
-
-
-
-
+
+
+
+ {slide.title}
+
+
+
+
+
+ {slide.content}
+
+
+
+
+ {Array.from({ length: slides.length }, (_, i) => {
+ const itemPage = i + 1;
+ return (
+ {
+ onChangePage(itemPage);
+ }}
+ >
+
+ {itemPage}
+
+
+ );
+ })}
+
+
+
);
};
+
+const usePage = ({
+ maxPage: maxPage,
+ minPage: minPage,
+}: {
+ maxPage: number;
+ minPage: number;
+}) => {
+ const key = 'page';
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const pageParams = searchParams.get(key);
+
+ const page = (() => {
+ const calculatedPage =
+ pageParams !== null && `${parseInt(pageParams)}` === pageParams
+ ? parseInt(pageParams)
+ : 1;
+
+ if (calculatedPage < minPage) return minPage;
+ if (calculatedPage > maxPage) return maxPage;
+ return calculatedPage;
+ })();
+
+ const onChangePage = useCallback(
+ (newPage: number) => {
+ if (newPage < 1 || newPage > maxPage) return;
+ const newSearchParams = new URLSearchParams(searchParams);
+ newSearchParams.set(key, newPage.toString());
+ setSearchParams(newSearchParams, { replace: true });
+ },
+ [searchParams, setSearchParams, maxPage],
+ );
+
+ return { page, onChangePage };
+};
diff --git a/frontend/lecture/src/designsystem/ui/carousel.tsx b/frontend/lecture/src/designsystem/ui/carousel.tsx
deleted file mode 100644
index fb83063..0000000
--- a/frontend/lecture/src/designsystem/ui/carousel.tsx
+++ /dev/null
@@ -1,259 +0,0 @@
-import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
-import useEmblaCarousel, {
- type UseEmblaCarouselType,
-} from 'embla-carousel-react';
-import * as React from 'react';
-
-import { Button } from '@/designsystem/ui/button';
-import { cn } from '@/utils/designsystem';
-
-type CarouselApi = UseEmblaCarouselType[1];
-type UseCarouselParameters = Parameters;
-type CarouselOptions = UseCarouselParameters[0];
-type CarouselPlugin = UseCarouselParameters[1];
-
-type CarouselProps = {
- opts?: CarouselOptions;
- plugins?: CarouselPlugin;
- orientation?: 'horizontal' | 'vertical';
- setApi?: (api: CarouselApi) => void;
-};
-
-type CarouselContextProps = {
- carouselRef: ReturnType[0];
- api: ReturnType[1];
- scrollPrev: () => void;
- scrollNext: () => void;
- canScrollPrev: boolean;
- canScrollNext: boolean;
-} & CarouselProps;
-
-const CarouselContext = React.createContext(null);
-
-function useCarousel() {
- const context = React.useContext(CarouselContext);
-
- if (context === null) {
- throw new Error('useCarousel must be used within a ');
- }
-
- return context;
-}
-
-const Carousel = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes & CarouselProps
->(
- (
- {
- orientation = 'horizontal',
- opts,
- setApi,
- plugins,
- className,
- children,
- ...props
- },
- ref,
- ) => {
- const [carouselRef, api] = useEmblaCarousel(
- {
- ...opts,
- axis: orientation === 'horizontal' ? 'x' : 'y',
- },
- plugins,
- );
- const [canScrollPrev, setCanScrollPrev] = React.useState(false);
- const [canScrollNext, setCanScrollNext] = React.useState(false);
-
- const onSelect = React.useCallback((carouselApi: CarouselApi) => {
- if (carouselApi === undefined) {
- return;
- }
-
- setCanScrollPrev(carouselApi.canScrollPrev());
- setCanScrollNext(carouselApi.canScrollNext());
- }, []);
-
- const scrollPrev = React.useCallback(() => {
- api?.scrollPrev();
- }, [api]);
-
- const scrollNext = React.useCallback(() => {
- api?.scrollNext();
- }, [api]);
-
- const handleKeyDown = React.useCallback(
- (event: React.KeyboardEvent) => {
- if (event.key === 'ArrowLeft') {
- event.preventDefault();
- scrollPrev();
- } else if (event.key === 'ArrowRight') {
- event.preventDefault();
- scrollNext();
- }
- },
- [scrollPrev, scrollNext],
- );
-
- React.useEffect(() => {
- if (api === undefined || setApi === undefined) {
- return;
- }
-
- setApi(api);
- }, [api, setApi]);
-
- React.useEffect(() => {
- if (api === undefined) {
- return;
- }
-
- onSelect(api);
- api.on('reInit', onSelect);
- api.on('select', onSelect);
-
- return () => {
- api.off('select', onSelect);
- };
- }, [api, onSelect]);
-
- return (
-
-
- {children}
-
-
- );
- },
-);
-Carousel.displayName = 'Carousel';
-
-const CarouselContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => {
- const { carouselRef, orientation } = useCarousel();
-
- return (
-
- );
-});
-CarouselContent.displayName = 'CarouselContent';
-
-const CarouselItem = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => {
- const { orientation } = useCarousel();
-
- return (
-
- );
-});
-CarouselItem.displayName = 'CarouselItem';
-
-const CarouselPrevious = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps
->(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
- const { orientation, scrollPrev, canScrollPrev } = useCarousel();
-
- return (
-
- );
-});
-CarouselPrevious.displayName = 'CarouselPrevious';
-
-const CarouselNext = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps
->(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
- const { orientation, scrollNext, canScrollNext } = useCarousel();
-
- return (
-
- );
-});
-CarouselNext.displayName = 'CarouselNext';
-
-export {
- type CarouselApi,
- Carousel,
- CarouselContent,
- CarouselItem,
- CarouselPrevious,
- CarouselNext,
-};
diff --git a/frontend/lecture/src/designsystem/ui/pagination.tsx b/frontend/lecture/src/designsystem/ui/pagination.tsx
new file mode 100644
index 0000000..99a6703
--- /dev/null
+++ b/frontend/lecture/src/designsystem/ui/pagination.tsx
@@ -0,0 +1,121 @@
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ DotsHorizontalIcon,
+} from '@radix-ui/react-icons';
+import * as React from 'react';
+
+import { ButtonProps, buttonVariants } from '@/designsystem/ui/button';
+import { cn } from '@/utils/designsystem';
+
+const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
+
+);
+Pagination.displayName = 'Pagination';
+
+const PaginationContent = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<'ul'>
+>(({ className, ...props }, ref) => (
+
+));
+PaginationContent.displayName = 'PaginationContent';
+
+const PaginationItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<'li'>
+>(({ className, ...props }, ref) => (
+
+));
+PaginationItem.displayName = 'PaginationItem';
+
+type PaginationLinkProps = {
+ isActive?: boolean;
+} & Pick &
+ React.ComponentProps<'button'>;
+
+const PaginationLink = ({
+ className,
+ isActive,
+ size = 'icon',
+ ...props
+}: PaginationLinkProps) => (
+
+);
+PaginationLink.displayName = 'PaginationLink';
+
+const PaginationPrevious = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+
+ Previous
+
+);
+PaginationPrevious.displayName = 'PaginationPrevious';
+
+const PaginationNext = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+ Next
+
+
+);
+PaginationNext.displayName = 'PaginationNext';
+
+const PaginationEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) => (
+
+
+ More pages
+
+);
+PaginationEllipsis.displayName = 'PaginationEllipsis';
+
+export {
+ Pagination,
+ PaginationContent,
+ PaginationLink,
+ PaginationItem,
+ PaginationPrevious,
+ PaginationNext,
+ PaginationEllipsis,
+};
diff --git a/frontend/lecture/src/lectures/OT/index.tsx b/frontend/lecture/src/lectures/OT/index.tsx
index 8245277..3441701 100644
--- a/frontend/lecture/src/lectures/OT/index.tsx
+++ b/frontend/lecture/src/lectures/OT/index.tsx
@@ -366,6 +366,10 @@ export const otLecture = getLectureItem({
세미나 목표 중 하나인 친목에 큰 도움이 될 것
같다
+
+ 저도 이렇게 해 보는 게 처음이라 잘 될지 모르겠어요 아니다
+ 싶으면 바로 접겠습니다
+
형태
@@ -373,11 +377,16 @@ export const otLecture = getLectureItem({
서로 도움을 주고받는다
상대가 혹시나 트롤이어도 발목잡히지는 않는 구조
- 조마다 세미나장 포함한 DM방을 생성해 드립니다
+ 조마다 세미나장 포함한 비공개 채널을 생성해 드립니다
각자 얼마나 기여하시는지 봐야 하고, 질문을 받아 드려야 해서
저는 항상 채팅방에 포함되어야 합니다
+
+ 당연히 저 빼고 DM하셔도 되고 그런 건 상관 없는데, 그렇게
+ 되면 질문받아드리거나 얼마나 기여하시는지 체크하는 건 어렵단
+ 점 참고 부탁드립니다
+
친목이나 과제 삽질 측면에서, 가능하면 비슷한 사람들끼리 배정
diff --git a/frontend/lecture/yarn.lock b/frontend/lecture/yarn.lock
index de98359..af33c02 100644
--- a/frontend/lecture/yarn.lock
+++ b/frontend/lecture/yarn.lock
@@ -2403,34 +2403,6 @@ __metadata:
languageName: node
linkType: hard
-"embla-carousel-react@npm:8.2.0":
- version: 8.2.0
- resolution: "embla-carousel-react@npm:8.2.0"
- dependencies:
- embla-carousel: "npm:8.2.0"
- embla-carousel-reactive-utils: "npm:8.2.0"
- peerDependencies:
- react: ^16.8.0 || ^17.0.1 || ^18.0.0
- checksum: 10/388a11e2ee7a7a09ac3a4001d3dd9d2c77b15bf9028e9fc90b89e96bfc08fb7a4a3df54e7ddf3a1f8e3d844a2d095e49fd73eb89c38bc90c7e0b10671e3582e2
- languageName: node
- linkType: hard
-
-"embla-carousel-reactive-utils@npm:8.2.0":
- version: 8.2.0
- resolution: "embla-carousel-reactive-utils@npm:8.2.0"
- peerDependencies:
- embla-carousel: 8.2.0
- checksum: 10/34a189543e476582dd5af1cf3d3bdd8bfea3faf47a90f04a642c4d65eefd699377cf65380619b7b98365a02a0eba5e75a2b716ef09c6fea8a703521a8ca47eab
- languageName: node
- linkType: hard
-
-"embla-carousel@npm:8.2.0":
- version: 8.2.0
- resolution: "embla-carousel@npm:8.2.0"
- checksum: 10/18cd9d06573c27f14523ec0d33cbbcc091492aff42f69b3f53265c5ff3d2fd3da89ad7ea9ae54d398d440723f6c764d0d5936c35a1975f2410143ca6f55064ce
- languageName: node
- linkType: hard
-
"emoji-regex@npm:^8.0.0":
version: 8.0.0
resolution: "emoji-regex@npm:8.0.0"
@@ -4097,7 +4069,6 @@ __metadata:
autoprefixer: "npm:10.4.20"
class-variance-authority: "npm:0.7.0"
clsx: "npm:2.1.1"
- embla-carousel-react: "npm:8.2.0"
eslint: "npm:9.9.0"
knip: "npm:5.27.3"
postcss: "npm:8.4.41"