diff --git a/.gitignore b/.gitignore index d32cc78..5d4a153 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +*.idea \ No newline at end of file diff --git a/package.json b/package.json index f38b46f..2ba0f33 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,10 @@ "*.{ts,tsx,js,jsx}": "eslint" }, "dependencies": { + "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", "@tanstack/react-query": "^5.52.2", "@tanstack/react-query-devtools": "^5.52.2", "@testing-library/react": "^16.0.1", diff --git a/src/app/desktop/payer-inquiry/_components/TableComponent/index.tsx b/src/app/desktop/payer-inquiry/_components/TableComponent/index.tsx index 6823571..69265a5 100644 --- a/src/app/desktop/payer-inquiry/_components/TableComponent/index.tsx +++ b/src/app/desktop/payer-inquiry/_components/TableComponent/index.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Table, TableBody, @@ -6,42 +7,125 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; interface Invoice { name: string; student_id: string; - admin: boolean; + admin?: boolean; } interface TableComponentProps { data: Invoice[]; + showCheckboxes?: boolean; // 고민이에요... ESLint: propType "handleDelete" is not required, but has no corresponding defaultProps declaration 에러가 뜸 + headers?: string[]; + selected: string[]; + setSelected: (selectedIds: (prev: string[]) => string[]) => void; + handleDelete?: (selectedIds: string[]) => void; // 22 } -export default function TableComponent({ data }: TableComponentProps) { +export default function TableComponent({ + data, + showCheckboxes = true, + headers = ['이름', '학번', '관리자 여부'], // 기본값을 설정 + selected, + setSelected, + handleDelete = () => {}, // 기본값으로 빈 함수 설정 +}: TableComponentProps) { + const [currentPage, setCurrentPage] = useState(1); + const rowsPerPage = 10; + + const handleSelect = (student_id: string) => { + setSelected((prev: string[]) => + prev.includes(student_id) + ? prev.filter((id) => id !== student_id) + : [...prev, student_id], + ); + }; + + const paginatedData = data.slice( + (currentPage - 1) * rowsPerPage, + currentPage * rowsPerPage, + ); + + const handleSelectAll = () => { + const visibleIds = paginatedData.map((item) => item.student_id); + setSelected((prev: string[]) => + prev.length === visibleIds.length ? [] : visibleIds, + ); + }; + return ( -
+
- 이름 - 학번 - 관리자 여부 + {showCheckboxes && ( + + + + )} + {headers.map((header) => ( + + {header} + + ))} - {data.map((item, index) => ( - + {paginatedData.map((item) => ( + + {showCheckboxes && ( + + handleSelect(item.student_id)} + /> + + )} {item.name} {item.student_id} - - {item.admin ? '⭕' : '❌'} - + {headers.includes('관리자 여부') && ( + + {item.admin !== undefined && (item.admin ? 'o' : 'x')} + + )} ))}
+
+ + + {currentPage} / {Math.ceil(data.length / rowsPerPage)} + + +
); } + +// defaultProps를 사용하여 headers에 기본값 설정 +TableComponent.defaultProps = { + headers: ['이름', '학번', '관리자 여부'], +}; diff --git a/src/app/desktop/payer-inquiry/page.tsx b/src/app/desktop/payer-inquiry/page.tsx index d52ba72..3370367 100644 --- a/src/app/desktop/payer-inquiry/page.tsx +++ b/src/app/desktop/payer-inquiry/page.tsx @@ -1,30 +1,193 @@ -import Sidebar from '@/components/desktop/SideBar'; +'use client'; + +import Sidebar from 'src/components/desktop/Sidebar'; import Search from '@/components/desktop/Search'; +import { useState } from 'react'; +import AddStudentId from '@/components/desktop/AddStudentId'; +import { Button } from '@/components/ui/button'; import TableComponent from './_components/TableComponent'; +import AddInput from '../../../components/desktop/AddInput'; const dummyData = [ { name: '조다운', student_id: '20223139', admin: true }, - { name: '조다운', student_id: '20223139', admin: true }, - { name: '조다운', student_id: '20223139', admin: false }, + { name: '이정욱', student_id: '20223888', admin: true }, + { name: '윤신지', student_id: '20223122', admin: false }, + { name: '황수민', student_id: '20223130', admin: true }, +]; + +const dummyData2 = [ + { name: '조다운', student_id: '20223139' }, + { name: '황현진', student_id: '20223158' }, ]; export default function PayerInquiryPage() { + const [data, setData] = useState(dummyData); // 기존 데이터 + const [addedData, setAddedData] = useState(dummyData2); // 추가된 데이터 + const [isDeleteModeOriginal, setIsDeleteModeOriginal] = useState(false); // 기존 데이터 삭제 모드 + const [isDeleteModeAdded, setIsDeleteModeAdded] = useState(false); // 추가된 데이터 삭제 모드 + const [selectedOriginal, setSelectedOriginal] = useState([]); // 기존 데이터에서 선택된 항목 + const [selectedAdded, setSelectedAdded] = useState([]); // 추가된 데이터에서 선택된 항목 + + const [newStudentId, setNewStudentId] = useState(''); + const [newStudentName, setNewStudentName] = useState(''); + + // 학번 입력 핸들러 + const handleStudentIdChange = (e: React.ChangeEvent) => { + setNewStudentId(e.target.value); + }; + + // 이름 입력 핸들러 + const handleStudentNameChange = (e: React.ChangeEvent) => { + setNewStudentName(e.target.value); + }; + + // 추가 버튼 클릭 시 실행될 함수 + const handleAddStudent = () => { + if (!newStudentId || !newStudentName) { + alert('이름과 학번을 입력해주세요.'); + return; + } + + // 학번이 8자리 숫자인지 검증 + const studentIdPattern = /^\d{8}$/; + if (!studentIdPattern.test(newStudentId)) { + alert('학번은 8자리 숫자로 입력해야 합니다.'); + return; + } + + const newEntry = { name: newStudentName, student_id: newStudentId }; + setAddedData([...addedData, newEntry]); // 추가된 데이터 업데이트 + setNewStudentId(''); + setNewStudentName(''); + }; + + // 기존 데이터 삭제 + const handleDeleteOriginal = () => { + const updatedData = data.filter( + (item) => !selectedOriginal.includes(item.student_id), + ); + setData(updatedData); + setIsDeleteModeOriginal(false); + setSelectedOriginal([]); + }; + + // 추가된 데이터 삭제 + const handleDeleteAdded = () => { + const updatedData = addedData.filter( + (item) => !selectedAdded.includes(item.student_id), + ); + setAddedData(updatedData); + setIsDeleteModeAdded(false); + setSelectedAdded([]); + }; + + // 기존 데이터 삭제 모드 토글 + const toggleDeleteModeOriginal = () => { + setIsDeleteModeOriginal((prev) => !prev); + setSelectedOriginal([]); + }; + + // 추가된 데이터 삭제 모드 토글 + const toggleDeleteModeAdded = () => { + setIsDeleteModeAdded((prev) => !prev); + setSelectedAdded([]); + }; + + const api = () => { + console.log('api 적용할 곳입니다.'); + }; + return ( -
+

학생회비 납부자 조회하기

-
+
-

Here you can change your account settings.

+
+
+ + +
+
+ +
+ + {isDeleteModeAdded && ( + + )} +
+
+
+
+ +
- +
+ +
+ + + {isDeleteModeOriginal && ( + + )} +
+
); } diff --git a/src/components/desktop/AddInput/index.tsx b/src/components/desktop/AddInput/index.tsx new file mode 100644 index 0000000..34a7fbe --- /dev/null +++ b/src/components/desktop/AddInput/index.tsx @@ -0,0 +1,24 @@ +import { Input } from '@/components/ui/input'; +import { Plus } from 'lucide-react'; + +interface AddInputProps { + value: string; + onClick: () => void; + onChange: (event: React.ChangeEvent) => void; +} + +export default function AddInput({ onClick, value, onChange }: AddInputProps) { + return ( + + } + value={value} + onChange={onChange} + placeholder="이름을 입력해주세요." + /> + ); +} diff --git a/src/components/desktop/AddStudentId/index.tsx b/src/components/desktop/AddStudentId/index.tsx new file mode 100644 index 0000000..cd84cf5 --- /dev/null +++ b/src/components/desktop/AddStudentId/index.tsx @@ -0,0 +1,23 @@ +import { Input } from '../../ui/input'; +import { Label } from '../../ui/label'; + +interface AddStudentIdProps { + value: string; + onChange: (event: React.ChangeEvent) => void; +} + +export default function AddStudentId({ value, onChange }: AddStudentIdProps) { + return ( +
+ {/* */} + +
+ ); +} diff --git a/src/components/desktop/Search/index.tsx b/src/components/desktop/Search/index.tsx index 128b6e1..f466474 100644 --- a/src/components/desktop/Search/index.tsx +++ b/src/components/desktop/Search/index.tsx @@ -2,10 +2,16 @@ import { SearchInput } from '../../ui/search-input'; -export default function Search() { +interface SearchProps { + placeholder?: string; +} + +export default function Search({ + placeholder = '검색어를 입력하세요.', +}: SearchProps) { return ( console.log(query)} /> ); diff --git a/src/components/desktop/SideBar/index.tsx b/src/components/desktop/Sidebar/index.tsx similarity index 65% rename from src/components/desktop/SideBar/index.tsx rename to src/components/desktop/Sidebar/index.tsx index 35e3179..7f06a76 100644 --- a/src/components/desktop/SideBar/index.tsx +++ b/src/components/desktop/Sidebar/index.tsx @@ -18,18 +18,20 @@ interface SidebarProps { export default function Sidebar({ children, title = 'Sidebar Title', - description = '', + // description = '', triggerText = 'Open', }: SidebarProps) { return ( - + {triggerText} - {title} - {description && {description}} + + {title} + + {/* {description && {description}} */}
{children}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..a49fc42 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + { + variants: { + variant: { + default: + 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: + 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + // 우리 버튼 추가 + primary: + 'whitespace-nowrap rounded-md bg-gray-primary px-3 py-1 text-sm text-white-primary', + secondary: + 'whitespace-nowrap rounded-md bg-gray-secondary px-3 py-1 text-sm text-white-primary', + deletePrimary: + 'whitespace-nowrap rounded-md bg-gray-primary px-1 py-1 text-sm text-white-primary', + deleteSecondary: + 'whitespace-nowrap rounded-md bg-gray-secondary px-1 py-1 text-sm text-white-primary', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', + chevron: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + }, +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..c6fdd07 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 993530e..4eacb59 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,9 +1,21 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; -import { Search } from 'lucide-react'; +import { Plus } from 'lucide-react'; // 기본 아이콘은 Plus로 설정 -const Input = React.forwardRef>( - ({ className, type = 'text', ...props }, ref) => { +interface InputProps extends React.ComponentProps<'input'> { + Icon?: React.ReactNode; // 아이콘을 전달할 수 있는 props +} + +const Input = React.forwardRef( + ( + { + className, + type = 'text', + Icon = , + ...props + }, + ref, + ) => { return (
>( ref={ref} {...props} /> - +
+ {Icon} {/* 전달된 아이콘을 렌더링 */} +
); }, diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..7114fb0 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +'use client'; + +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const labelVariants = cva( + 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index c0df655..d0c6422 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from 'react'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; const Table = React.forwardRef< HTMLTableElement, @@ -9,20 +9,20 @@ const Table = React.forwardRef<
-)) -Table.displayName = "Table" +)); +Table.displayName = 'Table'; const TableHeader = React.forwardRef< HTMLTableSectionElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( - -)) -TableHeader.displayName = "TableHeader" + +)); +TableHeader.displayName = 'TableHeader'; const TableBody = React.forwardRef< HTMLTableSectionElement, @@ -30,11 +30,11 @@ const TableBody = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -TableBody.displayName = "TableBody" +)); +TableBody.displayName = 'TableBody'; const TableFooter = React.forwardRef< HTMLTableSectionElement, @@ -43,13 +43,13 @@ const TableFooter = React.forwardRef< tr]:last:border-b-0", - className + 'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', + className, )} {...props} /> -)) -TableFooter.displayName = "TableFooter" +)); +TableFooter.displayName = 'TableFooter'; const TableRow = React.forwardRef< HTMLTableRowElement, @@ -58,13 +58,13 @@ const TableRow = React.forwardRef< -)) -TableRow.displayName = "TableRow" +)); +TableRow.displayName = 'TableRow'; const TableHead = React.forwardRef< HTMLTableCellElement, @@ -73,13 +73,13 @@ const TableHead = React.forwardRef<
[role=checkbox]]:translate-y-[2px]", - className + 'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', + className, )} {...props} /> -)) -TableHead.displayName = "TableHead" +)); +TableHead.displayName = 'TableHead'; const TableCell = React.forwardRef< HTMLTableCellElement, @@ -88,13 +88,13 @@ const TableCell = React.forwardRef< [role=checkbox]]:translate-y-[2px]", - className + 'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', + className, )} {...props} /> -)) -TableCell.displayName = "TableCell" +)); +TableCell.displayName = 'TableCell'; const TableCaption = React.forwardRef< HTMLTableCaptionElement, @@ -102,11 +102,11 @@ const TableCaption = React.forwardRef< >(({ className, ...props }, ref) => (
-)) -TableCaption.displayName = "TableCaption" +)); +TableCaption.displayName = 'TableCaption'; export { Table, @@ -117,4 +117,4 @@ export { TableRow, TableCell, TableCaption, -} +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 7fd69b7..aa075d7 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -13,7 +13,7 @@ export default { pretendard: ['Pretendard', 'sans-serif'], }, maxWidth: { - sm: '40rem', // sm:max-w-sm 클래스를 40rem으로 설정 + sm: '30rem', // sm:max-w-sm 클래스를 30rem으로 설정 }, colors: { foreground: 'hsl(var(--foreground))',