diff --git a/package-lock.json b/package-lock.json index 69d5c9a..5cc8f12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.3", + "tailwind-variants": "^3.2.2", "tailwindcss": "^4.1.11" }, "devDependencies": { @@ -5110,6 +5111,25 @@ "dev": true, "license": "MIT" }, + "node_modules/tailwind-variants": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", + "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", + "license": "MIT", + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwind-merge": ">=3.0.0", + "tailwindcss": "*" + }, + "peerDependenciesMeta": { + "tailwind-merge": { + "optional": true + } + } + }, "node_modules/tailwindcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", diff --git a/package.json b/package.json index 7b4d64b..0beb32d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.3", + "tailwind-variants": "^3.2.2", "tailwindcss": "^4.1.11" }, "devDependencies": { diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 313d1b5..a45f7d7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,29 @@ -import {BrowserRouter, Routes, Route} from 'react-router-dom'; +import {BrowserRouter, Routes, Route, useLocation} from 'react-router-dom'; import Layout from './layout/Layout'; import LandingPage from './pages/common/LandingPage'; import UserIdInputPage from './pages/common/UserIdInputPage'; import Dashboard from './pages/common/Dashboard'; import AssignmentsPage from './pages/admin/assignments/AssignmentsPage'; import AssignmentSelectPage from './pages/admin/assignments/AssignmentSelectPage'; -import CourseOverviewPage from './pages/common/CourseOverviewPage'; +import CourseOverviewPage from './pages/course-overview/CourseOverviewPage'; import AssignmentCreatePage from './pages/admin/assignments/AssignmentCreatePage'; import CourseCreatePage from './pages/admin/courses/CourseCreatePage'; import StudentManagementPage from './pages/admin/student/studentManagementPage'; +import type {UserType} from './models/common'; +import {createContext} from 'react'; + +export const UserTypeContext = createContext('guest'); + +const AppContent = () => { + const pathname = useLocation().pathname; + const userType = pathname.startsWith('/admin') + ? 'admin' + : pathname.startsWith('/student') + ? 'student' + : 'guest'; -function App() { return ( - + {/* 공통 영역 */} }> @@ -39,6 +50,14 @@ function App() { } /> + + ); +}; + +function App() { + return ( + + ); } diff --git a/public/svg/addIcon.svg b/src/assets/svg/addIcon.svg similarity index 100% rename from public/svg/addIcon.svg rename to src/assets/svg/addIcon.svg diff --git a/public/svg/arrowdownIcon.svg b/src/assets/svg/arrowdownIcon.svg similarity index 100% rename from public/svg/arrowdownIcon.svg rename to src/assets/svg/arrowdownIcon.svg diff --git a/public/svg/arrowleftIcon.svg b/src/assets/svg/arrowleftIcon.svg similarity index 100% rename from public/svg/arrowleftIcon.svg rename to src/assets/svg/arrowleftIcon.svg diff --git a/public/svg/arrowrightIcon.svg b/src/assets/svg/arrowrightIcon.svg similarity index 100% rename from public/svg/arrowrightIcon.svg rename to src/assets/svg/arrowrightIcon.svg diff --git a/public/svg/chatIcon.svg b/src/assets/svg/chatIcon.svg similarity index 100% rename from public/svg/chatIcon.svg rename to src/assets/svg/chatIcon.svg diff --git a/public/svg/chevrondown.svg b/src/assets/svg/chevrondown.svg similarity index 100% rename from public/svg/chevrondown.svg rename to src/assets/svg/chevrondown.svg diff --git a/src/assets/images/correct.svg b/src/assets/svg/correct.svg similarity index 100% rename from src/assets/images/correct.svg rename to src/assets/svg/correct.svg diff --git a/public/svg/deleteIcon.svg b/src/assets/svg/deleteIcon.svg similarity index 100% rename from public/svg/deleteIcon.svg rename to src/assets/svg/deleteIcon.svg diff --git a/public/svg/dragAndDropIcon.svg b/src/assets/svg/dragAndDropIcon.svg similarity index 100% rename from public/svg/dragAndDropIcon.svg rename to src/assets/svg/dragAndDropIcon.svg diff --git a/public/svg/editIcon.svg b/src/assets/svg/editIcon.svg similarity index 100% rename from public/svg/editIcon.svg rename to src/assets/svg/editIcon.svg diff --git a/public/svg/ellipsisIcon.svg b/src/assets/svg/ellipsisIcon.svg similarity index 100% rename from public/svg/ellipsisIcon.svg rename to src/assets/svg/ellipsisIcon.svg diff --git a/public/svg/file.svg b/src/assets/svg/file.svg similarity index 100% rename from public/svg/file.svg rename to src/assets/svg/file.svg diff --git a/src/assets/images/incorrect.svg b/src/assets/svg/incorrect.svg similarity index 100% rename from src/assets/images/incorrect.svg rename to src/assets/svg/incorrect.svg diff --git a/src/assets/images/lock.svg b/src/assets/svg/lock.svg similarity index 100% rename from src/assets/images/lock.svg rename to src/assets/svg/lock.svg diff --git a/public/svg/notificationIcon.svg b/src/assets/svg/notificationIcon.svg similarity index 100% rename from public/svg/notificationIcon.svg rename to src/assets/svg/notificationIcon.svg diff --git a/public/svg/profileImage.svg b/src/assets/svg/profileImage.svg similarity index 100% rename from public/svg/profileImage.svg rename to src/assets/svg/profileImage.svg diff --git a/public/svg/search.svg b/src/assets/svg/search.svg similarity index 100% rename from public/svg/search.svg rename to src/assets/svg/search.svg diff --git a/public/svg/signoutIcon.svg b/src/assets/svg/signoutIcon.svg similarity index 100% rename from public/svg/signoutIcon.svg rename to src/assets/svg/signoutIcon.svg diff --git a/public/svg/singleEllipsisIcon.svg b/src/assets/svg/singleEllipsisIcon.svg similarity index 100% rename from public/svg/singleEllipsisIcon.svg rename to src/assets/svg/singleEllipsisIcon.svg diff --git a/src/assets/images/unsubmitted.svg b/src/assets/svg/unsubmitted.svg similarity index 100% rename from src/assets/images/unsubmitted.svg rename to src/assets/svg/unsubmitted.svg diff --git a/public/svg/userIcon.svg b/src/assets/svg/userIcon.svg similarity index 100% rename from public/svg/userIcon.svg rename to src/assets/svg/userIcon.svg diff --git a/src/components/admin/assignments/AssignmentCard.tsx b/src/components/admin/assignments/AssignmentCard.tsx index 214f459..9dfc275 100644 --- a/src/components/admin/assignments/AssignmentCard.tsx +++ b/src/components/admin/assignments/AssignmentCard.tsx @@ -1,8 +1,8 @@ import {useState} from 'react'; -import SingleEllipsisIcon from '/svg/singleEllipsisIcon.svg?react'; -import DragAndDropIcon from '/svg/dragAndDropIcon.svg?react'; -import DeleteIcon from '/svg/deleteIcon.svg?react'; -import EditIcon from '/svg/editIcon.svg?react'; +import SingleEllipsisIcon from '@/assets/svg/singleEllipsisIcon.svg?react'; +import DragAndDropIcon from '@/assets/svg/dragAndDropIcon.svg?react'; +import DeleteIcon from '@/assets/svg/deleteIcon.svg?react'; +import EditIcon from '@/assets/svg/editIcon.svg?react'; import type {Assignment} from './dummy/types'; interface AssignmentCardProps extends Assignment { diff --git a/src/components/admin/assignments/AssignmentFormLayout.tsx b/src/components/admin/assignments/AssignmentFormLayout.tsx index 3ddd194..97e487a 100644 --- a/src/components/admin/assignments/AssignmentFormLayout.tsx +++ b/src/components/admin/assignments/AssignmentFormLayout.tsx @@ -29,8 +29,12 @@ const AssignmentFormLayout = ({ {/* 하단 버튼 */}
- +
diff --git a/src/components/admin/assignments/AssignmentPageLayout.tsx b/src/components/admin/assignments/AssignmentPageLayout.tsx index 66327d1..49a381b 100644 --- a/src/components/admin/assignments/AssignmentPageLayout.tsx +++ b/src/components/admin/assignments/AssignmentPageLayout.tsx @@ -2,7 +2,7 @@ import CourseSelector from './CourseSelector'; import AssignmentList from './AssignmentList'; import Button from '@/components/common/Button'; import type {Course} from './dummy/types'; -import AddIcon from '/svg/addIcon.svg?react'; +import AddIcon from '@/assets/svg/addIcon.svg?react'; import {useState} from 'react'; interface AssignmentPageLayoutProps { @@ -32,15 +32,14 @@ const AssignmentPageLayout = ({
{!selectMode && ( - )}
- +
diff --git a/src/components/admin/assignments/CourseSelector.tsx b/src/components/admin/assignments/CourseSelector.tsx index aef0a1a..5d248a4 100644 --- a/src/components/admin/assignments/CourseSelector.tsx +++ b/src/components/admin/assignments/CourseSelector.tsx @@ -1,5 +1,5 @@ import {useState} from 'react'; -import ArrowdownIcon from '/svg/arrowdownIcon.svg?react'; +import ArrowdownIcon from '@/assets/svg/arrowdownIcon.svg?react'; import type {Course} from './dummy/types'; interface CourseSelectorProps { diff --git a/src/components/admin/form/FileUpload.tsx b/src/components/admin/form/FileUpload.tsx index b86e3e7..0da270b 100644 --- a/src/components/admin/form/FileUpload.tsx +++ b/src/components/admin/form/FileUpload.tsx @@ -1,6 +1,6 @@ import {useRef, useState} from 'react'; import type {ChangeEvent} from 'react'; -import FileIcon from '/svg/file.svg?react'; +import FileIcon from '@/assets/svg/file.svg?react'; type FileUploadProps = { label: string; diff --git a/src/components/admin/form/LabeledDropdown.tsx b/src/components/admin/form/LabeledDropdown.tsx index 20e22cb..d55c0f6 100644 --- a/src/components/admin/form/LabeledDropdown.tsx +++ b/src/components/admin/form/LabeledDropdown.tsx @@ -1,5 +1,5 @@ import {useState, useRef} from 'react'; -import Chevrondown from '/svg/chevrondown.svg?react'; +import Chevrondown from '@/assets/svg/chevrondown.svg?react'; import useClickOutside from '@/hooks/useClickOutside'; interface LabeledDropdownProps diff --git a/src/components/common/Badge.tsx b/src/components/common/Badge.tsx new file mode 100644 index 0000000..401aff5 --- /dev/null +++ b/src/components/common/Badge.tsx @@ -0,0 +1,112 @@ +import {tv, type VariantProps} from 'tailwind-variants/lite'; +import Correct from '@/assets/svg/correct.svg?react'; +import Incorrect from '@/assets/svg/incorrect.svg?react'; +import Unsubmitted from '@/assets/svg/unsubmitted.svg?react'; + +const badgeStyles = tv({ + base: 'rounded-full px-3.5 py-1.5 leading-[19px] text-center text-base font-medium border whitespace-nowrap', +}); + +const scheduleBadgeStyles = tv({ + extend: badgeStyles, + variants: { + schedule: { + upcoming: + 'bg-radial-[50%_50%_at_50%_50%] from-[#7D63FF] from-38% to-[#AB9AFF] to-100% border-0 text-white', + later: 'bg-[#403D4D] border-[#5C5B7F] text-white', + }, + }, +}); + +const submissionBadgeStyles = tv({ + extend: badgeStyles, + base: 'bg-transparent flex-center gap-2 text-sm', + variants: { + status: { + CORRECT: 'border-primary text-primary', + INCORRECT: 'border-[#FF6F6F] text-[#FF6F6F]', + NOT_SUBMITTED: 'border-light-black text-light-black', + }, + }, +}); + +const indexBadgeStyles = tv({ + extend: badgeStyles, + variants: { + kind: { + unit: 'bg-secondary-black border-secondary-black text-white', + problem: 'bg-light-black border-light-black text-white', + }, + }, +}); + +type ScheduleBadgeProps = { + variant: 'schedule'; + children: React.ReactNode; +} & VariantProps; + +type SubmissionBadgeProps = { + variant: 'submission'; +} & VariantProps; + +type IndexBadgeProps = { + children: React.ReactNode; + variant: 'index'; +} & VariantProps; + +type BadgeProps = ScheduleBadgeProps | SubmissionBadgeProps | IndexBadgeProps; + +const SubmissionMeta = { + CORRECT: { + label: '정답', + icon: , + }, + INCORRECT: { + label: '오답', + icon: , + }, + NOT_SUBMITTED: { + label: '미제출', + icon: , + }, +} as const; + +const Badge = (props: BadgeProps) => { + const {variant} = props; + + switch (variant) { + // 일정 배지 + case 'schedule': + return ( + + {props.children}일 전 + + ); + + // 인덱스 배지 (단원, 문제 등) + case 'index': + const suffix = props.kind === 'unit' ? '단원' : ' 문제'; + + return ( + + {props.children} + {suffix} + + ); + + // 제출 상태 배지 + case 'submission': + const {label, icon} = SubmissionMeta[props.status!]; + + return ( + + {icon} {label} + + ); + } +}; + +export default Badge; diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx index 9ff4b1c..34bdec6 100644 --- a/src/components/common/Button.tsx +++ b/src/components/common/Button.tsx @@ -1,28 +1,61 @@ -interface ButtonProps { - theme: string; - text: string; - icon?: React.ReactElement; - onClick?: () => void; -} +import {tv, type VariantProps} from 'tailwind-variants/lite'; -interface ButtonTheme { - [key: string]: string; -} +const button = tv({ + base: 'cursor-pointer rounded-[10px] border', + variants: { + color: { + primary: 'bg-primary text-white border-primary', + secondary: + 'bg-white text-primary-black border-purple-stroke hover:bg-hover hover:text-white', + outlinePurple: 'bg-white text-primary border-primary', + outlineWhite: 'bg-transparent text-white border-white', + tonal: 'bg-purple-stroke text-secondary-black border-purple-stroke', + ghost: 'bg-transparent text-black border-none', + ghostWhite: 'bg-white text-secondary-black border-none', + }, + size: { + default: 'w-24 h-10 px-3 py-1.5', + compact: 'w-fit px-3 py-1.5 leading-5', + wide: 'w-40 py-[15px]', + none: 'w-fit p-0', + icon: 'w-16 h-16 p-0 rounded-full', // 아이콘 버튼 rounded 속성 적용 + }, + content: { + text: 'text-center text-base font-medium whitespace-nowrap', + icon: 'flex-center', + mixed: + 'flex-center gap-2 text-center text-base font-medium whitespace-nowrap', + }, + }, + defaultVariants: { + color: 'primary', + size: 'default', + content: 'text', + }, +}); -const buttonTheme: ButtonTheme = { - primaryPurple: 'primary-btn bg-primary text-white', - primaryWhite: 'primary-btn bg-white text-primary border', - primaryTransparent: 'primary-btn text-white border border-white', - secondaryPurpleStroke: 'secondary-btn bg-purple-stroke text-secondary-black', -}; +type ButtonVariants = VariantProps; + +interface ButtonProps extends ButtonVariants { + children: React.ReactNode; + className?: string; + type?: 'button' | 'submit'; + disabled?: boolean; + onClick?: () => void; + onMouseEnter?: () => void; + onMouseLeave?: () => void; +} -const Button = ({theme, text, icon, onClick}: ButtonProps) => { +const Button = ({ + children, + onClick, + type = 'button', + disabled = false, + ...props +}: ButtonProps) => { return ( - ); }; diff --git a/src/components/common/CourseOverview/AssignmentItem.tsx b/src/components/common/CourseOverview/AssignmentItem.tsx deleted file mode 100644 index 5c667be..0000000 --- a/src/components/common/CourseOverview/AssignmentItem.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import type {Assignment} from '@/components/admin/assignments/dummy/types'; -import SubmissionBadge from './SubmissionBadge'; - -interface AssignmentItemProps extends Assignment { - index: number; - isOpen?: boolean; -} - -const AssignmentItem = ({ - title, - index, - submittedStatus, - isOpen, -}: AssignmentItemProps) => { - return ( -
-
- {/* 문제 번호 */} - - {index} - - {/* 문제 제목 */} - - {title} - -
- {isOpen && } -
- ); -}; - -export default AssignmentItem; diff --git a/src/components/common/CourseOverview/AssignmentList.tsx b/src/components/common/CourseOverview/AssignmentList.tsx deleted file mode 100644 index edd50a2..0000000 --- a/src/components/common/CourseOverview/AssignmentList.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type {Assignment} from '@/components/admin/assignments/dummy/types'; -import AssignmentItem from './AssignmentItem'; - -interface AssignmentListProps { - isOpen?: boolean; - assignments: Assignment[]; -} - -const AssignmentList = ({isOpen, assignments}: AssignmentListProps) => { - return ( -
- {assignments.map((assignment, index) => ( - - ))} -
- ); -}; - -export default AssignmentList; diff --git a/src/components/common/CourseOverview/CourseActionsBar.tsx b/src/components/common/CourseOverview/CourseActionsBar.tsx deleted file mode 100644 index 9287e4b..0000000 --- a/src/components/common/CourseOverview/CourseActionsBar.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import {useNavigate} from 'react-router-dom'; -import Button from '@/components/common/Button'; - -interface CourseActionsBarProps { - isActive: boolean; - title: string; - section: string; -} - -const CourseActionsBar = ({isActive, title, section}: CourseActionsBarProps) => { - const navigate = useNavigate(); - - const handleStudentListClick = () => { - navigate(`/admin/student?course=${title} (${section})`); - }; - - return ( -
-
- ); -}; - -export default CourseActionsBar; diff --git a/src/components/common/CourseOverview/CourseContent.tsx b/src/components/common/CourseOverview/CourseContent.tsx deleted file mode 100644 index 1a36212..0000000 --- a/src/components/common/CourseOverview/CourseContent.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type {UnitInfo} from './dummy/types'; -import Unit from './Unit'; - -interface CourseContentProps { - units: UnitInfo[]; -} - -const CourseContent = ({units}: CourseContentProps) => { - return ( -
- {units.map((unit, index) => ( - - ))} -
- ); -}; - -export default CourseContent; diff --git a/src/components/common/CourseOverview/CourseHero.tsx b/src/components/common/CourseOverview/CourseHero.tsx deleted file mode 100644 index a9dcd4a..0000000 --- a/src/components/common/CourseOverview/CourseHero.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import {useLocation} from 'react-router-dom'; -import snowcodeOverviewMini from '@/assets/images/snowcode_overview_mini.svg'; -import {getSemesterLabel} from '@/utils/getSemesterLabel'; -import type {SemesterCode} from '@/components/admin/assignments/dummy/types'; -import CourseActionsBar from './CourseActionsBar'; -import CourseStat from './CourseStat'; - -interface CoursHeroProps { - title: string; - year: number; - semester: SemesterCode; - section: string; - unitCount: number; - assignmentCount: number; - studentCount?: number; - isActiveCourse: boolean; -} - -const CourseHero = ({ - title, - year, - semester, - section, - unitCount, - assignmentCount, - studentCount, - isActiveCourse, -}: CoursHeroProps) => { - const pathname = useLocation().pathname; - const isAdmin = pathname.startsWith('/admin'); - - return ( -
- {isAdmin && isActiveCourse && ( -
- -
- )} - logo - - {title} - - {`${year}년 ${getSemesterLabel( - semester - )}학기 ${section}분반`} - {isActiveCourse && ( - - )} -
- ); -}; - -export default CourseHero; diff --git a/src/components/common/CourseOverview/CourseStat.tsx b/src/components/common/CourseOverview/CourseStat.tsx deleted file mode 100644 index 4d3046a..0000000 --- a/src/components/common/CourseOverview/CourseStat.tsx +++ /dev/null @@ -1,21 +0,0 @@ -interface CourseStatProps { - unitCount: number; - assignmentCount: number; - studentCount?: number; -} - -const CourseStat = ({ - unitCount, - assignmentCount, - studentCount, -}: CourseStatProps) => { - return ( -
- {`${unitCount}단원 | ${assignmentCount}문제 ${ - studentCount ? `| ${studentCount}명` : '' - }`} -
- ); -}; - -export default CourseStat; diff --git a/src/components/common/CourseOverview/EmptyCourse.tsx b/src/components/common/CourseOverview/EmptyCourse.tsx deleted file mode 100644 index 5c473c2..0000000 --- a/src/components/common/CourseOverview/EmptyCourse.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import snowCodeOverview from '@/assets/images/snowcode_overview.svg'; -import CourseActionsBar from './CourseActionsBar'; - -const EmptyCourse = () => { - return ( -
- - - 비어있는 강의 - - - 아직 생성된 단원이 없어요 - - -
- ); -}; - -export default EmptyCourse; diff --git a/src/components/common/CourseOverview/SubmissionBadge.tsx b/src/components/common/CourseOverview/SubmissionBadge.tsx deleted file mode 100644 index b5488f0..0000000 --- a/src/components/common/CourseOverview/SubmissionBadge.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type {SubmissionStatus} from '@/components/admin/assignments/dummy/types'; -import correct from '@/assets/images/correct.svg'; -import incorrect from '@/assets/images/incorrect.svg'; -import unsubmitted from '@/assets/images/unsubmitted.svg'; - -interface SubmissionBadgeProps { - submittedStatus?: SubmissionStatus; -} - -const label_mapping: Record< - SubmissionStatus, - {label: string; variant: string; icon: string} -> = { - INCORRECT: { - label: '오답', - variant: 'border border-[#FF6F6F] text-[#FF6F6F]', - icon: incorrect, - }, - CORRECT: { - label: '정답', - variant: 'border border-[#856CFF] text-[#856CFF]', - icon: correct, - }, - NOT_SUBMITTED: { - label: '미제출', - variant: 'border border-[#7D7993 ] text-[#7D7993]', - icon: unsubmitted, - }, -}; - -const SubmissionBadge = ({submittedStatus}: SubmissionBadgeProps) => { - const safeStatus: SubmissionStatus = submittedStatus ?? 'NOT_SUBMITTED'; - const {label, variant, icon} = label_mapping[safeStatus]; - - return ( -
- {label} - {label} -
- ); -}; - -export default SubmissionBadge; diff --git a/src/components/common/CourseOverview/Unit.tsx b/src/components/common/CourseOverview/Unit.tsx deleted file mode 100644 index 3fe5e11..0000000 --- a/src/components/common/CourseOverview/Unit.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import AssignmentList from './AssignmentList'; -import type {UnitInfo} from './dummy/types'; -import UnitHeader from './UnitHeader'; - -interface UnitProps extends UnitInfo { - index: number; -} -const Unit = (unit: UnitProps) => { - const openStatus = unit.isOpen === undefined ? true : unit.isOpen; - return ( -
-
- - -
-
- ); -}; - -export default Unit; diff --git a/src/components/common/CourseOverview/UnitHeader.tsx b/src/components/common/CourseOverview/UnitHeader.tsx deleted file mode 100644 index 290bfbc..0000000 --- a/src/components/common/CourseOverview/UnitHeader.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import UnitLabel from './UnitLabel'; -import UnitLabel from './UnitLabel'; -import lock from '@/assets/images/lock.svg'; - -interface UnitHeaderProps { - index: number; - title: string; - isOpen?: boolean; - releaseDate: string; - dueDate: string; -} - -const UnitHeader = ({ - index, - title, - isOpen, - releaseDate, - dueDate, -}: UnitHeaderProps) => { - return ( -
-
- - - {title} - - {!isOpen && 잠금} -
- {`${releaseDate.replaceAll( - '-', - '.' - )} ~ ${dueDate.replaceAll('-', '.')}`} -
- ); -}; - -export default UnitHeader; diff --git a/src/components/common/CourseOverview/UnitLabel.tsx b/src/components/common/CourseOverview/UnitLabel.tsx deleted file mode 100644 index 71ba934..0000000 --- a/src/components/common/CourseOverview/UnitLabel.tsx +++ /dev/null @@ -1,13 +0,0 @@ -interface UnitLabelProps { - unitNo: number; -} - -const UnitLabel = ({unitNo}: UnitLabelProps) => { - return ( -
- {`${unitNo}단원`} -
- ); -}; - -export default UnitLabel; diff --git a/src/components/common/CourseOverview/dummy/types.ts b/src/components/common/CourseOverview/dummy/types.ts deleted file mode 100644 index 1d01bec..0000000 --- a/src/components/common/CourseOverview/dummy/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { - Assignment, - SemesterCode, -} from '@/components/admin/assignments/dummy/types'; - -// unit branch merge되면 삭제 -interface UnitInfo { - id: number; - title: string; - releaseDate: string; - dueDate: string; - isOpen?: boolean; - assignmentCount: number; - assignments: Assignment[]; -} - -interface CourseInfo { - id: number; - title: string; - year: number; - semester: SemesterCode; - section: string; - studentCount?: number; - unitCount: number; - units: UnitInfo[]; -} - -interface CourseResponse { - success: boolean; - response: CourseInfo; -} - -export type {UnitInfo, CourseInfo, CourseResponse}; diff --git a/src/components/common/Dashboard/CourseCard.tsx b/src/components/common/Dashboard/CourseCard.tsx index dc60025..3db3c8c 100644 --- a/src/components/common/Dashboard/CourseCard.tsx +++ b/src/components/common/Dashboard/CourseCard.tsx @@ -1,7 +1,7 @@ import {useState} from 'react'; -import EllipsisIcon from '/svg/ellipsisIcon.svg?react'; +import EllipsisIcon from '@/assets/svg/ellipsisIcon.svg?react'; import type {Course, UserType} from './types'; -import {getSemesterLabel} from '@/utils/getSemesterLabel'; +import {formatSemester} from '@/utils/course'; interface CourseCardProps extends Course { userType: UserType; @@ -27,7 +27,7 @@ const CourseCard = ({

- {`${year}\\${getSemesterLabel(semester)}학기\\${section}분반`} + {`${year}\\${formatSemester(semester)}학기\\${section}분반`}

{title}

diff --git a/src/components/common/Dashboard/CourseList.tsx b/src/components/common/Dashboard/CourseList.tsx index 537ef9a..4be3f4a 100644 --- a/src/components/common/Dashboard/CourseList.tsx +++ b/src/components/common/Dashboard/CourseList.tsx @@ -1,7 +1,8 @@ import logo from '@/assets/images/snowCode_logo_mini.svg'; import CourseCard from './CourseCard'; -import AddIcon from '/svg/addIcon.svg?react'; +import AddIcon from '@/assets/svg/addIcon.svg?react'; import type {Course, UserType} from './types'; +import Button from '../Button'; const courses: Course[] = [ { @@ -45,10 +46,13 @@ const CourseList = ({userType}: CourseListProps) => { 강의 목록 {userType === 'admin' && ( - + 추가 + )}

diff --git a/src/components/common/Dashboard/ScheduleCard.tsx b/src/components/common/Dashboard/ScheduleCard.tsx index 5d9ff04..17b2de1 100644 --- a/src/components/common/Dashboard/ScheduleCard.tsx +++ b/src/components/common/Dashboard/ScheduleCard.tsx @@ -1,3 +1,4 @@ +import Badge from '../Badge'; import type {Assignment} from './types'; interface ScheduleCardProps extends Assignment { remainingDays: number; @@ -11,14 +12,11 @@ const ScheduleCard = ({ }: ScheduleCardProps) => { return (
-
- {`${remainingDays}일 전`} -
+ + {remainingDays} +

{`${course} (${section})`}

diff --git a/src/index.css b/src/index.css index 26c7fe3..0749065 100644 --- a/src/index.css +++ b/src/index.css @@ -62,6 +62,10 @@ @apply flex items-center justify-center; } +@utility bg-radial-gradient { + @apply bg-radial-[80%_74%_at_34%_0%] from-[#AC9BFF] from-0% to-[#7D63FF] to-100%; +} + .header-btn { @apply bg-primary w-16 h-16 rounded-full flex-center; } diff --git a/src/layout/Layout.tsx b/src/layout/Layout.tsx index 09eeeae..bcefaaf 100644 --- a/src/layout/Layout.tsx +++ b/src/layout/Layout.tsx @@ -1,14 +1,12 @@ // Layout.js import {useLocation, Outlet} from 'react-router-dom'; import BaseHeader from '@/components/common/BaseHeader'; -import NotificationIcon from '/svg/notificationIcon.svg?react'; -import SignoutIcon from '/svg/signoutIcon.svg?react'; -import UserIcon from '/svg/userIcon.svg?react'; -import ChatIcon from '/svg/chatIcon.svg?react'; -import IconButton from '@/components/common/IconButton'; -import React from 'react'; - -type UserType = 'admin' | 'student'; +import NotificationIcon from '@/assets/svg/notificationIcon.svg?react'; +import SignoutIcon from '@/assets/svg/signoutIcon.svg?react'; +import UserIcon from '@/assets/svg/userIcon.svg?react'; +import ChatIcon from '@/assets/svg/chatIcon.svg?react'; +import Button from '@/components/common/Button'; +import type {UserType} from '@/models/common'; interface NavButton { icon: React.ReactElement; @@ -59,7 +57,13 @@ const Layout = () => {

); diff --git a/src/models/common.ts b/src/models/common.ts new file mode 100644 index 0000000..b40de49 --- /dev/null +++ b/src/models/common.ts @@ -0,0 +1,10 @@ +/** + * API 공통 응답 타입 + */ + +export interface ApiResponse { + success: boolean; + response: T; +} + +export type UserType = 'admin' | 'student' | 'guest'; diff --git a/src/models/course.ts b/src/models/course.ts new file mode 100644 index 0000000..c9c502d --- /dev/null +++ b/src/models/course.ts @@ -0,0 +1,44 @@ +import type {ApiResponse} from './common'; + +// 학기 및 제출 상태 상수 타입 정의 +export type SemesterCode = 'FIRST' | 'SECOND' | 'SUMMER' | 'WINTER'; +export type SubmissionStatus = 'NOT_SUBMITTED' | 'CORRECT' | 'INCORRECT'; + +// 과제(Assignment) 인터페이스 정의 +export interface Assignment { + id: number; + title: string; + submittedStatus: SubmissionStatus; +} + +// 단원(Unit) 인터페이스 정의 +export interface Unit { + id: number; + title: string; + releaseDate: string; + dueDate: string; + isOpen?: boolean; + assignmentCount: number; + assignments: Assignment[]; +} + +// 베이스 강의(Course) 인터페이스 정의 (공통 필드) +export interface BaseCourse { + id: number; + title: string; + year: number; + semester: SemesterCode; + section: string; + unitCount: number; +} + +/** + * 강의 상세 페이지용 (course-overview) 인터페이스 정의 + * 기본 정보 + 단원, 과제 상세 리스트 + */ +export interface CourseOverview extends BaseCourse { + studentCount?: number; + units: Unit[]; +} + +export type CourseOverviewResponse = ApiResponse; diff --git a/src/pages/admin/assignments/AssignmentCreatePage.tsx b/src/pages/admin/assignments/AssignmentCreatePage.tsx index b8cb713..24babde 100644 --- a/src/pages/admin/assignments/AssignmentCreatePage.tsx +++ b/src/pages/admin/assignments/AssignmentCreatePage.tsx @@ -3,6 +3,8 @@ import LabeledInput from '@/components/admin/form/LabeledInput'; import FileUpload from '@/components/admin/form/FileUpload'; import {useState} from 'react'; import LabeledDropdown from '@/components/admin/form/LabeledDropdown'; +import Button from '@/components/common/Button'; +import AddIcon from '@/assets/svg/addIcon.svg?react'; const AssignmentCreatePage = () => { const [examples, setExamples] = useState([{input: '', output: '', 공개: ''}]); @@ -58,11 +60,14 @@ const AssignmentCreatePage = () => { ))}
- + + 추가 + {}} diff --git a/src/pages/admin/student/studentManagementPage.tsx b/src/pages/admin/student/studentManagementPage.tsx index 270fe17..a59b9a9 100644 --- a/src/pages/admin/student/studentManagementPage.tsx +++ b/src/pages/admin/student/studentManagementPage.tsx @@ -1,5 +1,5 @@ import AssignmentFormLayout from '@/components/admin/assignments/AssignmentFormLayout'; -import Search from '/svg/search.svg?react'; +import Search from '@/assets/svg/search.svg?react'; import {useSearchParams} from 'react-router-dom'; export default function StudentManagementPage() { diff --git a/src/pages/common/CourseOverviewPage.tsx b/src/pages/common/CourseOverviewPage.tsx deleted file mode 100644 index ba392a3..0000000 --- a/src/pages/common/CourseOverviewPage.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import CourseContent from '@/components/common/CourseOverview/CourseContent'; -import CourseHero from '@/components/common/CourseOverview/CourseHero'; -import {courseResponse} from '@/components/common/CourseOverview/dummy/response'; -import EmptyCourse from '@/components/common/CourseOverview/EmptyCourse'; - -function countAssignments() { - let assignmentCount = 0; - courseResponse.response.units.forEach( - (unit) => (assignmentCount += unit.assignmentCount) - ); - - return assignmentCount; -} - -const isActiveCourse = courseResponse.response.unitCount !== 0; - -const CourseOverviewPage = () => { - return ( -
- - {isActiveCourse ? ( - - ) : ( - - )} -
- ); -}; - -export default CourseOverviewPage; diff --git a/src/pages/common/LandingPage.tsx b/src/pages/common/LandingPage.tsx index b6ccf21..1b2b1e5 100644 --- a/src/pages/common/LandingPage.tsx +++ b/src/pages/common/LandingPage.tsx @@ -1,12 +1,12 @@ import {useState} from 'react'; import {useNavigate} from 'react-router-dom'; -import snowCodeEntry from '/src/assets/images/snowCode_entry.svg'; -import snowCodeStudent from '/src/assets/images/snowCode_student.svg'; -import snowCodeAdmin from '/src/assets/images/snowCode_admin.svg'; -import googleLogo from '/src/assets/images/google_logo.svg'; -import ArrowrightIcon from '/svg/arrowrightIcon.svg?react'; -import ActionButton from '@/components/common/ActionButton'; +import snowCodeEntry from '@/assets/images/snowCode_entry.svg'; +import snowCodeStudent from '@/assets/images/snowCode_student.svg'; +import snowCodeAdmin from '@/assets/images/snowCode_admin.svg'; +import googleLogo from '@/assets/images/google_logo.svg'; +import ArrowrightIcon from '@/assets/svg/arrowrightIcon.svg?react'; +import Button from '@/components/common/Button'; type HoverState = 'none' | 'student' | 'admin'; @@ -39,11 +39,13 @@ export default function LandingPage() { {/* 상단 오른쪽 "다음으로" 버튼 */}
- +
{/* 로고 이미지 (선택/호버에 따라 이미지 변경) */} @@ -68,20 +70,22 @@ export default function LandingPage() {
- setSelected('student')} onMouseEnter={() => setHover('student')} - onMouseLeave={() => selected === 'none' && setHover('none')} - selected={selected === 'student'} - /> - selected === 'none' && setHover('none')}> + 학생 + +
diff --git a/src/pages/common/UserIdInputPage.tsx b/src/pages/common/UserIdInputPage.tsx index a9963a8..e7edd45 100644 --- a/src/pages/common/UserIdInputPage.tsx +++ b/src/pages/common/UserIdInputPage.tsx @@ -1,8 +1,8 @@ import {useState, useRef, useEffect} from 'react'; import {useNavigate, useLocation} from 'react-router-dom'; -import ActionButton from '@/components/common/ActionButton'; import SnowCodeEntryMini from '@/assets/images/snowCode_entry_mini.svg'; -import ArrowleftIcon from '/svg/arrowleftIcon.svg?react'; +import ArrowleftIcon from '@/assets/svg/arrowleftIcon.svg?react'; +import Button from '@/components/common/Button'; export default function UserIdInputPage() { const navigate = useNavigate(); @@ -78,13 +78,14 @@ export default function UserIdInputPage() {
- +
-
SnowCode Entry Mini
@@ -121,13 +122,14 @@ export default function UserIdInputPage() { /> ))}
- - + className='disabled:cursor-not-allowed'> + 확인 +
); diff --git a/src/pages/course-overview/CourseOverviewPage.tsx b/src/pages/course-overview/CourseOverviewPage.tsx new file mode 100644 index 0000000..9640844 --- /dev/null +++ b/src/pages/course-overview/CourseOverviewPage.tsx @@ -0,0 +1,23 @@ +import {getTotalAssignmentCount} from '@/utils/course'; +import CourseContent from './ui/CourseContent'; +import CourseHero from './ui/CourseHero'; +import {courseResponse} from '@/pages/course-overview/models/response'; + +const CourseOverviewPage = () => { + const courseData = courseResponse.response; + const totalAssignmentCount = getTotalAssignmentCount(courseData.units); // 총 문제 수 계산 + const hasUnits = courseData.unitCount !== 0 ? true : false; + + return ( +
+ + +
+ ); +}; + +export default CourseOverviewPage; diff --git a/src/components/common/CourseOverview/dummy/response.ts b/src/pages/course-overview/models/response.ts similarity index 95% rename from src/components/common/CourseOverview/dummy/response.ts rename to src/pages/course-overview/models/response.ts index 7a83642..9f00196 100644 --- a/src/components/common/CourseOverview/dummy/response.ts +++ b/src/pages/course-overview/models/response.ts @@ -1,6 +1,6 @@ -import type {CourseResponse} from './types'; +import type {CourseOverviewResponse} from '@/models/course'; -export const courseResponse: CourseResponse = { +export const courseResponse: CourseOverviewResponse = { success: true, response: { id: 1, diff --git a/src/pages/course-overview/models/types.ts b/src/pages/course-overview/models/types.ts new file mode 100644 index 0000000..d983352 --- /dev/null +++ b/src/pages/course-overview/models/types.ts @@ -0,0 +1,66 @@ +import type {Assignment, CourseOverview, Unit} from '@/models/course'; + +/** + * CourseHero 컴포넌트 props 타입 정의 + */ +export interface CourseHeroProps { + courseData: Omit; + assignmentCount: number; + isActiveCourse: boolean; +} + +/** + * CourseInfo 컴포넌트 props 타입 정의 + */ +export type CourseInfoProps = Pick< + CourseHeroProps['courseData'], + 'title' | 'year' | 'semester' | 'section' +>; + +/** + * CourseStats 컴포넌트 props 타입 정의 + */ +export interface CourseStatsProps + extends Pick { + assignmentCount: CourseHeroProps['assignmentCount']; + isAdmin: boolean; +} + +/** + * CourseContent 컴포넌트 props 타입 정의 + */ +export interface CourseContentProps { + units: CourseOverview['units']; + isActiveCourse: boolean; +} + +/** + * UnitItem 컴포넌트 props 타입 정의 + */ +export interface UnitProps extends Unit { + index: number; +} + +/** + * UnitHeader 컴포넌트 props 타입 정의 + */ +export type UnitHeaderProps = Omit< + UnitProps, + 'id' | 'assignmentCount' | 'assignments' +>; + +/** + * AssignmentList 컴포넌트 props 타입 정의 + */ +export interface AssignmentListProps { + isOpen?: boolean; + assignments: Assignment[]; +} + +/** + * AssignmentItem 컴포넌트 props 타입 정의 + */ +export interface AssignmentItemProps extends Assignment { + index: number; + isOpen?: boolean; +} diff --git a/src/pages/course-overview/ui/AssignmentList.tsx b/src/pages/course-overview/ui/AssignmentList.tsx new file mode 100644 index 0000000..6ecdaa3 --- /dev/null +++ b/src/pages/course-overview/ui/AssignmentList.tsx @@ -0,0 +1,66 @@ +import Badge from '@/components/common/Badge'; +import {Link} from 'react-router-dom'; +import type {AssignmentItemProps, AssignmentListProps} from '../models/types'; + +// 문제 목록 +const AssignmentList = ({isOpen, assignments}: AssignmentListProps) => { + return ( +
    + {assignments.map((assignment, index) => ( + + ))} +
+ ); +}; + +// 개별 문제 항목 +const AssignmentItem = ({ + title, + index, + submittedStatus, + isOpen, +}: AssignmentItemProps) => { + const isLocked = isOpen === false ? 'opacity-60 pointer-events-none' : ''; + + return ( +
  • + {/* 좌측: 인덱스, 문제명 */} +
    +
    + + {index} + + + {isOpen ? ( + +

    + {title} +

    + + ) : ( +

    + {title} +

    + )} +
    + + {/* 우측: 제출현황 배지 */} + {isOpen && ( +
    + +
    + )} +
    +
  • + ); +}; + +export default AssignmentList; diff --git a/src/pages/course-overview/ui/CourseActionsBar.tsx b/src/pages/course-overview/ui/CourseActionsBar.tsx new file mode 100644 index 0000000..158c795 --- /dev/null +++ b/src/pages/course-overview/ui/CourseActionsBar.tsx @@ -0,0 +1,22 @@ +import Button from '@/components/common/Button'; +import {Link} from 'react-router-dom'; + +// 강의 관리 버튼 바 (관리자 전용) +const CourseActionsBar = ({isActiveCourse}: {isActiveCourse: boolean}) => { + return ( +
    + + + + + + +
    + ); +}; + +export default CourseActionsBar; diff --git a/src/pages/course-overview/ui/CourseContent.tsx b/src/pages/course-overview/ui/CourseContent.tsx new file mode 100644 index 0000000..e3018ae --- /dev/null +++ b/src/pages/course-overview/ui/CourseContent.tsx @@ -0,0 +1,38 @@ +import snowCodeOverview from '@/assets/images/snowcode_overview.svg'; +import UnitItem from './UnitItem'; +import CourseActionsBar from './CourseActionsBar'; +import {useContext} from 'react'; +import {UserTypeContext} from '@/App'; +import type {CourseContentProps} from '../models/types'; + +const CourseContent = ({units, isActiveCourse}: CourseContentProps) => { + // 빈 강의 + if (isActiveCourse === false) { + return ; + } + + return ( +
    + {units.map((unit, index) => ( + + ))} +
    + ); +}; + +const EmptyCourse = () => { + const userType = useContext(UserTypeContext); // 유저 타입 확인 + + return ( +
    + snowCode 이미지 +

    비어있는 강의

    +

    + 아직 생성된 단원이 없어요 +

    + {/* 강의 관리 버튼은 관리자 전용 */} + {userType === 'admin' && } +
    + ); +}; +export default CourseContent; diff --git a/src/pages/course-overview/ui/CourseHero.tsx b/src/pages/course-overview/ui/CourseHero.tsx new file mode 100644 index 0000000..0048404 --- /dev/null +++ b/src/pages/course-overview/ui/CourseHero.tsx @@ -0,0 +1,79 @@ +import snowcodeOverviewMini from '@/assets/images/snowcode_overview_mini.svg'; +import {formatCourseInfo, formatSemester} from '@/utils/course'; +import CourseActionsBar from './CourseActionsBar'; +import {useContext} from 'react'; +import {UserTypeContext} from '@/App'; +import type { + CourseHeroProps, + CourseInfoProps, + CourseStatsProps, +} from '../models/types'; + +// 강의 상세 페이지 - Hero 섹션 +const CourseHero = ({ + courseData, + assignmentCount, + isActiveCourse, +}: CourseHeroProps) => { + const isAdmin = useContext(UserTypeContext) === 'admin' ? true : false; + const {title, year, semester, section, unitCount, studentCount} = courseData; + + return ( +
    + {isAdmin && isActiveCourse && ( + + )} + + + + {isActiveCourse && ( + + )} +
    + ); +}; + +// 강의 기본 정보 표시 +const CourseInfo = ({title, year, semester, section}: CourseInfoProps) => { + return ( +
    + snowCode logo +

    {title}

    +

    + {formatCourseInfo(year, semester, section)} +

    +
    + ); +}; + +// 강의 Stats 표시 +const CourseStats = ({ + unitCount, + assignmentCount, + studentCount, + isAdmin, +}: CourseStatsProps) => { + const studentInfo = + isAdmin && studentCount !== undefined ? ` | ${studentCount}명` : ''; + const courseStats = `${unitCount}단원 | ${assignmentCount}문제${studentInfo}`; + + return ( +
    + {courseStats} +
    + ); +}; + +export default CourseHero; diff --git a/src/pages/course-overview/ui/UnitItem.tsx b/src/pages/course-overview/ui/UnitItem.tsx new file mode 100644 index 0000000..46bc2ef --- /dev/null +++ b/src/pages/course-overview/ui/UnitItem.tsx @@ -0,0 +1,58 @@ +import AssignmentList from './AssignmentList'; +import Lock from '@/assets/svg/lock.svg?react'; +import Badge from '@/components/common/Badge'; +import {formatPeriod} from '@/utils/course'; +import type {UnitHeaderProps, UnitProps} from '../models/types'; + +const UnitItem = ({index, ...unit}: UnitProps) => { + const isOpen = unit.isOpen ?? true; + + return ( +
    + + +
    + ); +}; + +// 단원 헤더 컴포넌트 +const UnitHeader = ({ + index, + title, + isOpen, + releaseDate, + dueDate, +}: UnitHeaderProps) => { + return ( +
    +
    + {/* 좌측: 인덱스, 제목, 잠금 아이콘 */} +
    + + {index} + + +
    +

    + {title} +

    + {!isOpen && } +
    +
    + + {/* 우측: 데드라인 */} +
    + {formatPeriod(releaseDate, dueDate)} +
    +
    +
    + ); +}; + +export default UnitItem; diff --git a/src/utils/course.ts b/src/utils/course.ts new file mode 100644 index 0000000..5bdbb71 --- /dev/null +++ b/src/utils/course.ts @@ -0,0 +1,38 @@ +import type {SemesterCode, Unit} from '@/models/course'; + +const SEMESTER_MAP: Record = { + FIRST: '1', + SECOND: '2', + SUMMER: '여름', + WINTER: '겨울', +} as const; + +// 학기 포맷팅 +export const formatSemester = (semester: SemesterCode) => { + const label = SEMESTER_MAP[semester]; + return `${label}학기`; +}; + +// 날짜 포맷팅 +export const formatDate = (date: string) => { + return date.replaceAll('-', '.'); +}; + +// 과제 기간 포맷팅 +export const formatPeriod = (releaseDate: string, dueDate: string) => { + return `${formatDate(releaseDate)} ~ ${formatDate(dueDate)}`; +}; + +// 강의 정보 포맷팅 +export const formatCourseInfo = ( + year: number, + semester: SemesterCode, + section: string +) => { + return `${year}년 ${formatSemester(semester)} ${section}분반`; +}; + +// 총 과제 수 계산 +export const getTotalAssignmentCount = (units: Unit[]): number => { + return units.reduce((acc, unit) => acc + unit.assignmentCount, 0); +}; diff --git a/src/utils/getSemesterLabel.ts b/src/utils/getSemesterLabel.ts deleted file mode 100644 index f7d03a8..0000000 --- a/src/utils/getSemesterLabel.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type {SemesterCode} from '@/components/admin/assignments/dummy/types'; - -const SEMESTER_LABEL: Record = { - FIRST: '1', - SECOND: '2', - SUMMER: '여름', - WINTER: '겨울', -}; - -export const getSemesterLabel = (semester: SemesterCode) => { - return SEMESTER_LABEL[semester]; -};