diff --git a/package.json b/package.json index 1094675..71f8891 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@types/d3": "^7.4.0", "@types/node": "20.4.2", "@types/react": "18.2.15", + "@types/react-datepicker": "^4.11.2", "@types/react-dom": "18.2.7", "autoprefixer": "10.4.14", "cors": "^2.8.5", @@ -36,10 +37,12 @@ "next": "13.4.10", "postcss": "8.4.26", "react": "18.2.0", + "react-datepicker": "^4.16.0", "react-dnd": "^16.0.1", "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.45.2", + "react-intl": "^6.4.4", "react-redux": "^8.1.1", "tailwindcss": "3.3.3", "typescript": "5.1.6" diff --git a/public/calenda-toggle.svg b/public/calenda-toggle.svg new file mode 100644 index 0000000..b73fa0e --- /dev/null +++ b/public/calenda-toggle.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/right-arrow.svg b/public/images/right-arrow.svg new file mode 100644 index 0000000..6a6818a --- /dev/null +++ b/public/images/right-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/link.svg b/public/link.svg new file mode 100644 index 0000000..9e51cc6 --- /dev/null +++ b/public/link.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/next-mark.svg b/public/next-mark.svg new file mode 100644 index 0000000..3e2576b --- /dev/null +++ b/public/next-mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/note.svg b/public/note.svg new file mode 100644 index 0000000..88fe678 --- /dev/null +++ b/public/note.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/right-arrow.svg b/public/right-arrow.svg new file mode 100644 index 0000000..cfc824c --- /dev/null +++ b/public/right-arrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/video.svg b/public/video.svg new file mode 100644 index 0000000..e56ac42 --- /dev/null +++ b/public/video.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..54542c2 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,10 @@ +export const fetchClient = async ( + endpoint: string, + options?: RequestInit, +): Promise => { + const response = await fetch( + `${process.env.NEXT_PUBLIC_BASE_URL}${endpoint}`, + options, + ); + return response.json(); +}; diff --git a/src/api/lecture.ts b/src/api/lecture.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/app/classroom/[lectureId]/page.tsx b/src/app/classroom/[lectureId]/page.tsx index 0d7a8ae..3959cc3 100644 --- a/src/app/classroom/[lectureId]/page.tsx +++ b/src/app/classroom/[lectureId]/page.tsx @@ -24,7 +24,10 @@ const LectureHome: FC = ({ lectureId }) => {
- +
diff --git a/src/app/classroom/message.tsx b/src/app/classroom/message.tsx new file mode 100644 index 0000000..24f6b3a --- /dev/null +++ b/src/app/classroom/message.tsx @@ -0,0 +1,47 @@ +import { createIntl, createIntlCache, IntlShape } from "react-intl"; + +// 언어별 메시지 정의 +interface Messages { + [key: string]: { + calendar: { + monthNames: string; + dayNames: string; + }; + // 다른 언어 메시지 정의도 추가할 수 있습니다. + }; +} + +const messages: Messages = { + en: { + calendar: { + monthNames: `January, February, March, April, May, June, July, August, September, October, November, December`, + dayNames: `Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday`, + }, + // 영어 메시지들을 정의합니다. + }, + ko: { + calendar: { + monthNames: + "1월, 2월, 3월, 4월, 5월, 6월, 7월, 8월, 9월, 10월, 11월, 12월", + dayNames: "일, 월, 화, 수, 목, 금, 토", + }, + // 한국어 메시지들을 정의합니다. + }, + // 다른 언어의 메시지들도 정의합니다. +}; + +// 언어 설정과 캐시 생성 +const cache = createIntlCache(); +const defaultLocale = "ko"; +const defaultMessages = messages[defaultLocale]; +const defaultIntl: IntlShape = createIntl( + { + locale: defaultLocale, + messages: { + ...defaultMessages.calendar, + }, + }, + cache, +); + +export { defaultIntl, messages }; diff --git a/src/app/classroom/page.tsx b/src/app/classroom/page.tsx index 61c378a..1aaed06 100644 --- a/src/app/classroom/page.tsx +++ b/src/app/classroom/page.tsx @@ -1,18 +1,17 @@ -'use client' -import Aside from "@/components/classroom/Aside" -import ClassContent from "@/components/classroom/ClassContent" +"use client"; + +import Aside from "@/components/classroom/Aside"; +import ClassContent from "@/components/classroom/ClassContent"; const Classroom = () => { - return (
-
- ) -} - -export default Classroom + ); +}; +export default Classroom; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5e0e092..a95ca64 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -19,6 +19,7 @@ export default function RootLayout({ {children} + ); diff --git a/src/components/classroom/Aside.tsx b/src/components/classroom/Aside.tsx index 2ca3799..1aee180 100644 --- a/src/components/classroom/Aside.tsx +++ b/src/components/classroom/Aside.tsx @@ -1,19 +1,22 @@ -import useRenderAsideButton from "@/hooks/useRenderAsideButton" -import { useState } from "react" +import useRenderAsideButton from "@/hooks/useRenderAsideButton"; +import { useState } from "react"; const Aside = () => { - const {renderAsideButton, contentCardList} = useRenderAsideButton() + const { renderAsideButton, contentCardList } = useRenderAsideButton(); return ( - ) -} + ); +}; -export default Aside \ No newline at end of file +export default Aside; diff --git a/src/components/classroom/ClassContent.tsx b/src/components/classroom/ClassContent.tsx index 09af6b8..1e6ae9d 100644 --- a/src/components/classroom/ClassContent.tsx +++ b/src/components/classroom/ClassContent.tsx @@ -1,21 +1,51 @@ -import ContentCard from "./ContentCard" -import { MOCK_DATA } from "./MOCK_DATA" -import { IContent } from "./MOCK_DATA" +import ContentCard from "./ContentCard"; +import { MOCK_DATA } from "./MOCK_DATA"; +import { IContent } from "./MOCK_DATA"; +import MakeModal from "../classroomModal/MakeLectureModal"; +import LinkModal from "../classroomModal/AddLinkModal"; +import DurationModal from "../classroomModal/LectureDurationModal"; +import useClassroomModal from "@/hooks/useClassroomModal"; +import { useDispatch } from "react-redux"; +import { setModalVisibility } from "@/redux/slice/classroomModalSlice"; const ClassContent = () => { + const dispatch = useDispatch(); + const { + lectureTypeModalOpen, + noteModalOpen, + linkModalOpen, + videoFileModalOpen, + lectureDurationModalOpen, + commentModalOpen, + replyCommentModalOpen, + } = useClassroomModal(); + + const handleModalOpen = () => { + dispatch( + setModalVisibility({ modalName: "lectureTypeModalOpen", visible: true }), + ); + }; return (
-
-
-
[DAY1] IT기본
-
강의 1개
-
- +
+
+
[DAY1] IT기본
+
강의 1개
- {MOCK_DATA.map((e:IContent) => ( - - ))} + +
+ {MOCK_DATA.map((e: IContent) => ( + + ))} + {lectureTypeModalOpen && } + {linkModalOpen && } + {lectureDurationModalOpen && }
- ) -} + ); +}; -export default ClassContent \ No newline at end of file +export default ClassContent; diff --git a/src/components/classroom/ContentCard.tsx b/src/components/classroom/ContentCard.tsx index b49b6b7..d87bbcc 100644 --- a/src/components/classroom/ContentCard.tsx +++ b/src/components/classroom/ContentCard.tsx @@ -1,23 +1,29 @@ -import { IContent } from "./MOCK_DATA" +import { IContent } from "./MOCK_DATA"; -const ContentCard = ({props}:{props:IContent}) => { +const ContentCard = ({ props }: { props: IContent }) => { return (
-
img section
+
+ img section +
수정|삭제
-
{props.RUN_TIME}
+
+ {props.RUN_TIME} +
{props.TITLE}
[수강기간]
{props.CLASS_DATE}
- +
- ) -} + ); +}; -export default ContentCard \ No newline at end of file +export default ContentCard; diff --git a/src/components/classroom/MOCK_DATA.ts b/src/components/classroom/MOCK_DATA.ts index 87b8297..9fa9a68 100644 --- a/src/components/classroom/MOCK_DATA.ts +++ b/src/components/classroom/MOCK_DATA.ts @@ -1,25 +1,25 @@ // 임시로 component 폴더에 놨습니다. export interface IContent { - RUN_TIME : string; - TITLE : string; - CLASS_DATE : string + RUN_TIME: string; + TITLE: string; + CLASS_DATE: string; } -export const MOCK_DATA:IContent[] = [ +export const MOCK_DATA: IContent[] = [ { - RUN_TIME : '7', - TITLE : '[DAY1] 프론트엔드와 백엔드', - CLASS_DATE : '2023.03.03~2023.12.12' + RUN_TIME: "7", + TITLE: "[DAY1] 프론트엔드와 백엔드", + CLASS_DATE: "2023.03.03~2023.12.12", }, { - RUN_TIME : '10', - TITLE : '[DAY2] 프론트엔드 라이브러이', - CLASS_DATE : '2023.04.06~2023.12.25' + RUN_TIME: "10", + TITLE: "[DAY2] 프론트엔드 라이브러이", + CLASS_DATE: "2023.04.06~2023.12.25", }, { - RUN_TIME : '15', - TITLE : '[DAY3] 백엔드 라이브러이', - CLASS_DATE : '2023.05.04~2023.12.27' - } -] \ No newline at end of file + RUN_TIME: "15", + TITLE: "[DAY3] 백엔드 라이브러이", + CLASS_DATE: "2023.05.04~2023.12.27", + }, +]; diff --git a/src/components/classroomModal/AddLinkModal.tsx b/src/components/classroomModal/AddLinkModal.tsx new file mode 100644 index 0000000..5132b63 --- /dev/null +++ b/src/components/classroomModal/AddLinkModal.tsx @@ -0,0 +1,68 @@ +import { useSelector, useDispatch } from "react-redux"; +import { setInputContent } from "@/redux/contentSlice"; +import { setInputTitle } from "@/redux/titleSlice"; +import Layout from "./common/Layout"; +//import { ModalSubmitButton } from "./common/ModalSubmitButton"; +import useClassroomModal from "@/hooks/useClassroomModal"; + +const LinkModal: React.FC = ({}) => { + const inputTitle = useSelector((state: any) => state.title.inputTitle); + const inputContent = useSelector((state: any) => state.content.inputContent); + const dispatch = useDispatch(); + + const { handleModalMove } = useClassroomModal(); + + const handleInputTitle = (e: React.ChangeEvent) => { + dispatch(setInputTitle(e.target.value)); + }; + const handleInputContent = (e: React.ChangeEvent) => { + const value = e.target.value; + dispatch( + setInputContent(value.startsWith("http://") ? value : "http://" + value), + ); + }; + return ( + +
+ + + 링크 만들기 + +
+ +
+ + + {/* */} +
+
+ ); +}; + +export default LinkModal; diff --git a/src/components/classroomModal/DurationStyle.module.css b/src/components/classroomModal/DurationStyle.module.css new file mode 100644 index 0000000..b4bc8df --- /dev/null +++ b/src/components/classroomModal/DurationStyle.module.css @@ -0,0 +1,50 @@ +.toggle { + --width: 51px; + --height: 26px; + + position: relative; + display: inline-block; + width: var(--width); + height: var(--height); + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.3); + border-radius: var(--height); + cursor: pointer; +} + +.toggle input { + display: none; +} + +.toggle .slider { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: var(--height); + background-color: #f2f2f2; + transition: all 0.4s ease-in-out; +} + +.toggle .slider::before { + content: ""; + position: absolute; + top: 50%; + left: 5px; + width: 16px; + height: 16px; + border-radius: calc(var(--height) / 2); + background-color: #c5c5c5; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.3); + transition: all 0.4s ease-in-out; + transform: translateY(-50%); +} + +.toggle input:checked + .slider { + background-color: #e5eeff; +} + +.toggle input:checked + .slider::before { + background-color: #337aff; + transform: translateX(calc(var(--width) - var(--height))) translateY(-50%); +} diff --git a/src/components/classroomModal/LectureDurationModal.tsx b/src/components/classroomModal/LectureDurationModal.tsx new file mode 100644 index 0000000..944e1ef --- /dev/null +++ b/src/components/classroomModal/LectureDurationModal.tsx @@ -0,0 +1,91 @@ +import { useState } from "react"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import { ko } from "date-fns/esm/locale"; +import Layout from "./common/Layout"; +import { resetInputContent } from "@/redux/contentSlice"; +import { resetInputTitle } from "@/redux/titleSlice"; +import { useDispatch } from "react-redux"; +import styles from "@/components/modal/DurationStyle.module.css"; +//import { ModalSubmitButton } from "./common/ModalSubmitButton"; +import useClassroomModal from "@/hooks/useClassroomModal"; +import { closeModal } from "@/redux/slice/classroomModalSlice"; + +const DurationModal: React.FC = ({}) => { + const [startDate, setStartDate] = useState(new Date()); + const [endDate, setEndDate] = useState(new Date()); + const dispatch = useDispatch(); + + const { handleModalMove } = useClassroomModal(); + const lectureUpload = () => { + dispatch(closeModal()); + }; + + return ( + +
+
+ + + + + 세부 설정 + +
+ {/* + 강의 만들기 + next + 링크 만들기 + next + 세부 설정 + */} +
+
+ setStartDate(date)} + className="bg-white border-2 border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2 cursor-pointer" + dateFormat="yyyy.MM.dd" + /> +
+ ~ +
+ setEndDate(date)} + className="bg-white border-2 border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2 cursor-pointer" + dateFormat="yyyy.MM.dd" + /> +
+
+
+
+ 강의 공개 + + + {/* */} +
+
+
+
+ ); +}; +export default DurationModal; diff --git a/src/components/classroomModal/MakeLectureModal.tsx b/src/components/classroomModal/MakeLectureModal.tsx new file mode 100644 index 0000000..e4b5374 --- /dev/null +++ b/src/components/classroomModal/MakeLectureModal.tsx @@ -0,0 +1,104 @@ +import { useState } from "react"; +import Layout from "./common/Layout"; +import Image from "next/image"; +import { ModalSubmitButton } from "./common/ModalSubmitButton"; +import useClassroomModal from "@/hooks/useClassroomModal"; + +const MakeModal: React.FC = ({}) => { + const isSelectModal = (modal: string) => { + setSelectedModal(modal); + }; + const [selectedModal, setSelectedModal] = useState(null); + const { handleModalMove } = useClassroomModal(); + + const onNextButtonClick = () => { + if (selectedModal === "note") { + // 노트 모달 선택 시 처리할 로직 + handleModalMove("noteModalOpen", "lectureTypeModalOpen"); + } else if (selectedModal === "video") { + // 영상 모달 선택 시 처리할 로직 + handleModalMove("videoFileModalOpen", "lectureTypeModalOpen"); + } else if (selectedModal === "link") { + handleModalMove("linkModalOpen", "lectureTypeModalOpen"); + } + }; + + return ( + +
+ + + 링크 만들기 + +
+
isSelectModal("note")} + > + note + 노트 만들기 +
+
isSelectModal("video")} + > + video + 영상 강의 만들기 +
+ +
+ + {/* { + onNextButtonClick(); + }) + } + contents="다음" + /> */} +
+
+ ); +}; + +export default MakeModal; diff --git a/src/components/classroomModal/common/ModalTitle.tsx b/src/components/classroomModal/common/ModalTitle.tsx new file mode 100644 index 0000000..66aad2c --- /dev/null +++ b/src/components/classroomModal/common/ModalTitle.tsx @@ -0,0 +1,24 @@ +interface ModalTtileProps { + modalTitle: string[]; +} + +const ModalTitle: React.FC = ({ modalTitle }) => { + return ( +

+ {modalTitle.map((val, idx) => + idx === 0 ? ( + {val} + ) : ( + + {val} + + ), + )} +

+ ); +}; + +export default ModalTitle; diff --git a/src/components/index.tsx b/src/components/index.tsx new file mode 100644 index 0000000..398f2b5 --- /dev/null +++ b/src/components/index.tsx @@ -0,0 +1,145 @@ +"use client"; +import React, { useState, ChangeEvent, useEffect, useRef } from "react"; + +const ImageUploadModal: React.FC = () => { + const [isOpen, setIsOpen] = useState(false); + const modalRef = useRef(null); + const [selectedImages, setSelectedImages] = useState([]); + const [imagePreviews, setImagePreviews] = useState([]); + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + + const openModal = () => { + setIsOpen(true); + }; + const closeModal = () => { + setTitle(""); + setContent(""); + setSelectedImages([]); + setIsOpen(false); + }; + + useEffect(() => { + const handleOutsideClick = (event: MouseEvent) => { + if ( + modalRef.current && + !modalRef.current.contains(event.target as Node) + ) { + closeModal(); + } + }; + + document.addEventListener("mousedown", handleOutsideClick); + + return () => { + document.removeEventListener("mousedown", handleOutsideClick); + }; + }, []); + + const handleImageChange = (event: ChangeEvent) => { + const files = Array.from(event.target.files || []); + setSelectedImages(files); + + const previewUrls = files.map(file => URL.createObjectURL(file)); + setImagePreviews(previewUrls); + }; + + const removeImage = (index: number) => { + const updatedImages = [...selectedImages]; + updatedImages.splice(index, 1); + setSelectedImages(updatedImages); + + const updatedPreviews = [...imagePreviews]; + updatedPreviews.splice(index, 1); + setImagePreviews(updatedPreviews); + }; + + const handleUpload = () => { + console.log(selectedImages, title, content); + setTitle(""); + setContent(""); + setSelectedImages([]); + closeModal(); + }; + + return ( +
+ + {isOpen && ( +
+
+
+
강의 만들기 - 노트 만들기
+ +
+ setTitle(e.target.value)} + /> +
+