diff --git a/.idea/.name b/.idea/.name index bfcf1700..356d1537 100644 --- a/.idea/.name +++ b/.idea/.name @@ -1 +1 @@ -Swiper.tsx \ No newline at end of file +.name \ No newline at end of file diff --git a/package.json b/package.json index cbe74439..4adf7b3e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "@apollo/client": "^3.8.4", "@apollo/react-hooks": "^4.0.0", "@craco/craco": "^7.1.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/base": "^5.0.0-beta.40", diff --git a/src/api/apollo/gql/queries/NoticesResponseQuery.gql.ts b/src/api/apollo/gql/queries/NoticesResponseQuery.gql.ts index 8cdcddf0..ec45664d 100644 --- a/src/api/apollo/gql/queries/NoticesResponseQuery.gql.ts +++ b/src/api/apollo/gql/queries/NoticesResponseQuery.gql.ts @@ -1,9 +1,44 @@ import { gql } from '@apollo/client'; -export const SEARCH_NOTICE = gql` - query SearchNotice($request: NoticeSearchRequest!){ - searchNotice(request: $request){ - +// 이벤트 타입들 조회 +export const GET_EVENT_TYPE = gql` + query GetEventType { + __typename(name: "EventType") { + enumValues { + name + description + } + } + } +`; + +// 이벤트 목록 조회 +export const GET_EVENT_LIST = gql` + query GetEventList($eventType: EventType) { + getEventList(eventType: $eventType) { + id + startedAt + endedAt + title + contents + thumbnail + } + } +`; + +// 이벤트 상세 조회 +export const GET_EVENT_INFO = gql` + query GetEventInfo($eventId: Long!) { + getEventInfo(eventId: $eventId) { + id + startedAt + endedAt + title + contents + thumbnail + items + images + link } } `; diff --git a/src/components/atoms/collaseCell/CollapseCell.tsx b/src/components/atoms/collaseCell/CollapseCell.tsx new file mode 100644 index 00000000..95af57d7 --- /dev/null +++ b/src/components/atoms/collaseCell/CollapseCell.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { TableCell } from '@mui/material'; +import clsN from 'classnames'; +import styles from './styles/CollapseCell.module.scss'; + +export interface CollapseHaderProps { + size?: 'small' | 'medium'; + item: React.ReactNode; + className?: string; +} + +export const CollapseCell = ({ ...props }: CollapseHaderProps) => { + return ( + + {props.item} + + ); +}; + +CollapseCell.defaultProps = { + size: 'small', +}; diff --git a/src/components/atoms/collaseCell/styles/CollapseCell.module.scss b/src/components/atoms/collaseCell/styles/CollapseCell.module.scss new file mode 100644 index 00000000..3772fd8a --- /dev/null +++ b/src/components/atoms/collaseCell/styles/CollapseCell.module.scss @@ -0,0 +1,7 @@ +@import "index"; + +.table-cell{ + text-wrap: nowrap; + text-overflow: ellipsis; + +} \ No newline at end of file diff --git a/src/components/atoms/collaseCell/styles/index.scss b/src/components/atoms/collaseCell/styles/index.scss new file mode 100644 index 00000000..86e5d894 --- /dev/null +++ b/src/components/atoms/collaseCell/styles/index.scss @@ -0,0 +1 @@ +@import "../../../../styles/scss/index"; \ No newline at end of file diff --git a/src/components/atoms/datePicker/DatePicker.tsx b/src/components/atoms/datePicker/DatePicker.tsx index fab6288f..0b50b005 100644 --- a/src/components/atoms/datePicker/DatePicker.tsx +++ b/src/components/atoms/datePicker/DatePicker.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { TextFieldClasses } from '@mui/material'; import { Dayjs } from 'dayjs'; import { DatePicker as MuiDatePicker, DatePickerProps as MuiDatePickerProps } from '@mui/x-date-pickers'; import clsN from 'classnames'; @@ -17,8 +18,10 @@ const DatePicker = ({ value, onChange, slotProps, + classes, }: MuiDatePickerProps & { className?: string; + classes?: Partial; label?: React.ReactNode; }): React.ReactElement => { /* 렌더 */ @@ -29,7 +32,8 @@ const DatePicker = ({ textField: { size: 'small', classes: { - root: clsN(className, styles['date-root']), + ...classes, + root: clsN(className, styles['date-root'], classes?.root), }, inputProps: { className: clsN(styles['date-root__input']), diff --git a/src/components/atoms/input/Input.tsx b/src/components/atoms/input/Input.tsx index c16c9378..c95753c1 100644 --- a/src/components/atoms/input/Input.tsx +++ b/src/components/atoms/input/Input.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import TextField from '@mui/material/TextField'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; import clsN from 'classnames'; import styles from './style/Input.module.scss'; @@ -20,9 +20,9 @@ interface InputProps { // 문자나 이미지 등의 요소가 들어갈 자리에 임시로 채워놓는 내용물을 의미 placeholder?: string | undefined; // input 값 - inputVal: string | number; + inputVal: unknown | undefined; // onChange 이벤트 활성화 시 오브젝트 e 를 통한 작업 처리 => 반환 없음 - onChange?: (e: React.ChangeEvent) => void; + onChange: TextFieldProps['onChange']; // 여러줄 여부 multiline?: boolean | undefined; // form 컨트롤의 이름을 지정 주로 폼에 있는 내용을 서버에 보낼때 활용 @@ -31,6 +31,9 @@ interface InputProps { fullWidth?: boolean | undefined; // 새로운 type 속성 추가 type?: React.InputHTMLAttributes['type']; + // ref + ref?: TextFieldProps['ref']; + required?: TextFieldProps['required']; } export const Input = ({ @@ -47,9 +50,12 @@ export const Input = ({ fullWidth, type, inputVal, + ref, + required, }: InputProps) => { return ( ); }; Input.defaultProps = { - className: styles[''], - outlineClsN: styles[''], + className: styles['root-input'], + outlineClsN: styles['notched-outline'], variant: 'filled', size: 'medium', endAdornment: undefined, label: undefined, placeholder: undefined, - onChange: undefined, multiline: undefined, name: undefined, fullWidth: undefined, diff --git a/src/components/atoms/popover/PopOver.tsx b/src/components/atoms/popover/PopOver.tsx new file mode 100644 index 00000000..ebae29c2 --- /dev/null +++ b/src/components/atoms/popover/PopOver.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Popover, PopoverProps } from '@mui/material'; + +/** + * popover : 클릭 후 다음 요소가 활성화 되는 컴포넌트 + * 기존의 PopoverProps 를 Omit 하여 children 을 제외한 모든 인터페이스를 상속하였으나 아토믹 디자인 관점으론 그리 유익하지 않음 + */ +interface PopOverProps extends Omit { + children?: React.ReactNode; // 리액트 노드(선택적) +} + +export const PopOver = ({ ...props }: PopOverProps) => { + return {props.children}; +}; +PopOver.defaultProps = { + children: undefined, +}; diff --git a/src/components/atoms/popover/styles/PopOver.module.scss b/src/components/atoms/popover/styles/PopOver.module.scss new file mode 100644 index 00000000..16aae1d1 --- /dev/null +++ b/src/components/atoms/popover/styles/PopOver.module.scss @@ -0,0 +1,2 @@ +@import "index"; + diff --git a/src/components/atoms/popover/styles/index.scss b/src/components/atoms/popover/styles/index.scss new file mode 100644 index 00000000..16a66a97 --- /dev/null +++ b/src/components/atoms/popover/styles/index.scss @@ -0,0 +1 @@ +@import 'src/styles/scss/index'; \ No newline at end of file diff --git a/src/components/atoms/sortableItem/SortableItem.tsx b/src/components/atoms/sortableItem/SortableItem.tsx new file mode 100644 index 00000000..8f51e098 --- /dev/null +++ b/src/components/atoms/sortableItem/SortableItem.tsx @@ -0,0 +1,45 @@ +/* eslint-disable */ +import React from 'react'; +import { useSortable } from '@dnd-kit/sortable'; +// utilities for CSS animation +import { CSS } from '@dnd-kit/utilities'; +import { Box } from '@mui/material'; +import clsN from 'classnames'; +import styles from './style/SortableItem.module.scss'; + +interface SortableItemProps { + id: string; + content: React.ReactNode; + isActive: boolean; + className?: string; +} + +export const SortableItem = ({ id, content, isActive }: SortableItemProps) => { + const { + attributes, + listeners, + setNodeRef, // Element Reference + transform, // for element position moves + transition, // for smooth animation + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), // when Drag start elemet's position changes + transition, // when Drag end animation end as smoothely + }; + + return ( + + {content} + + ); +}; diff --git a/src/components/atoms/sortableItem/style/SortableItem.module.scss b/src/components/atoms/sortableItem/style/SortableItem.module.scss new file mode 100644 index 00000000..4f5be912 --- /dev/null +++ b/src/components/atoms/sortableItem/style/SortableItem.module.scss @@ -0,0 +1,11 @@ +@import "./index"; + +.sortable-item{ + padding: 4px; + background-color: inherit; + border: $border-default-color 1px; + border-radius: 4px; + &--is-active{ + box-shadow: 1px 1px 3px rgba(0,0,0,0.2); + } +} \ No newline at end of file diff --git a/src/components/organisms/admin/searchBar/styles/index.scss b/src/components/atoms/sortableItem/style/index.scss similarity index 100% rename from src/components/organisms/admin/searchBar/styles/index.scss rename to src/components/atoms/sortableItem/style/index.scss diff --git a/src/components/commons/draggableItem/DragableItem.tsx b/src/components/commons/draggableItem/DragableItem.tsx new file mode 100644 index 00000000..6acc3aec --- /dev/null +++ b/src/components/commons/draggableItem/DragableItem.tsx @@ -0,0 +1,3 @@ +const DragableItem = () => { + return <>; +}; diff --git a/src/components/commons/dropAndDrop/draggable/Draggable.tsx b/src/components/commons/dropAndDrop/draggable/Draggable.tsx new file mode 100644 index 00000000..8d6595e4 --- /dev/null +++ b/src/components/commons/dropAndDrop/draggable/Draggable.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { useDraggable } from '@dnd-kit/core'; +import { Button } from '@mui/material'; +import clsN from 'classnames'; +import styles from './styles/Draggable.module.scss'; + +interface DraggableProps { + // content + children: React.ReactNode; + // important + id: string; + // button Click Event + onClick?: () => void; +} +export const Draggable = ({ ...props }: DraggableProps) => { + /* use Button component for Draggable + * why ? button's basically can interact and focus with keyboard + * role = "button" it's means good at optimizing for screen reader + * Button compoenet has basic event example "click", "tab" + * Button compoenet are clickable element (UX) and have cursor : pointer + */ + + const { attributes, listeners, setNodeRef, transform } = useDraggable({ + id: props.id, + }); + + const style = transform + ? { + transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, + } + : undefined; + + return ( + + ); +}; diff --git a/src/components/commons/dropAndDrop/draggable/styles/Draggable.module.scss b/src/components/commons/dropAndDrop/draggable/styles/Draggable.module.scss new file mode 100644 index 00000000..451b16c4 --- /dev/null +++ b/src/components/commons/dropAndDrop/draggable/styles/Draggable.module.scss @@ -0,0 +1,7 @@ +@import "./index"; + +.draggable{ + &--transform{ + + } +} \ No newline at end of file diff --git a/src/components/commons/dropAndDrop/draggable/styles/index.scss b/src/components/commons/dropAndDrop/draggable/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/commons/dropAndDrop/draggable/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/commons/dropAndDrop/droppable/Droppable.tsx b/src/components/commons/dropAndDrop/droppable/Droppable.tsx new file mode 100644 index 00000000..04ffad9a --- /dev/null +++ b/src/components/commons/dropAndDrop/droppable/Droppable.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { useDroppable } from '@dnd-kit/core'; +import { Box } from '@mui/material'; +import clsN from 'classnames'; +import styles from './styles/Droppable.module.scss'; + +interface DroppableProps { + children: React.ReactNode; +} + +// for Draggable Item Landing Place +export const Droppable = ({ ...props }: DroppableProps) => { + const { isOver, setNodeRef } = useDroppable({ + id: 'droppable', + }); + + const style = { + color: isOver ? 'green' : undefined, + }; + + return ( + + {props.children} + + ); +}; diff --git a/src/components/commons/dropAndDrop/droppable/styles/Droppable.module.scss b/src/components/commons/dropAndDrop/droppable/styles/Droppable.module.scss new file mode 100644 index 00000000..765053d6 --- /dev/null +++ b/src/components/commons/dropAndDrop/droppable/styles/Droppable.module.scss @@ -0,0 +1,8 @@ +@import "./index"; + +.droppable{ + &--is-over{ + background-color: green; + color: green; + } +} \ No newline at end of file diff --git a/src/components/commons/dropAndDrop/droppable/styles/index.scss b/src/components/commons/dropAndDrop/droppable/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/commons/dropAndDrop/droppable/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/commons/dropAndDrop/sortableItem/SortableItem.tsx b/src/components/commons/dropAndDrop/sortableItem/SortableItem.tsx new file mode 100644 index 00000000..41e06212 --- /dev/null +++ b/src/components/commons/dropAndDrop/sortableItem/SortableItem.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { Box } from '@mui/material'; +import clsN from 'classnames'; +import styles from './styles/SortableItem.module.scss'; +import type { Disabled } from '@dnd-kit/sortable/dist/types'; +import type { UniqueIdentifier } from '@dnd-kit/core/dist/types'; + +interface SortableItemProps { + // uniqueIdentifier type : string | number + id: UniqueIdentifier; + children: React.ReactNode; + className?: string; + draggingClassName?: string; + disabled?: boolean | Disabled; +} + +export const SortableItem = ({ id, children, className, draggingClassName, disabled }: SortableItemProps) => { + const { attributes, listeners, setNodeRef, transform, transition, isDragging, setActivatorNodeRef } = useSortable({ + id, + disabled, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + zIndex: isDragging ? 1 : 'auto', + position: 'relative' as const, + touchAction: 'none', + }; + // inject CSS(dnd-Kit) style for "transform", "transition" + + // return JSX Element + return ( + { + setNodeRef(node); + setActivatorNodeRef(node); + }} + style={style} + {...attributes} + {...listeners} + data-handle + > + {children} + + ); +}; + +SortableItem.defaultProps = { + className: clsN(styles.box), +}; diff --git a/src/components/commons/dropAndDrop/sortableItem/styles/SortableItem.module.scss b/src/components/commons/dropAndDrop/sortableItem/styles/SortableItem.module.scss new file mode 100644 index 00000000..4353db68 --- /dev/null +++ b/src/components/commons/dropAndDrop/sortableItem/styles/SortableItem.module.scss @@ -0,0 +1,14 @@ +@import "./index"; + +.box{ + will-change: transform; + transition: transform 200ms ease; + + // when dragging + &.dragging{ + + } + &.disabled{ + + } +} diff --git a/src/components/commons/dropAndDrop/sortableItem/styles/index.scss b/src/components/commons/dropAndDrop/sortableItem/styles/index.scss new file mode 100644 index 00000000..16a66a97 --- /dev/null +++ b/src/components/commons/dropAndDrop/sortableItem/styles/index.scss @@ -0,0 +1 @@ +@import 'src/styles/scss/index'; \ No newline at end of file diff --git a/src/components/commons/tinyEditor/TinyEditorBasic.tsx b/src/components/commons/tinyEditor/TinyEditorBasic.tsx index 23d3e8cf..c9ac5b71 100644 --- a/src/components/commons/tinyEditor/TinyEditorBasic.tsx +++ b/src/components/commons/tinyEditor/TinyEditorBasic.tsx @@ -1,53 +1,31 @@ import React from 'react'; import { Editor as TinyMCEEditor } from 'tinymce'; import { Editor } from '@tinymce/tinymce-react'; +import { plugins, toolbar } from '@util/tinyMCE/tinyEditorPlugins.init'; interface TinyEditorBasicProps { - editorRef: React.MutableRefObject; initialValue?: string; - onChange: (content: string) => void; + onEditorChange: (content: string, editor: TinyMCEEditor) => void; } -export const TinyEditorBasic = ({ editorRef, initialValue, onChange }: TinyEditorBasicProps) => { - const editorParam = editorRef; +export const TinyEditorBasicComponent = ({ initialValue, onEditorChange }: TinyEditorBasicProps) => { + // 에디터 레퍼런스 + const editorRef = React.useRef(null); + // 에디터 초기 실행 이벤트 + const handleEditorInit = (_: unknown, editor: TinyMCEEditor) => { + editorRef.current = editor; + }; + return ( { - editorParam.current = editor; - }} + onInit={handleEditorInit} initialValue={initialValue} - onEditorChange={(content) => { - onChange?.(content); - }} init={{ height: 500, menubar: false, - plugins: [ - 'advlist', - 'autolink', - 'lists', - 'link', - 'image', - 'charmap', - 'preview', - 'anchor', - 'searchreplace', - 'visualblocks', - 'code', - 'fullscreen', - 'insertdatetime', - 'media', - 'table', - 'code', - 'help', - 'wordcount', - ], - toolbar: - 'undo redo | blocks | ' + - 'bold italic forecolor | alignleft aligncenter ' + - 'alignright alignjustify | bullist numlist outdent indent | link image ' + - 'removeformat | help', + plugins: [...plugins], + toolbar: [...toolbar], automatic_uploads: true, // 드래그앤 드롭 설정 /* URL of our upload hander @@ -90,6 +68,7 @@ export const TinyEditorBasic = ({ editorRef, initialValue, onChange }: TinyEdito }, content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }', }} + onEditorChange={onEditorChange} /> ); }; diff --git a/src/components/molecules/actionIcon/ActionIcon.tsx b/src/components/molecules/actionIcon/ActionIcon.tsx new file mode 100644 index 00000000..8038f3db --- /dev/null +++ b/src/components/molecules/actionIcon/ActionIcon.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import clsN from 'classnames'; +import styles from './styles/ActionIcon.module.scss'; + +interface ActionIconProps { + // file Index Id + id?: string | number; + // input Reference + inputRef?: React.Ref | ((el: HTMLInputElement | null) => void); + // allowed FileType + accept?: string; + // styles + style?: React.CSSProperties; + // Image Upload Call Back Event + onImageLoad?: (imageUrl: string, id?: string | number) => void; + // Component Root className + className?: string; + // Component Visibility + disabled?: boolean; +} + +export const ActionIcon = ({ id, inputRef, accept, style, onImageLoad, className, disabled }: ActionIconProps) => { + // file Change Handler + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + // new FileReader Obj : creates the filereader + const reader = new FileReader(); + reader.onload = (e) => { + // file select phase + const imageUrl = e.target?.result as string; + if (onImageLoad) { + // if has CallBack fcn + onImageLoad(imageUrl, id); // call back + } + }; + // encode as URL + reader.readAsDataURL(file); + } + // eslint-disable-next-line no-param-reassign + event.target.value = ''; // reset value of event (event: input) + }; + + return ( + } + onChange={handleFileChange} + /> + ); +}; diff --git a/src/components/molecules/actionIcon/styles/ActionIcon.module.scss b/src/components/molecules/actionIcon/styles/ActionIcon.module.scss new file mode 100644 index 00000000..0113ade5 --- /dev/null +++ b/src/components/molecules/actionIcon/styles/ActionIcon.module.scss @@ -0,0 +1,5 @@ +@import "./index"; + +.action-input{ + display: none; +} \ No newline at end of file diff --git a/src/components/molecules/actionIcon/styles/index.scss b/src/components/molecules/actionIcon/styles/index.scss new file mode 100644 index 00000000..16a66a97 --- /dev/null +++ b/src/components/molecules/actionIcon/styles/index.scss @@ -0,0 +1 @@ +@import 'src/styles/scss/index'; \ No newline at end of file diff --git a/src/components/molecules/admin/notice/collapseForm/CollapseForm.tsx b/src/components/molecules/admin/notice/collapseForm/CollapseForm.tsx index d02f8b8d..0a1dc283 100644 --- a/src/components/molecules/admin/notice/collapseForm/CollapseForm.tsx +++ b/src/components/molecules/admin/notice/collapseForm/CollapseForm.tsx @@ -19,11 +19,22 @@ interface CollapseFormProps { onClick: (e: React.MouseEvent) => void; } export const CollapseForm = ({ ...props }: CollapseFormProps) => { + /* + * React.useState 의 경우 컴포넌트가 많을시 성능이 하향되지만 한번 테스트해봄 + * */ + // 상태 + const [popState, setPopState] = React.useState(false); + + const handleSetClose = () => { + setPopState(false); + }; + return ( ) => void; + popoverProps: { + popstate: boolean; + onClose: () => void; + }; } export const CollapseTitle = ({ ...props }: CollapseTitleProps) => { + const { popoverProps } = props; return ( ) => void; + onClose: () => void; + popstate: boolean; + innerContent?: React.ReactNode; } export const TRowTitleArea = ({ ...props }: TRowTitleProps) => { - const { stackClsN, titleClsN, IconClsN } = props.tRowClassNames; + const { stackClsN, titleClsN } = props.tRowClassNames; + + const [anchorEl, setAnchorEl] = React.useState(null); + + const handleClick = (e: React.MouseEvent) => { + setAnchorEl(e.currentTarget); + props.onClick(e); + }; + const handleClose = () => { + setAnchorEl(null); + props.onClose(); + }; + return ( - - - + , + className: clsN(styles['popover-button__icon']), + onClick: handleClick, + }} + popoverProps={{ + anchorEl, + open: props.popstate, + classes: { root: '', paper: '' }, + anchorOrigin: { horizontal: 'right', vertical: 'bottom' }, + onClose: handleClose, + children: props.innerContent, + }} + /> ); }; diff --git a/src/components/molecules/admin/notice/collapseForm/tRowTitle/styles/TRowTitle.module.scss b/src/components/molecules/admin/notice/collapseForm/tRowTitle/styles/TRowTitle.module.scss index 7eb5f5d8..615620b6 100644 --- a/src/components/molecules/admin/notice/collapseForm/tRowTitle/styles/TRowTitle.module.scss +++ b/src/components/molecules/admin/notice/collapseForm/tRowTitle/styles/TRowTitle.module.scss @@ -1 +1,9 @@ -@import 'index'; \ No newline at end of file +@import 'index'; + + +// popoverButton Root +.popover-button{ + &__icon{ + transform: scale(0.8); + } +} \ No newline at end of file diff --git a/src/components/molecules/admin/product/component/productDetail/ProductDetail.tsx b/src/components/molecules/admin/product/component/productDetail/ProductDetail.tsx new file mode 100644 index 00000000..de9bd947 --- /dev/null +++ b/src/components/molecules/admin/product/component/productDetail/ProductDetail.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Box, Button } from '@mui/material'; +import Text from '@atoms/text/Text'; +import styles from './styles/ProductDetail.module.scss'; + +interface ProductDetailProps { + // 상품 등록 상태 + state: boolean; + // 상품 클릭 이벤트 + onClick: () => void; +} + +export const ProductDetail = ({ ...props }: ProductDetailProps) => { + const [context, setContext] = React.useState(''); + React.useEffect(() => { + if (props.state) { + setContext('작성된 상세 설명이 존재합니다.'); + } + if (!props.state) { + setContext('상세 설명이 존재하지 않습니다.'); + } + }, [props.state]); + return ( + + + + + ); +}; diff --git a/src/components/molecules/admin/product/component/productDetail/styles/ProductDetail.module.scss b/src/components/molecules/admin/product/component/productDetail/styles/ProductDetail.module.scss new file mode 100644 index 00000000..cd4c9d34 --- /dev/null +++ b/src/components/molecules/admin/product/component/productDetail/styles/ProductDetail.module.scss @@ -0,0 +1,45 @@ +@import "index"; + +.text{ + padding: 26px 0 16px; + text-align: center; + @include font-size(headline2); +} +.button{ + @include font-size(headline3); + width: 200px; +} +// 300px ~ +@include media-breakpoint-up(xxs) { + +} + +// 360px ~ +@include media-breakpoint-up(xs) { + +} + +// 480px ~ +@include media-breakpoint-up(sm) { + +} + +// 760px ~ +@include media-breakpoint-up(md) { + +} + +// 980px ~ +@include media-breakpoint-up(lg) { + +} + +// 1080px ~ +@include media-breakpoint-up(xl) { + +} + +// 1400px ~ +@include media-breakpoint-up(xxl) { + +} \ No newline at end of file diff --git a/src/components/molecules/admin/product/component/productDetail/styles/index.scss b/src/components/molecules/admin/product/component/productDetail/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/molecules/admin/product/component/productDetail/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/molecules/admin/product/register/ProductRegisterButtonGroup.tsx b/src/components/molecules/admin/product/register/ProductRegisterButtonGroup.tsx new file mode 100644 index 00000000..cb5de2ca --- /dev/null +++ b/src/components/molecules/admin/product/register/ProductRegisterButtonGroup.tsx @@ -0,0 +1,121 @@ +/* eslint-disable */ +import React from 'react'; +import { Button, Stack } from '@mui/material'; +import { AddShoppingCart, ContentPasteSearch, FileOpen } from '@mui/icons-material'; +import Text from '@atoms/text/Text'; +import { ProductEventType } from '@interface/button/admin/product/register/ProductRegisterInterface'; +import clsN from 'classnames'; +import styles from './styles/ProductRegisterButtonGroup.module.scss'; +import { ModalProductRegister } from '@templates/product/register/ModalProductRegister/ModalProductRegister'; +import { useRecoilState } from 'recoil'; +import { productRegisterAtom } from '@recoil/atoms/admin/product/register/ProductRegisterAtom'; +import { INITIAL_PRODUCT_STATE } from '@util/common/admin/product/ProductStateSetup'; + +export const ProductRegisterButtonGroup = () => { + const [productData, setProductData] = useRecoilState(productRegisterAtom); // 상품 정보 전역 상태 사용 + type ButtonItem = { + label: string; + }; + const [modalState, setModalState] = React.useState(false); + + // 버튼 속성 + const buttonItems: ButtonItem[] = [ + { + label: '상품등록', + }, + { + label: '불러오기', + }, + { + label: '템플릿', + }, + ]; + + /* 핸들러 */ + // 모달 반전 핸들러 + const handleModalChange = () => { + setModalState((prev) => !prev); + }; + + // 상품 상세 정보 상태 초기화 + const handleResetProductInfo = () => { + setProductData(INITIAL_PRODUCT_STATE); + }; + + // 버튼 클릭 이벤트 + const handleClick = (event: ProductEventType['event']) => { + switch (event) { + case 'regist': // 등록 + handleModalChange(); // 모달 반전 처리 + handleResetProductInfo(); // 전역상태 리코일 빈 값으로 설정 + break; + case 'load': + // 불러오기 + break; + case 'template': + // 템플릿 불러오기 + break; + } + }; + + // 인덱스를 통해 이벤트 타입 반환 + const getEventType = (index: number): ProductEventType['event'] => { + switch (index) { + case 0: + return 'regist'; // 0 등록 + case 1: + return 'load'; // 1 불러오기 + case 2: + return 'template'; // 2 템플릿 불러오기 + default: + return 'regist'; // 기본 0 등록으로 + } + }; + // 인덱스를 통해 아이콘 반환 + const getEventIcon = (index: number) => { + switch (index) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + default: + return ; + } + }; + + // 버튼 컴포넌트 렌더 + const renderRegistButtons = (items: ButtonItem[]) => + items.map((item, index) => ( + + )); + + return ( + :not(style)': { + margin: 'unset', + marginTop: 'unset', + }, + }} + className={clsN(styles['product-register'])} + direction="row" + gap={4} + alignItems="center" + justifyContent="center" + > + {renderRegistButtons(buttonItems)} + + + ); +}; diff --git a/src/components/molecules/admin/product/register/styles/ProductRegisterButtonGroup.module.scss b/src/components/molecules/admin/product/register/styles/ProductRegisterButtonGroup.module.scss new file mode 100644 index 00000000..ebb9dd35 --- /dev/null +++ b/src/components/molecules/admin/product/register/styles/ProductRegisterButtonGroup.module.scss @@ -0,0 +1,61 @@ +@import "index"; + +.product-register{ + padding: 0 20%; + &__button{ + width: 100%; + aspect-ratio: 1/1; + gap: 12px; + background-color: $background-navy; + border-radius: 4px; + // 호버 및 클릭 스타일 + &:hover{ + background-color: rgb(38, 61, 100); + } + &__icon{ + @include font-size(headline1); + + } + &__label{ + @include font-size(subtitle1); + } + &__icon, &__label{ + color: $color-text-white; + } + } +} + +// 300px ~ +@include media-breakpoint-up(xxs) { + +} + +// 360px ~ +@include media-breakpoint-up(xs) { + +} + +// 480px ~ +@include media-breakpoint-up(sm) { + +} + +// 760px ~ +@include media-breakpoint-up(md) { + +} + +// 980px ~ +@include media-breakpoint-up(lg) { + +} + +// 1080px ~ +@include media-breakpoint-up(xl) { + +} + +// 1400px ~ +@include media-breakpoint-up(xxl) { + +} \ No newline at end of file diff --git a/src/components/molecules/admin/product/register/styles/index.scss b/src/components/molecules/admin/product/register/styles/index.scss new file mode 100644 index 00000000..a2b479d4 --- /dev/null +++ b/src/components/molecules/admin/product/register/styles/index.scss @@ -0,0 +1 @@ +@import "../../../../../../styles/scss/index"; \ No newline at end of file diff --git a/src/components/molecules/button/buttonGroup/ButtonGroup.tsx b/src/components/molecules/button/buttonGroup/ButtonGroup.tsx index 5370f40f..64745a8a 100644 --- a/src/components/molecules/button/buttonGroup/ButtonGroup.tsx +++ b/src/components/molecules/button/buttonGroup/ButtonGroup.tsx @@ -3,32 +3,26 @@ import { Box } from '@mui/material'; import PropTypes from 'prop-types'; import clsN from 'classnames'; import { ButtonProps } from '@atoms/button/Button'; -import {IconBtnProps} from '@molecules/button/iconButton/IconButton'; +import { IconBtnProps } from '@molecules/button/iconButton/IconButton'; import { ImageBtnProps } from '@molecules/button/imageButton/ImageButton'; import style from './style/ButtonGroup.module.scss'; - interface BtnGroupProps { wrapperClsN?: string; - buttons: React.ReactElement[]; + buttons: React.ReactElement[]; } -const ButtonGroup = ( - { - wrapperClsN, - buttons - }:BtnGroupProps) => { - - return( - +export const ButtonGroup = ({ wrapperClsN, buttons }: BtnGroupProps) => { + return ( + {buttons} - ) -} + ); +}; ButtonGroup.propTypes = { wrapperClsN: PropTypes.string, buttons: PropTypes.arrayOf(PropTypes.element.isRequired).isRequired, -} +}; ButtonGroup.defaultProps = { - wrapperClsN: `${style.btnWrapper}` -} \ No newline at end of file + wrapperClsN: `${style.btnWrapper}`, +}; diff --git a/src/components/molecules/button/iconButton/IconButton.tsx b/src/components/molecules/button/iconButton/IconButton.tsx index 3a7bbc34..f0406ea9 100644 --- a/src/components/molecules/button/iconButton/IconButton.tsx +++ b/src/components/molecules/button/iconButton/IconButton.tsx @@ -19,10 +19,5 @@ const IconButton = ({ className, iconClsN, icon, fontSize, onClick }: IconBtnPro ); }; -IconButton.defaultProps = { - className: `${style.btnIcon}`, - iconClsN: `${style.icon}`, - fontSize: 'inherit', - onClick: undefined, -}; + export default IconButton; diff --git a/src/components/molecules/button/popoverButton/PopoverButton.tsx b/src/components/molecules/button/popoverButton/PopoverButton.tsx new file mode 100644 index 00000000..f6741662 --- /dev/null +++ b/src/components/molecules/button/popoverButton/PopoverButton.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Box, Button as MuiButton, IconButton, IconButtonProps, PopoverProps } from '@mui/material'; +import { PopOver } from '@atoms/popover/PopOver'; +import clsN from 'classnames'; +import styles from './styles/PopoverButton.module.scss'; + +// 원본 버튼 타입 +type ButtonProps = React.ComponentProps; + +/** + * PopoverButton : '버튼' 클릭이벤트를 참조해 해당 컴포넌트 위치로 popover 이벤트를 발생해 특정 컴포넌트 렌더 + */ +interface PopoverButtonProps { + // 버튼 속성 + buttonProps: { + children: React.ReactNode; // 자식 요소 + className?: string; // 클래스명(선택적) + onClick: (e: React.MouseEvent) => void; // 버튼 클릭 이벤트 + isIconButton?: boolean; // 아이콘 버튼 여부(조건부 렌더링) + iconButtonProps?: Omit; // 아이콘 버튼 특정 속성(onclick)을 제외한 나머지 속성을 포함하여 타입 생성 + }; + // popover 속성 + popoverProps: { + children: React.ReactNode; // 자식 요소 + open: boolean; // popover 활성화 여부 상태 + onClose: () => void; // popover 소멸 이벤트 + /** + * 클래스명 객체 + * root: root 클래스명 + * paper : popover 컨텐츠를 감싸는 클래스명 + */ + classes?: { + root: string; + paper: string; + }; + anchorEl: HTMLElement | null; + // popover 가 표시될 좌표값 + anchorOrigin?: PopoverProps['anchorOrigin']; + }; +} + +export const PopoverButton = ({ buttonProps, popoverProps }: PopoverButtonProps) => { + // 앵커 선택된 요소 상태 (popover의 위치값이 될 요소) + const [anchorEl, setAnchorEl] = React.useState(null); + + // 버튼 클릭 이벤트 : 해당 버튼의 이벤트를 참고해 앵커를 지정함 + const handleClick = (e: React.MouseEvent) => { + setAnchorEl(e.currentTarget); // 이벤트를 통한 앵커 설정 + buttonProps.onClick(e); // 버튼 속성의 onClick 이벤트 + }; + + // 공통 버튼들의 속성 모음 + const commonButtonProps: ButtonProps = { + onClick: handleClick, // 현재 handleClick 의 이벤트를 실행 + className: buttonProps.className, // 버튼속성의 클래스명 상속 + }; + + // 조건부 렌더링 + const renderButton = () => { + if (buttonProps.isIconButton) { + // 아이콘 버튼 속성일 경우 아이콘 버튼 컴포넌트 반환 + return ( + + {buttonProps.children} + + ); + } + // 이외에는 기본 버튼 반환 + return {buttonProps.children}; + }; + + return ( + + {renderButton()} + { + setAnchorEl(null); + popoverProps.onClose(); + }} + > + {popoverProps.children} + + + ); +}; diff --git a/src/components/molecules/button/popoverButton/styles/PopoverButton.module.scss b/src/components/molecules/button/popoverButton/styles/PopoverButton.module.scss new file mode 100644 index 00000000..58b12691 --- /dev/null +++ b/src/components/molecules/button/popoverButton/styles/PopoverButton.module.scss @@ -0,0 +1,15 @@ +@import 'index'; + +// root +.root{ + position: relative; + + // button + &__button{ + position: relative; + } + // popover + &__popover{ + + } +} \ No newline at end of file diff --git a/src/components/molecules/button/popoverButton/styles/index.scss b/src/components/molecules/button/popoverButton/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/molecules/button/popoverButton/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/molecules/collapseBody/contentRow/ContextRow.tsx b/src/components/molecules/collapseBody/contentRow/ContextRow.tsx new file mode 100644 index 00000000..bbb30a94 --- /dev/null +++ b/src/components/molecules/collapseBody/contentRow/ContextRow.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Collapse, TableCell, TableRow } from '@mui/material'; +import clsN from 'classnames'; +import styles from './styles/ContextRow.module.scss'; + +interface ContextRowProps { + className?: string; // 클래스명 + cellClsN?: string; // cell 클래스명 + isOpen?: boolean; // collapse 열림 상태 + content: React.ReactNode; // 컨텐츠 + colSpan?: number; // 합칠 셀 수량 +} +export const ContextRow = ({ className, cellClsN, isOpen, content, colSpan }: ContextRowProps) => { + const cellClassNames = clsN(cellClsN, styles['table-row__cell'], { + [styles['table-row__cell--on']]: isOpen, + [styles['table-row__cell--off']]: !isOpen, + }); // 테이블 셀 클래스명 isOpen 상태에 따라 변경 + + return ( + + + {content} + + + ); +}; +ContextRow.defaultProps = { + className: styles['table-row'], + cellClsN: styles['table-ror__cell'], +}; diff --git a/src/components/molecules/collapseBody/contentRow/styles/ContextRow.module.scss b/src/components/molecules/collapseBody/contentRow/styles/ContextRow.module.scss new file mode 100644 index 00000000..6d89cecb --- /dev/null +++ b/src/components/molecules/collapseBody/contentRow/styles/ContextRow.module.scss @@ -0,0 +1,16 @@ +@import "../../../../../index.css"; + +// root +.table-row{ + // cell + &__cell{ + // 펼쳐짐 + &--on{ + padding: inherit; + } + // 펼쳐지지 않음 + &--off{ + padding: 0; + } + } +} \ No newline at end of file diff --git a/src/components/molecules/collapseBody/contentRow/styles/index.scss b/src/components/molecules/collapseBody/contentRow/styles/index.scss new file mode 100644 index 00000000..e33b984f --- /dev/null +++ b/src/components/molecules/collapseBody/contentRow/styles/index.scss @@ -0,0 +1 @@ +@import "../../../../../styles/scss/index"; \ No newline at end of file diff --git a/src/components/molecules/collapseBody/titleRow/TitleRow.tsx b/src/components/molecules/collapseBody/titleRow/TitleRow.tsx new file mode 100644 index 00000000..b08a0a31 --- /dev/null +++ b/src/components/molecules/collapseBody/titleRow/TitleRow.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { TableCell, TableRow } from '@mui/material'; +import { ExpandLess, ExpandMore } from '@mui/icons-material'; +import IconButton from '@molecules/button/iconButton/IconButton'; +import clsN from 'classnames'; +import styles from './styles/TitleRow.module.scss'; + +interface TitleRowProps { + isOpen: boolean; // 콜렙스 펼침 체크 + className?: string; // 클래스명 + cellClsN?: string; // 테이블셀 클래스명 + data: React.ReactNode[]; + align: 'inherit' | 'left' | 'center' | 'right' | 'justify'; + onClick: () => void; // 콜랩스 클릭 이벤트 +} + +export const TitleRow = ({ isOpen, className, cellClsN, onClick, data, align }: TitleRowProps) => { + return ( + + + : } onClick={onClick} /> + + {data.map((item) => ( + + {item} + + ))} + + ); +}; diff --git a/src/components/molecules/collapseBody/titleRow/styles/TitleRow.module.scss b/src/components/molecules/collapseBody/titleRow/styles/TitleRow.module.scss new file mode 100644 index 00000000..44b6dc90 --- /dev/null +++ b/src/components/molecules/collapseBody/titleRow/styles/TitleRow.module.scss @@ -0,0 +1,10 @@ +@import "../../../../../index.css"; + +.table-row{ + &__cell{ + &:last-of-type{ + text-wrap: nowrap; + text-overflow: ellipsis; + } + } +} \ No newline at end of file diff --git a/src/components/molecules/collapseBody/titleRow/styles/index.scss b/src/components/molecules/collapseBody/titleRow/styles/index.scss new file mode 100644 index 00000000..e33b984f --- /dev/null +++ b/src/components/molecules/collapseBody/titleRow/styles/index.scss @@ -0,0 +1 @@ +@import "../../../../../styles/scss/index"; \ No newline at end of file diff --git a/src/components/molecules/collapseHeader/CollapseHeader.tsx b/src/components/molecules/collapseHeader/CollapseHeader.tsx new file mode 100644 index 00000000..46fc7b16 --- /dev/null +++ b/src/components/molecules/collapseHeader/CollapseHeader.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { TableCell, TableRow } from '@mui/material'; +import clsN from 'classnames'; +import styles from './styles/CollapseHeader.module.scss'; + +interface CollapseHeaderProps { + tableCells: React.ReactNode; +} + +// 테이블 헤더 영역, CollapseHaderProps[] 들을 관리 +export const CollapseHeader = ({ ...props }: CollapseHeaderProps) => { + return ( + + + {props.tableCells} + + + ); +}; diff --git a/src/components/molecules/collapseHeader/styles/CollapseHeader.module.scss b/src/components/molecules/collapseHeader/styles/CollapseHeader.module.scss new file mode 100644 index 00000000..629fc440 --- /dev/null +++ b/src/components/molecules/collapseHeader/styles/CollapseHeader.module.scss @@ -0,0 +1,10 @@ +@import "../../../../index.css"; + +.table{ + &__head{ + &__cell{ + text-wrap: nowrap; + text-overflow: ellipsis; + } + } +} \ No newline at end of file diff --git a/src/components/molecules/collapseHeader/styles/index.scss b/src/components/molecules/collapseHeader/styles/index.scss new file mode 100644 index 00000000..86e5d894 --- /dev/null +++ b/src/components/molecules/collapseHeader/styles/index.scss @@ -0,0 +1 @@ +@import "../../../../styles/scss/index"; \ No newline at end of file diff --git a/src/components/molecules/dateRange/DateRange.tsx b/src/components/molecules/dateRange/DateRange.tsx index bc770afe..926b6bad 100644 --- a/src/components/molecules/dateRange/DateRange.tsx +++ b/src/components/molecules/dateRange/DateRange.tsx @@ -1,243 +1,215 @@ +/* eslint-disable */ import React from 'react'; import { Dayjs } from 'dayjs'; import 'dayjs/locale/ko'; -import { Paper, Stack } from '@mui/material'; +import { Box, FormControl, InputLabel, Paper, Popover, Stack } from '@mui/material'; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import DatePicker from '@atoms/datePicker/DatePicker'; import Button from '@atoms/button/Button'; import { ArrowDropDown, ArrowDropUp, CalendarMonth } from '@mui/icons-material'; -import { useRecoilState } from 'recoil'; -import { noticesFilterStateAtom } from '@recoil/atoms/admin/inquiry/notices/noticesFilterAtom'; +import { formatDayjs, swapDateCheck } from '@util/dayjs/DayJsUtill'; import clsN from 'classnames'; import styles from './styles/DateRange.module.scss'; interface DateRangeProps { className?: string; // 클래스명 + rootClsN?: string; // root 클래스명 pickerClsN?: string; // 데이터 피커 클래스명 - resetTrigger: boolean; // 리셋 트리거 + fromDate: Dayjs | null; // 시작일 + endDate: Dayjs | null; // 종료일 + onDateChange: (startDate: Date | undefined, endDate: Date | undefined) => void; + onReset: () => void; // 리셋 이벤트 + defaultText: string; // 기본버튼 내용 + inputLabel?: string; + inputLabelId?: string; } -const DateRange = ({ className, pickerClsN, resetTrigger }: DateRangeProps) => { +const DateRange = ({ + className, + rootClsN, + pickerClsN, + fromDate, + endDate, + onDateChange, + onReset, + defaultText, + inputLabel, + inputLabelId, +}: DateRangeProps) => { /* 상태 */ - // 버튼 컨텍스트 - const [btnText, setBtnText] = React.useState('날짜범위'); - + const [anchorEl, setAnchorEl] = React.useState(null); // 현제 포커스된 앵커 // 캘린더 시작일, 종료일 - const [fromD, setFromD] = React.useState(null); - const [endD, setEndD] = React.useState(null); + const [fromDateState, setFromDateState] = React.useState(fromDate); + const [endDateState, setEndDateState] = React.useState(endDate); - // 버튼 토글 상태 - const [isOpen, setIsOpen] = React.useState(false); + const [buttonText, setButtonText] = React.useState(defaultText); - // submit 신호 상태 - const [datePicked, setDatePicked] = React.useState(false); + React.useEffect(() => { + setFromDateState(fromDate); + setEndDateState(endDate); + if (!fromDate && !endDate) { + setButtonText(defaultText); + } + }, [fromDate, endDate, defaultText]); // 상태 동기화 // 부터 ~ 까지에 쓰이는 레이블 const dateLabels = ['from', 'to']; - // 리코일 날짜설정 - const [dayRecoil, setDayRecoil] = useRecoilState(noticesFilterStateAtom); + // 앵커된 요소 존재시 open == true + const open = Boolean(anchorEl); /* 함수 */ - // Dayjs 를 받아 YYYY.MM.DD 형태로 포맷하기 - const formatDayjs = (date: Dayjs | null) => { - return date ? date.format('YYYY.MM.DD') : ''; + // popover 활성화 이벤트 + const handlePopoverClick = (e: React.MouseEvent) => { + setAnchorEl(e.currentTarget); // e 를 통해 선택된 버튼 요소에 앵커 설정 }; - - // Dayjs 타입인 두 값을 비교하여 특정값 배출 (이후 switch 문으로 분기 처리하기) - const compareDate = (date1: Dayjs | null, date2: Dayjs | null): number | null => { - // 인자가 null 이 아닐 경우만 - if (date1 && date2) { - if (date1?.isBefore(date2)) { - // 인자1 이 인자2 보다 작음 (-1) - return -1; - } - if (date1?.isAfter(date2)) { - // 인자1 이 인자2 보다 큼 (+1) - return 1; - } - if (date1?.isSame(date2)) { - // 두 인자값이 같을 경우 (0) - return 0; - } - } else if (date1 == null && date2 == null) { - // 둘다 null 일 경우 - return null; - } - // 인자중 하나라도 null 일 경우 (0) - return 0; + // popover 비활성화 이벤트 + const handlePopoverOff = () => { + setAnchorEl(null); // 앵커값 null }; - // Dayjs 스왑 이벤트 - const fixDateState = (compare: number | null) => { - switch (compare) { - case -1: - // 인자1 이 인자2 보다 작음 - break; - case null: - break; - case 0: { - // 종료일 찾기 // 인자중 하나라도 null 인 상황, 그 중 큰값은 endD 로 정하기 -1 까지 조회하도록 하기 - const tempEndDate = fromD?.isAfter(endD) ? fromD : endD; - // 널 병합 연산자 (??) 를 사용해서 undefined 발생할 경우 방지하기 - const tempStartDate = tempEndDate?.subtract(1, 'day') ?? null; - // 상태 갱신 - setFromD(tempStartDate); - setEndD(tempEndDate); - break; - } - case 1: { - // 인자1 이 인자2 보다 큼 - const tempLastDate = fromD; - setFromD(endD); - setEndD(tempLastDate); - break; - } - default: - break; - } + // 날짜 변경 이벤트 + const handleDateChange = (newFromDate: Dayjs | null, newEndDate: Dayjs | null) => { + const [neatedFromDate, neatedEndDate] = swapDateCheck(newFromDate, newEndDate); + setFromDateState(neatedFromDate); + setEndDateState(neatedEndDate); }; - // 버튼 내용 분기에 따라 번경하기 - const setButtonContext = () => { - // submit true 일 때 아래 분기 실행 - const startDate = formatDayjs(fromD); - const endDate = formatDayjs(endD); - - // 상태값 변경 - setBtnText(`${startDate} ~ ${endDate}`); - // 아톰 내용 변경 + // 날짜 변경 이벤트 : 시작기준 + const handleStartDateChange = (newValue: Dayjs | null) => { + handleDateChange(newValue, endDateState); }; + const handleEndDateChange = (newValue: Dayjs | null) => { + handleDateChange(fromDateState, newValue); + }; + // 날짜 변경 이벤트 : 종료기준 - // Dayjs 라이브러리를 Date 타입에 맞게 변환 - - // 현재 날짜를 리코일에 갱신 - // TODO : 리코일 혹은 상태 갱신할때 useMemo 활용하기 - const updateDateRecoil = React.useMemo(() => { - return () => { - // 시작일 - const firstDate = fromD?.toDate(); - // 종료일 - const lastDate = endD?.toDate(); - // 리코일 업데이트 - setDayRecoil((formState) => ({ - ...formState, - startDate: firstDate, - endDate: lastDate, - })); - }; - }, [fromD, endD, setDayRecoil]); - - // 날짜 범위 컴포넌트 열기 - const handleDateOpen = () => { - setIsOpen(!isOpen); + const dateContextChange = () => { + if (!fromDateState || !endDateState) { + // 둘중하나 null 일 경우 + setButtonText(defaultText); + return; + } + setButtonText(`${formatDayjs(fromDateState)} ~ ${formatDayjs(endDateState)}`); }; + // 날짜 데이터 확정 이벤트 const handleDateSubmit = () => { - console.log('DateSubmit!!!'); - // 아래 수행할 함수 실행 - setDatePicked((prevState) => !prevState); + onDateChange(fromDateState?.toDate(), endDateState?.toDate()); + dateContextChange(); + handlePopoverOff(); // popover 이벤트 종료 }; // 날짜 초기화 이벤트 const resetDateRange = () => { - setFromD(null); - setEndD(null); - setBtnText('날짜범위'); - setIsOpen(false); - setDatePicked(false); - setDayRecoil((prevVal) => ({ - ...prevVal, - startDate: undefined, - endDate: undefined, - })); + setFromDateState(null); + setEndDateState(null); + setButtonText(defaultText); + onDateChange(undefined, undefined); + onReset(); }; /* JSX 컴포넌트 */ // submit 버튼 const dateSubmitBtn = ( - + ); + // reset 버튼 + const dateResetIconButton = ( + ); // 받은 인자로 DatePicker 컴포넌트 반환 const DatePickerRender = ( - + { - setFromD(newValue); + className={clsN(styles['date-picker-wrapper__picker'], pickerClsN)} + value={fromDateState} + onChange={handleStartDateChange} + classes={{ + root: styles[''], }} /> -

+

setEndD(newValue)} + className={clsN(styles['date-picker-wrapper__picker'], pickerClsN)} + value={endDateState} + onChange={handleEndDateChange} /> ); // date picker 컴포넌트를 지역에 맞게 양식을 수정하고 배포 - const DatePickerProvider = ( - - - {DatePickerRender} - {dateSubmitBtn} - - + const DatePickerPopover = ( + + + + {DatePickerRender} + + {dateResetIconButton} + {dateSubmitBtn} + + + + ); // 날짜조회 드롭다운 버튼 컴포넌트 const DateRangeBtn = ( ); - React.useEffect(() => { - const result = compareDate(fromD, endD); - if (result == null) { - setBtnText('날짜범위'); - } else { - fixDateState(result); - setButtonContext(); - updateDateRecoil(); - } - setIsOpen(false); - setDatePicked(false); - console.log(`current day from : ${dayRecoil.startDate}, current day end ${dayRecoil.endDate}`); - }, [datePicked]); - - React.useEffect(() => { - if (resetTrigger) { - resetDateRange(); - } - }, [resetTrigger, setDayRecoil]); - return ( - - {DateRangeBtn} - {DatePickerProvider} - + + + + {inputLabel} + + {DateRangeBtn} + + {DatePickerPopover} + ); }; DateRange.defaultProps = { diff --git a/src/components/molecules/dateRange/styles/DateRange.module.scss b/src/components/molecules/dateRange/styles/DateRange.module.scss index 3eb76f64..089b1a8f 100644 --- a/src/components/molecules/dateRange/styles/DateRange.module.scss +++ b/src/components/molecules/dateRange/styles/DateRange.module.scss @@ -2,63 +2,145 @@ // 실질적인 root .date-range-stack{ - z-index: 50; - position: relative; - height: 100%; - // 130px ~ 260px + + &__label{ + padding-top: 20px; + width: 100%; + } &__button{ - position: relative; - width: fit-content; - height: 100%; text-wrap: nowrap; text-overflow: ellipsis; - &--on{ - - } - &--off{ - } } } .date-range { - visibility: hidden; padding: 8px; align-items: center; - position: absolute; &__picker { - width: 180px; + flex-grow: 1; &:first-child { //margin-right: 8px; } &:last-child { //margin-left: 8px; } - // 구분선 - &__separator{ - display: block; - margin: 0 8px; - width: 8px; - height: 1px; - background-color: $background-dimgray; - border-radius: 2px; - } + } - &--open{ - visibility: visible; - position: absolute; - top: calc(100% + 8px); + &__confirm{ // stack root + width: 100%; } // 확인 버튼 - &__btn-submit{ - margin-left: 8px; + &__button{ + &-clear{ + + } + &-submit { + + } + } +} +.background{ + width: 100%; +} +.date-picker-wrapper{ + width: 100%; + :global(.MuiFormControl-root){ // global 선택자를 통해 일치하는 클래스 명에 접속 + width: 100%; } - &__btn-reset{ + &__picker{ + } + &__seperator{ + display: block; + margin: 0 8px; + width: 8px; + height: 1px; + background-color: $background-dimgray; + border-radius: 2px; + } +} +.popover{ + width: 100%; + &__paper{ + width: 100%; } } +// 300px~ +@include media-breakpoint-up(xxs) { + .date-range{ + &__confirm{ // stack root + width: 100%; + padding: 0 4px; + } + // 확인 버튼 + &__button{ + flex-grow: 1; + &-clear{ -.background{ - box-shadow: none; + } + &-submit { + + } + } + } + .background{ + } + .date-range{ + flex-direction: column; + gap: 16px; + } + .date-picker-wrapper{ // 달력 컴포넌트 감싸는 영역 + width: 100%; + flex-direction: column; + gap: 16px; + padding: 16px 4px 0; + &__picker{ + flex-grow: 1; + margin: inherit; + width: fit-content; + } + &__seperator{ + flex-grow: 1; + } + } + .popover{ // popover 컴포넌트 + width: 100%; + + } +} +// 768px ~ 테블릿 +@include media-breakpoint-up(md) { + .popover{ + width: inherit; + &__paper{ + width: inherit; + } + } + .date-picker-wrapper{ + width: inherit; + padding:0; + flex-direction: row; + gap: 0; + :global(.MuiFormControl-root){ // global 선택자를 통해 일치하는 클래스 명에 접속 + width: 200px; + } + &__picker{ + width: 200px; + } + &__seperator{; + } + } + .date-range{ + flex-direction: row; + &__confirm{ // stack root + width: inherit; + padding: 0 4px; + } + // 확인 버튼 + &__button{ + flex-grow: inherit; + } + } } \ No newline at end of file diff --git a/src/components/molecules/draggableList/DraggableList.tsx b/src/components/molecules/draggableList/DraggableList.tsx new file mode 100644 index 00000000..110cccf6 --- /dev/null +++ b/src/components/molecules/draggableList/DraggableList.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { + closestCorners, + DndContext, + DragEndEvent, + DragStartEvent, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { + arrayMove, + horizontalListSortingStrategy, + SortableContext, + sortableKeyboardCoordinates, +} from '@dnd-kit/sortable'; +import { Box } from '@mui/material'; +import { SortableItem } from '@atoms/sortableItem/SortableItem'; + +export interface DraggableItmes { + id: string; + content: React.ReactNode; +} + +interface DraggableListProps { + items: DraggableItmes[]; +} + +export const DraggableList = ({ ...props }: DraggableListProps) => { + // active ID state + const [activeId, setActiveId] = React.useState(null); + // sorted Item Lists + const [dragItems, setDragItems] = React.useState(props.items); + + // useSensors for input : keybord and mouse sensors + const sensors = useSensors( + useSensor(PointerSensor), // enable to mouse drag + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), // enable to keyboard drag + ); + + // when drag event starts + const handleDragStart = (e: DragStartEvent) => { + // find actived id and set State + setActiveId(e.active.id.toString()); + }; + + // when drag event ends + const handleDragEnd = (e: DragEndEvent) => { + // get object atrributes from e + const { active, over } = e; + + if (over && active.id !== over.id) { + setDragItems((items) => { + // drag start point is old index + const oldIndex = items.findIndex((item) => item.id === activeId); + // now drag end point is new index + const newIndex = items.findIndex((item) => item.id === over.id); + + // arrange item list + return arrayMove(items, oldIndex, newIndex); + }); + + // id is currently not active + setActiveId(null); + } + }; + + return ( + + + + {dragItems.map((item) => ( + + ))} + + + + ); +}; diff --git a/src/components/organisms/admin/searchBar/styles/SearchBar.module.scss b/src/components/molecules/draggableList/styles/DraggableList.module.scss similarity index 100% rename from src/components/organisms/admin/searchBar/styles/SearchBar.module.scss rename to src/components/molecules/draggableList/styles/DraggableList.module.scss diff --git a/src/components/molecules/draggableList/styles/index.scss b/src/components/molecules/draggableList/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/molecules/draggableList/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/molecules/imageBox/ImageBox.tsx b/src/components/molecules/imageBox/ImageBox.tsx new file mode 100644 index 00000000..0e15e250 --- /dev/null +++ b/src/components/molecules/imageBox/ImageBox.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { ImageListItem, ImageListItemBar } from '@mui/material'; +import { ImageBoxPropsInterface } from '@interface/image/ImageInterface'; +import clsN from 'classnames'; +import styles from './styles/ImageBox.module.scss'; + +interface ImageBoxProps extends ImageBoxPropsInterface { + actionIcon?: React.ReactNode; + position?: 'bottom' | 'top' | 'below'; + actionPosition?: 'left' | 'right'; + titleBarClsN?: string; +} + +export const ImageBox = ({ ...props }: ImageBoxProps) => { + return ( + + {props.alt} + + + ); +}; diff --git a/src/components/molecules/imageBox/styles/ImageBox.module.scss b/src/components/molecules/imageBox/styles/ImageBox.module.scss new file mode 100644 index 00000000..f4e01b2c --- /dev/null +++ b/src/components/molecules/imageBox/styles/ImageBox.module.scss @@ -0,0 +1,26 @@ +@import "./index"; + +.imagebox{ + aspect-ratio: 1/1; + box-sizing: border-box; + border: 1px solid $border-default-color; + border-radius: 4px; + &__img{ + &-invisible{ + @include alt-hidden; + } + } + &__bar{ + background-color: inherit; + &__title{ + + } + } + &__button{ + &--svg{ + // transparent setup + filter: brightness(200%) contrast(150%); + color: transparentize($background-dimgray, 0.4); + } + } +} \ No newline at end of file diff --git a/src/components/molecules/imageBox/styles/index.scss b/src/components/molecules/imageBox/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/molecules/imageBox/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/molecules/searchBar/SearchBar.tsx b/src/components/molecules/searchBar/SearchBar.tsx index de466193..2b53a81d 100644 --- a/src/components/molecules/searchBar/SearchBar.tsx +++ b/src/components/molecules/searchBar/SearchBar.tsx @@ -16,6 +16,8 @@ interface SearchBarProps { } const SearchBar = ({ wrapperClsN, inputClsN, placeholder, onChange, label, inputVal }: SearchBarProps) => { + // 상태 관리 + /* 함수 */ // form 전송 이벤트 방지 const handleSubmit = (e: React.FormEvent) => { diff --git a/src/components/molecules/searchBar/styles/SearchBar.module.scss b/src/components/molecules/searchBar/styles/SearchBar.module.scss index 9fc0df2a..4b10b98e 100644 --- a/src/components/molecules/searchBar/styles/SearchBar.module.scss +++ b/src/components/molecules/searchBar/styles/SearchBar.module.scss @@ -11,4 +11,39 @@ &__icon{ } +} + +// 300px ~ +@include media-breakpoint-up(xxs) { + +} + +// 360px ~ +@include media-breakpoint-up(xs) { + +} + +// 480px ~ +@include media-breakpoint-up(sm) { + +} + +// 760px ~ +@include media-breakpoint-up(md) { + +} + +// 980px ~ +@include media-breakpoint-up(lg) { + +} + +// 1080px ~ +@include media-breakpoint-up(xl) { + +} + +// 1400px ~ +@include media-breakpoint-up(xxl) { + } \ No newline at end of file diff --git a/src/components/molecules/selectBox/SelectBox.tsx b/src/components/molecules/selectBox/SelectBox.tsx index 7701b088..b3719292 100644 --- a/src/components/molecules/selectBox/SelectBox.tsx +++ b/src/components/molecules/selectBox/SelectBox.tsx @@ -57,7 +57,7 @@ export const SelectBox = ({ /* JSX 컴포넌트 */ // inputLabel 컴포넌트 const iptLabel = ( - + {inputLabel} ); @@ -75,6 +75,8 @@ export const SelectBox = ({ root: clsN(styles['select-root']), }} onChange={handleMenuChange} + notched + label={inputLabel} > {menuMapping} diff --git a/src/components/molecules/selectBox/styles/SelectBox.module.scss b/src/components/molecules/selectBox/styles/SelectBox.module.scss index 9a977e4e..2ae234d3 100644 --- a/src/components/molecules/selectBox/styles/SelectBox.module.scss +++ b/src/components/molecules/selectBox/styles/SelectBox.module.scss @@ -9,13 +9,15 @@ max-height: inherit; padding: 0; height: 40px; + min-width: 88px; } // form Root .select-from{ // label &__label{ - + background-color: white; + padding: 0 4px; } // select box &__select{ diff --git a/src/components/molecules/userLoginPlace/UserLoginPlace.tsx b/src/components/molecules/userLoginPlace/UserLoginPlace.tsx index 160a24ed..be3a0f3e 100644 --- a/src/components/molecules/userLoginPlace/UserLoginPlace.tsx +++ b/src/components/molecules/userLoginPlace/UserLoginPlace.tsx @@ -1,12 +1,13 @@ -/* eslint-disable */ import React from 'react'; import IconButton from '@molecules/button/iconButton/IconButton'; import Button from '@atoms/button/Button'; import PropTypes from 'prop-types'; +import { Stack } from '@mui/material'; +import { Chat, Notifications, DragHandle } from '@mui/icons-material'; +import { useSetRecoilState } from 'recoil'; +import { drawerAdminNavAtom } from '@recoil/atoms/admin/drawer/drawerAdminNavAtom'; import clsN from 'classnames'; import styles from './styles/UserLoginPlace.module.scss'; -import { Stack } from '@mui/material'; -import { Chat, Notifications } from '@mui/icons-material'; interface UserLoginPlaceProps { // 클래스명 @@ -18,41 +19,52 @@ interface UserLoginPlaceProps { // 로그인 버튼 클래스 명 loginClsN?: string; } -const UserLoginPlace = ( - { - className, - notifyClsN, - chatClsN, - loginClsN - }:UserLoginPlaceProps -) => { +const UserLoginPlace = ({ className, notifyClsN, chatClsN, loginClsN }: UserLoginPlaceProps) => { /* await 로 데이터 받고 정리하기 */ // 상태 + const setDrawState = useSetRecoilState(drawerAdminNavAtom); // 메뉴 펼치기 상태 + + const handleDrawMenu = () => { + setDrawState((prev) => !prev); + }; + /* 알림 */ /* 문자 */ /* */ - return( - - } /> - } /> - + /* TODO 현재 컴포넌트에서 GQL 을 통해 알림이나 문의내역 알림 시스템 설계 */ + + /* TODO : 관리자 로그인 버튼이 아닌 로그아웃 하기 + 로그인 시 보여줄 정보 설계하기 */ + + return ( + + } + className={clsN(styles.stack__icon, styles.stack__drawer)} + onClick={handleDrawMenu} + /> + } + /> + } /> + - ) -} + ); +}; UserLoginPlace.propTypes = { className: PropTypes.string, notifyClsN: PropTypes.string, chatClsN: PropTypes.string, loginClsN: PropTypes.string, -} +}; UserLoginPlace.defaultProps = { className: styles.stack, notifyClsN: styles.stack__notify, chatClsN: styles.stack__chat, loginClsN: styles.stack__login, -} -export default UserLoginPlace; \ No newline at end of file +}; +export default UserLoginPlace; diff --git a/src/components/molecules/userLoginPlace/styles/UserLoginPlace.module.scss b/src/components/molecules/userLoginPlace/styles/UserLoginPlace.module.scss index 914d1f1a..480216d2 100644 --- a/src/components/molecules/userLoginPlace/styles/UserLoginPlace.module.scss +++ b/src/components/molecules/userLoginPlace/styles/UserLoginPlace.module.scss @@ -1,6 +1,17 @@ @import "index"; .stack{ + width: 100%; + &__icon{ + color:white; + } + &__drawer{ + box-sizing: border-box; + margin-right: auto; + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.09); + aspect-ratio: 1/1; + } &__notify{ } diff --git a/src/components/organisms/admin/asideNav/AsideNav.tsx b/src/components/organisms/admin/asideNav/AsideNav.tsx index a480dd1c..ea001a30 100644 --- a/src/components/organisms/admin/asideNav/AsideNav.tsx +++ b/src/components/organisms/admin/asideNav/AsideNav.tsx @@ -1,5 +1,7 @@ import React from 'react'; -import { Box, List } from '@mui/material'; +import { Box, Drawer, List } from '@mui/material'; +import { useRecoilState } from 'recoil'; +import { drawerAdminNavAtom } from '@recoil/atoms/admin/drawer/drawerAdminNavAtom'; import clsN from 'classnames'; import styles from './styles/AsideNav.module.scss'; @@ -20,35 +22,32 @@ interface AsideNavProps { hoverClsN?: string; } -export const AsideNav = ( - { - className, - listWrapperClsN, - items, - itemFactor, - hoverClsN - }: AsideNavProps, -) => { +export const AsideNav = ({ className, listWrapperClsN, items, itemFactor, hoverClsN }: AsideNavProps) => { + /* TODO GQL 로 라우트할 아이템 받고 렌더하기 */ + + const [drawState, setDrawState] = useRecoilState(drawerAdminNavAtom); // 메뉴 펼치기 상태 + // 상수 처리 const ItemTemp = items.map((item: T, idx: number) => { // 제너릭 타입 받아서 처리 return itemFactor(item, idx); }); return ( - -

- - DeamHome - -

- - {ItemTemp} -
- - ); + setDrawState(false)}> + +

+ DeamHome +

+ + {ItemTemp} +
+ + + + ); }; AsideNav.defaultProps = { className: styles.nav, listWrapperClsN: styles.nav__ul, - hoverClsN : styles.hover, -} + hoverClsN: styles.hover, +}; diff --git a/src/components/organisms/admin/asideNav/styles/AsideNav.module.scss b/src/components/organisms/admin/asideNav/styles/AsideNav.module.scss index 2deea501..87d5297f 100644 --- a/src/components/organisms/admin/asideNav/styles/AsideNav.module.scss +++ b/src/components/organisms/admin/asideNav/styles/AsideNav.module.scss @@ -1,4 +1,6 @@ /* 헤더 높이 전역변수 정하기 */ +@import "index"; + .nav{ box-sizing: border-box; &__logo{ @@ -21,4 +23,36 @@ // 애니메이션 .hover{ +} + +// 300px ~ +@include media-breakpoint-up(xxs) { + +} + +// 360px ~ +@include media-breakpoint-up(xs) { + +} + +// 480px ~ +@include media-breakpoint-up(sm) { + +} + +// 768px ~ +@include media-breakpoint-up(md) { + +} +// 980px +@include media-breakpoint-up(lg){ + +} +// 1080px +@include media-breakpoint-up(xl){ + +} +// 1400px +@include media-breakpoint-up(xxl){ + } \ No newline at end of file diff --git a/src/components/organisms/admin/buttonGroup/ButtonGroup.tsx b/src/components/organisms/admin/buttonGroup/ButtonGroup.tsx index 324a9c8f..e3aeae3a 100644 --- a/src/components/organisms/admin/buttonGroup/ButtonGroup.tsx +++ b/src/components/organisms/admin/buttonGroup/ButtonGroup.tsx @@ -5,6 +5,7 @@ import clsN from 'classnames'; // 여러 버튼을 받음, 그리고 각 버튼마다 기능이 있고, 조건부로 어떤버튼은 보이거나 보여주지 않을 수 있음 interface ButtonGroupProps { + rootPaperClsN?: string; // 버튼들을 감싸는 부모 클래스 명 buttonItems: ButtonProps[]; // 버튼들 객체 배열로 받음 {}, {} } @@ -15,8 +16,11 @@ export const ButtonGroup = ({ ...props }: ButtonGroupProps) => { }); return ( - + {buttonProvoder} ); }; +ButtonGroup.defaultProps = { + rootPaperClsN: undefined, +}; diff --git a/src/components/organisms/admin/filteredList/FilteredList.tsx b/src/components/organisms/admin/filteredList/FilteredList.tsx new file mode 100644 index 00000000..4f725ecf --- /dev/null +++ b/src/components/organisms/admin/filteredList/FilteredList.tsx @@ -0,0 +1,94 @@ +/* eslint-disable */ +import React from 'react'; +import { Box, Divider, Pagination, Paper } from '@mui/material'; +import { Heading } from '@molecules/admin/layout/heading/Heading'; +import { EventPageRequest } from '@interface/evnet/filter/EventFilterType'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { eventIdAtom } from '@recoil/atoms/admin/inquiry/notices/noticesFilterAtom'; +import clsN from 'classnames'; +import styles from './styles/FilteredList.module.scss'; +import { CollapsibleTable } from '@organisms/collapsibleTable/CollapsibleTable'; +import { TableDB } from '@interface/table/TableDB'; +import { EventInfoResponse, EventItemResponse } from '@interface/evnet/response/EventItemResponse'; +import useGraphQL from '@hooks/useGraphQL'; +import { GET_EVENT_INFO, GET_EVENT_LIST, GET_EVENT_TYPE } from '@api/apollo/gql/queries/NoticesResponseQuery.gql'; +import { noticeFilterAtom } from '@recoil/atoms/admin/inquiry/notices/filter/noticeFilterAtom'; + +interface FilteredListInterface { + filteredTitle?: string; +} + +export const FilteredList = ({ ...props }: FilteredListInterface) => { + const [tableData, setTableData] = React.useState([]); // 테이블 데이터 + const [selectedEventInfo, setSelectedEventInfo] = React.useState(null); // 선택된 이벤트의 상세 정보 + const [page, setPage] = React.useState(1); // 선택된 페이지 + const [pageSize] = React.useState(10); // 페이지 범위 + const filterState = useRecoilValue(noticeFilterAtom); // 필터 리코일 상태 + + // 테이블 th 부분 gql 을 통해 불러오기 + const thItem = ['제목', '카테고리', '상태', '기간', '등록일', '수정일']; + + const { data: eventTypeData } = useGraphQL({ + query: GET_EVENT_TYPE, + type: 'query', + request: {}, + option: {}, + }); // 이벤트 타입(enum)목록 조회 + + const { data: eventListData, refetch: refetchEventData } = useGraphQL({ + query: GET_EVENT_LIST, + type: 'query', + request: { eventType: 'NOTICE' }, + option: {}, + }); // 타입에 맞는 이벤트 목록 조회 + + const { refetch } = useGraphQL({ + query: GET_EVENT_INFO, + type: 'query', + request: { evenId: 0 }, + option: { skip: 1 }, + }); // eventId를 통해 등록한 글 조회 + + React.useEffect(() => { + refetchEventData({ + variables: { + eventType: 'NOTICE', + }, + }); // 리패치 + }, [refetchEventData]); + + React.useEffect(() => { + if (!eventListData?.getEventList) return; // 이벤트 체크 + const convertedData: TableDB[] = eventListData.getEventList.map((event: EventItemResponse) => { + return { + id: event.id, + tRowTitle: [ + event.title, // 제목 + + new Date(event.startedAt).toLocaleDateString(), + new Date(event.endedAt).toLocaleDateString(), + event.thumbnail ? {event.title} : null, + ], + tCollContext: ( + + + {event.content &&
} + + ), + }; + }); + setTableData(convertedData); + }, [eventListData]); + + return ( + + + + + + ); +}; diff --git a/src/components/organisms/admin/filteredList/styles/FilteredList.module.scss b/src/components/organisms/admin/filteredList/styles/FilteredList.module.scss new file mode 100644 index 00000000..7a840b3a --- /dev/null +++ b/src/components/organisms/admin/filteredList/styles/FilteredList.module.scss @@ -0,0 +1,17 @@ +@import "index"; + +.paper{ + padding: 16px; + &__heading{ + padding: 0; + } + &__title{ + @include font-size(headline2); + } + +} + +// for minimum-phone +@include media-breakpoint-up(xxs){ + +} \ No newline at end of file diff --git a/src/components/organisms/admin/filteredList/styles/index.scss b/src/components/organisms/admin/filteredList/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/organisms/admin/filteredList/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/organisms/admin/filteredSearch/FilterSearchRevising.tsx b/src/components/organisms/admin/filteredSearch/FilterSearchRevising.tsx new file mode 100644 index 00000000..d289345f --- /dev/null +++ b/src/components/organisms/admin/filteredSearch/FilterSearchRevising.tsx @@ -0,0 +1,299 @@ +import React from 'react'; +import { ButtonProps, MenuItem, SelectChangeEvent, Stack } from '@mui/material'; +import { SelectMenuItemProps } from '@organisms/admin/filteredSearch/FilteredSearch'; +import { SelectBox } from '@molecules/selectBox/SelectBox'; +import Button from '@atoms/button/Button'; +import SearchBar from '@molecules/searchBar/SearchBar'; +import DateRange from '@molecules/dateRange/DateRange'; +import { useRecoilState } from 'recoil'; +import { noticeFilterAtom } from '@recoil/atoms/admin/inquiry/notices/filter/noticeFilterAtom'; +import { EnumValue, EventFilterType, TypenameDataResponse } from '@interface/evnet/filter/EventFilterType'; +import dayjs, { Dayjs } from 'dayjs'; +import useGraphQL from '@hooks/useGraphQL'; +import { GET_EVENT_TYPE } from '@api/apollo/gql/queries/NoticesResponseQuery.gql'; +import clsN from 'classnames'; +import styles from './styles/FilteredSearch.module.scss'; + +// select 변경이벤트를 위한 타입 +type SelectType = 'category' | 'postStatus'; + +export const FilterSearchRevising = () => { + /** 상태 */ + const [postedDate, setPostedDate] = React.useState(null); // 수정일일 범위 1 상태 + const [fixedDate, setFixedDate] = React.useState(null); // 등록일 범위 2 상태 + + const [eventFromDate, setEventFromDate] = React.useState(null); // 등록일 범위 1 상태 (이벤트) + const [eventEndDate, setEventEndDate] = React.useState(null); // 등록일 범위 2 상태 (이벤트) + + // 카테고리, 등록상태 gql 로 데이터와 패치 받기 아래는 구조 예시 + const [categoryList, setCategoryList] = React.useState([ + { + value: '0', + title: '전체 임시로 공지', + children: 'NOTICE', + }, + ]); + + const postStatusList: ButtonProps[] = [ + { + value: '0', + title: '전체', + children: 'ALL', + }, + { + value: '1', + title: '공개', + children: 'published', + }, + { + value: '2', + title: '비공개', + children: 'privated', + }, + ]; + // 클래스 이름 + const dateRangeClassNames = [ + styles['filter-root__button'], + styles['filter-root__button__date'], + styles['date-range'], + ]; + const popoverAreaClassNames = [ + styles['filter-root__button'], + styles['filter-root__button__date'], + styles['date-range__picker'], + ]; + const [searchFilterState, setSearchFilterState] = useRecoilState(noticeFilterAtom); // 검색 필터 리코일 상태 + const [keywardValue, setKeywardValue] = React.useState(''); // 검색 키워드 상태 + + const [categoryValue, setCategoryValue] = React.useState(categoryList[0]); // 카테고리 셀렉트 값 + + const [postStatusValue, setPostStatusValue] = React.useState(postStatusList[0]); // 공개상태 셀렉트 값 + + const { data: eventTypeData } = useGraphQL({ + query: GET_EVENT_TYPE, + type: 'query', + request: {}, + option: {}, + }); // 이벤트 타입목록 enum 조회 + + /** 필터 리코일 상태가 변경될때마다 셀렉트 박스 갱신 */ + React.useEffect(() => { + // 카테고리 검색 + const startItem = categoryList.find((item) => item.children === searchFilterState.eventType); + const startItemPost = postStatusList.find((item) => item.children === searchFilterState.postStatus); + if (startItem) { + setCategoryValue(startItem); + } + if (startItemPost) { + setPostStatusValue(startItemPost); + } + }, [searchFilterState.eventType, setSearchFilterState.prototype]); + + // 마운트 될때 카테고리 목록 불러오기 + React.useEffect(() => { + // eslint-disable-next-line no-underscore-dangle + if (!eventTypeData?.__typename?.enumValues) return; + + const defaultButtonOption: ButtonProps = { + value: '0', + title: '전체 임시 공지', + children: 'NOTICE', + }; // 기본 버튼 속성 + + // eslint-disable-next-line no-underscore-dangle + const eventTypes: ButtonProps[] = (eventTypeData as TypenameDataResponse).__typename.enumValues.map( + (enumType: EnumValue, index: number) => ({ + value: (index + 1).toString(), + title: enumType.name, + children: enumType.name, + }), + ); // 쿼리 조회 성공 시 버튼 속성에 맞게 매핑 + setCategoryList([defaultButtonOption, ...eventTypes]); // 카테고리 리스트 갱신 + if (!searchFilterState.eventType) { + // 이벤트 타입 값이 없을 경우 : selectBox, recoil 값을 default 로 설정 + setCategoryValue(defaultButtonOption); // 선택된 카테고리 상태 기본값으로 설정 + setSearchFilterState((prev) => ({ + ...prev, + eventType: 'NOTICE', + })); + } + }); + + /** 함수 */ + // MenuItem 컴포넌트 제공 함수 + const menuItemProvider = (menuItem: SelectMenuItemProps) => { + const menuItemCode = menuItem.children?.toString(); + return {menuItemCode}; + }; + // 키워드 입력 이벤트 + const handleKeywardChange = (e: React.ChangeEvent) => { + setKeywardValue(e.target.value); + }; + // selectBox : Category 셀렉트박스 이벤트 + const handleselectChange = (e: SelectChangeEvent, type: SelectType, buttonGroupProps: ButtonProps[]) => { + const selectedValue = e.target.value; // 선택된 값 + const filtedValue = buttonGroupProps.find((item) => item.value === selectedValue); // 조회된 값 + if (filtedValue) { + // 필터를 통한 값이 존재하면... + if (type === 'category') { + // 카테고리 분기 + setCategoryValue(filtedValue); // 상태 변경 + // 리코일 상태 수정 + setSearchFilterState((prev) => ({ + ...prev, + eventType: 'NOTICE', // 이벤트 타입 배포되기전에 default 는 NOTICE + // eventType: filtedValue.children as string, + })); + } else if (type === 'postStatus') { + // 공개상태 분기 + setPostStatusValue(filtedValue); // 상태 변경 + // 리코일 상태 수정 + setSearchFilterState((prev) => ({ + ...prev, + postStatus: filtedValue.children as string, + })); + } + } + }; + + // DateRange 등록일 수정일 이벤트 + const handleDateChange = (startDate: Date | undefined, endDate: Date | undefined) => { + setSearchFilterState((prev) => ({ + ...prev, + startedAt: startDate, + endedAt: endDate, + })); + setPostedDate(startDate ? dayjs(startDate) : null); + setFixedDate(endDate ? dayjs(endDate) : null); + }; + + // DateRange 이벤트기간 이벤트 + const handleEventDateChange = (startDate: Date | undefined, endDate: Date | undefined) => { + setSearchFilterState((prev) => ({ + ...prev, + postedAt: startDate, + fixedAt: endDate, + })); + setEventFromDate(startDate ? dayjs(startDate) : null); + setEventEndDate(endDate ? dayjs(startDate) : null); + }; + + // 초기화 이벤트 + const handleResetClick = () => { + setSearchFilterState(() => ({ + startedAt: undefined, + endedAt: undefined, + postedAt: undefined, + fixedAt: undefined, + postStatus: '', + eventType: '', + keyword: '', + })); // 리코일 상태 초기화 + setKeywardValue(''); // 키워드 입력 초기화 + setCategoryValue(categoryList[0]); + setPostStatusValue(postStatusList[0]); + setPostedDate(null); + setFixedDate(null); + setEventFromDate(null); + setEventEndDate(null); + }; + // 검색 이벤트 + const handleSearchClick = () => { + const newFilters: Partial = { + keyword: keywardValue, + eventType: categoryValue.children as string, + postStatus: postStatusValue.children as string, + postedAt: searchFilterState.startedAt, + fixedAt: searchFilterState.fixedAt, + startedAt: searchFilterState.startedAt, + endedAt: searchFilterState.endedAt, + }; + setSearchFilterState((prev) => ({ + ...prev, + ...newFilters, + })); + }; + + /* TSX */ + // 날짜 조회 DateContainer 컴포넌트 + + // 초기화 버튼 컴포넌트 + const ClearBtn = ( + + ); + // 조회 버튼 컴포넌트 + const SearchBtn = ( + + ); + /** 렌더 */ + return ( + + + + menuItemProvider(item)} + handleMenuChange={(e) => handleselectChange(e, 'category', categoryList)} + /> + menuItemProvider(item)} + handleMenuChange={(e) => handleselectChange(e, 'postStatus', categoryList)} + /> + + + + + + + {ClearBtn} + {SearchBtn} + + + ); +}; diff --git a/src/components/organisms/admin/filteredSearch/FilteredSearch.tsx b/src/components/organisms/admin/filteredSearch/FilteredSearch.tsx index 2d5e5ea5..a4057256 100644 --- a/src/components/organisms/admin/filteredSearch/FilteredSearch.tsx +++ b/src/components/organisms/admin/filteredSearch/FilteredSearch.tsx @@ -7,6 +7,7 @@ import DateRange from '@molecules/dateRange/DateRange'; import SearchBar from '@molecules/searchBar/SearchBar'; import clsN from 'classnames'; import styles from './styles/FilteredSearch.module.scss'; +import dayjs from 'dayjs'; export interface SelectMenuItemProps extends ButtonProps {} @@ -54,13 +55,17 @@ export const FilteredSearch = ({ // 날짜조회 컴포넌트 const DateRangeCont = ( {}} + onReset={() => {}} + endDate={dayjs()} + fromDate={dayjs()} className={clsN(styles['filter-root__button'], styles['filter-root__button__date'], styles['date-range'])} pickerClsN={clsN( styles['filter-root__button'], styles['filter-root__button__date'], styles['date-range__picker'], )} - resetTrigger={resetTrigger} /> ); // 초기화 버튼 컴포넌트 diff --git a/src/components/organisms/admin/filteredSearch/styles/FilteredSearch.module.scss b/src/components/organisms/admin/filteredSearch/styles/FilteredSearch.module.scss index 620edd7d..356d07bc 100644 --- a/src/components/organisms/admin/filteredSearch/styles/FilteredSearch.module.scss +++ b/src/components/organisms/admin/filteredSearch/styles/FilteredSearch.module.scss @@ -4,8 +4,14 @@ .filter-root{ z-index: 50; width: 100%; - height: 40px; - // + margin-bottom: 16px; + gap: 12px; + &__field{ // searchbar, selectbox1...,2 stack + gap: 12px; + & > :first-child{ + flex-grow: 1; + } + } &__button{ margin-left: auto; text-wrap: nowrap; @@ -13,33 +19,248 @@ text-wrap: nowrap; } &__clear{ - background-color: rgba(0,0,0,0); - border-color: $background-dimgray; - color: $color-text-dimgray; &:hover{ - background-color: $background-dimgray; - color: $color-text-white; - border-color: $background-dimgray; } } &__search{ - margin-left: 4px; - background-color: $background-primary; - color: $color-text-white; &:hover{ - background-color: $background-hover; } } } } +// dateRange Wrapper 영역 +.date-range-box{ + display: flex; + &__calender{ + } +} + // dateRage 영역 .date-range{ z-index: 50; padding: 16px; border-radius: 4px; background-color: white; - width: fit-content; &__picker{ } +} +// TODO : 관리자 검색 버튼 스타일 수정 +// submitButton area +.submit{ + padding-top: 12px; + flex-direction: column; + gap: 8px; + &__button{ // 버튼 + &-clear{ // 취소 + &:hover{ + } + } + &-search{ // 검색 + &:active{ + + } + &:hover{ + + } + } + } +} + +// 300px ~ +@include media-breakpoint-up(xxs) { + .filter-root{ + &__field{ // searchbar, selectbox1...,2 stack + + } + &__button{ + &__date{ + } + &__clear{ + &:hover{ + } + } + &__search{ + margin-left: 0; + &:hover{ + } + } + } + } + // dateRange Wrapper 영역 + .date-range-box{ + display: flex; + flex-direction: column; + gap: 12px; + } + .date-range{ + &__picker{ + width: fit-content; + } + } + // submitButton area + .submit{ + &__button{ // 버튼 + &-clear{ // 취소 + &:hover{ + } + } + &-search{ // 검색 + &:active{ + + } + &:hover{ + + } + } + } + } +} + + +// 360px ~ +@include media-breakpoint-up(xs) { + .filter-root{ + &__button{ + &__date{ + } + &__clear{ + &:hover{ + } + } + &__search{ + margin-left: 0; + &:hover{ + } + } + } + } + // submitButton area + .submit{ + &__button{ // 버튼 + &-clear{ // 취소 + &:hover{ + } + } + &-search{ // 검색 + &:active{ + + } + &:hover{ + + } + } + } + } +} + +// 480px ~ +@include media-breakpoint-up(sm) { + .filter-root{ + + &__button{ + &__date{ + } + &__clear{ + &:hover{ + } + } + &__search{ + &:hover{ + } + } + } + } +} + +// 768px ~ +@include media-breakpoint-up(md) { + .filter-root{ + + &__field{ // searchbar, selectbox1...,2 stack + flex-direction: row; + & > :first-child{ + flex-grow: inherit; + } + &__form{ + + } + } + &__button{ + &__date{ + } + &__clear{ + &:hover{ + } + } + &__search{ + &:hover{ + } + } + } + } + // dateRange Wrapper 영역 + .date-range-box{ + flex-direction: row; + &__picker{ + + } + &__calender{ + } + } + .submit{ + flex-direction: row; + } + +} +// 980px +@include media-breakpoint-up(lg){ + .filter-root{ + &__button{ + &__date{ + } + &__clear{ + &:hover{ + } + } + &__search{ + &:hover{ + } + } + } + } +} +// 1080px +@include media-breakpoint-up(xl){ + .filter-root{ + &__button{ + &__date{ + } + &__clear{ + &:hover{ + } + } + &__search{ + &:hover{ + } + } + } + } +} +// 1400px +@include media-breakpoint-up(xxl){ + .filter-root{ + &__button{ + &__date{ + } + &__clear{ + &:hover{ + } + } + &__search{ + &:hover{ + } + } + } + } } \ No newline at end of file diff --git a/src/components/organisms/admin/mainNav/MainNav.tsx b/src/components/organisms/admin/mainNav/MainNav.tsx index 877fcde0..2e8afd35 100644 --- a/src/components/organisms/admin/mainNav/MainNav.tsx +++ b/src/components/organisms/admin/mainNav/MainNav.tsx @@ -1,12 +1,11 @@ import React from 'react'; import { Stack } from '@mui/material'; -import SearchBarAuto from '@molecules/searchBarAuto/SearchBarAuto'; +// import SearchBarAuto from '@molecules/searchBarAuto/SearchBarAuto'; import UserLoginPlace from '@molecules/userLoginPlace/UserLoginPlace'; import clsN from 'classnames'; import styles from './styles/MainNav.module.scss'; - -interface MainNavProps{ +interface MainNavProps { // 클래스명 className?: string; // 현재 라우터에 맞는 아이템 @@ -17,11 +16,7 @@ interface MainNavProps{ * * @constructor */ -const MainNav = ( - { - className - }:MainNavProps -) => { +const MainNav = ({ className }: MainNavProps) => { // 상태 // 함수 @@ -29,16 +24,12 @@ const MainNav = ( // 생성기 // const linkGen - React.useEffect(()=>{ - - },[]); - return( - - - - + React.useEffect(() => {}, []); + return ( + + ); }; -export default MainNav; \ No newline at end of file +export default MainNav; diff --git a/src/components/organisms/admin/modalEditor/ModalEditor.tsx b/src/components/organisms/admin/modalEditor/ModalEditor.tsx index 1b805757..03da03a9 100644 --- a/src/components/organisms/admin/modalEditor/ModalEditor.tsx +++ b/src/components/organisms/admin/modalEditor/ModalEditor.tsx @@ -1,37 +1,98 @@ -import React from 'react'; -import { Editor as TinyMCEEditor } from 'tinymce'; -import { Box, ButtonProps, Modal } from '@mui/material'; -import { TinyEditorBasic } from '@commons/tinyEditor/TinyEditorBasic'; +import React, { ChangeEvent } from 'react'; +import { Box, ButtonProps, Modal, Stack } from '@mui/material'; +import { TinyEditorBasicComponent } from '@commons/tinyEditor/TinyEditorBasic'; +import { Input } from '@atoms/input/Input'; import { ButtonGroup } from '@organisms/admin/buttonGroup/ButtonGroup'; +import clsN from 'classnames'; +import styles from './styles/ModalEditor.module.scss'; interface ModalEditorProps { - editorRef: React.MutableRefObject; // 포커스된 DOM 요소 - initialValue?: string; // 에디터 시작 컨텐츠 - open: boolean; // 모달 상태 - onClose: (event: Event, reason: 'backdropClick' | 'escapeKeyDown') => void; // 모달 종료 이벤트 - onEditorChange?: (content: string) => void; // 에디터 입력 이벤트 - buttonItems: ButtonProps[]; // 버튼 요소 + /* 제목 입력란 혹은 카테고리가 필요할 수 있음 */ + // 제목 입력란 여부 + requireTitle?: boolean; + // 제목 입력란 여부 + title?: string; + // 제목 입력 이벤트 + onTitleChange?: (e: React.ChangeEvent) => void; + // 에디터 시작 컨텐츠 + initialValue?: string; + // 모달 상태 + open: boolean; + // 모달 종료 이벤트 + onClose: (event: Event, reason: 'backdropClick' | 'escapeKeyDown') => void; + // 버튼 요소 + buttonItems: ButtonProps[]; + // 저장 이벤트 : 콜백 함수 + onSave?: (content: string) => void; } export const ModalEditor = ({ ...props }: ModalEditorProps) => { + // 모달 컴포넌트 내부에만 에디터 상태가 관리되므로 현재 ModalEditor 에서 상태관리하기 + const [editorContent, setEditorContent] = React.useState(''); + // 제목 영역 상태관리도 동일 + const [editorTitle, setEditorTitle] = React.useState(''); + // 에디터 제목 입력 이벤트 + const handleTitlechange = (e: ChangeEvent) => { + setEditorTitle(e.target.value); + }; // 에디터 입력 이벤트 - const handleEditorChange = (newContent: string) => { - props.onEditorChange?.(newContent); + const handleEditorChange = React.useCallback((content: string) => { + setEditorContent(content); + }, []); + + // Save 이벤트 + const handleSave = () => { + if (props.onSave) { + // 해당 함수 존재할 경우 + props.onSave(editorContent); // 콜백 호출 + } }; + + // 제목 상태 입력 이벤트가 느려서 메모이제이션 활용하기 + const titleArea = props.requireTitle && ( + + + + ); + + // save 버튼 JSX + const buttonItems = props.buttonItems.map((button) => { + // props.buttonItems [] 속성을 매핑하면서 특정 이름을 가진 속성의 함수를 수정 + if (button.name === 'save') { + // 버튼 속성 이름이 save 일 경우 + return { + ...button, + onClick: handleSave, // 콜백함수 추가 + }; + } + return button; + }); + // 렌더 return ( - - - - + + + {titleArea} + + + + ); }; ModalEditor.defaultProps = { initialValue: '', + requireTitle: false, + title: undefined, }; diff --git a/src/components/organisms/admin/modalEditor/styles/ModalEditor.module.scss b/src/components/organisms/admin/modalEditor/styles/ModalEditor.module.scss new file mode 100644 index 00000000..0ad1c4aa --- /dev/null +++ b/src/components/organisms/admin/modalEditor/styles/ModalEditor.module.scss @@ -0,0 +1,18 @@ +@import "index"; + +.paper{ + height: 100%; + &__input{ + width: 100%; + margin-bottom: 12px; + background-color: white; + border-radius: 4px; + &-title{ + } + } + &__editor{ + width: 72%; + } + &__button-group{ + } +} diff --git a/src/components/organisms/admin/modalEditor/styles/index.scss b/src/components/organisms/admin/modalEditor/styles/index.scss new file mode 100644 index 00000000..16a66a97 --- /dev/null +++ b/src/components/organisms/admin/modalEditor/styles/index.scss @@ -0,0 +1 @@ +@import 'src/styles/scss/index'; \ No newline at end of file diff --git a/src/components/organisms/admin/product/register/productCategory/ProductCategory.tsx b/src/components/organisms/admin/product/register/productCategory/ProductCategory.tsx new file mode 100644 index 00000000..c514d53d --- /dev/null +++ b/src/components/organisms/admin/product/register/productCategory/ProductCategory.tsx @@ -0,0 +1,22 @@ +/* eslint-disable */ +import React from 'react'; +import clsN from 'classnames'; +import { Divider, Paper } from '@mui/material'; +import { Heading } from '@molecules/admin/layout/heading/Heading'; +import { ProductLayerInterface } from '@interface/layer/ProductLayerInterface'; + +interface ProductCategoryProps extends ProductLayerInterface {} + +export const ProductCategoryRegister = ({ + className, + parentHeadlineCleN, + sectionHeadlineClsN, +}: ProductCategoryProps) => { + return ( + + + + etc + + ); +}; diff --git a/src/components/organisms/admin/product/register/productCategory/styles/ProductCategory.modlue.scss b/src/components/organisms/admin/product/register/productCategory/styles/ProductCategory.modlue.scss new file mode 100644 index 00000000..2ed8b9c3 --- /dev/null +++ b/src/components/organisms/admin/product/register/productCategory/styles/ProductCategory.modlue.scss @@ -0,0 +1 @@ +@import "./index"; \ No newline at end of file diff --git a/src/components/organisms/admin/product/register/productCategory/styles/index.scss b/src/components/organisms/admin/product/register/productCategory/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/organisms/admin/product/register/productCategory/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/organisms/admin/product/register/productContent/ProductContent.tsx b/src/components/organisms/admin/product/register/productContent/ProductContent.tsx new file mode 100644 index 00000000..f43755d5 --- /dev/null +++ b/src/components/organisms/admin/product/register/productContent/ProductContent.tsx @@ -0,0 +1,101 @@ +/* eslint-disable */ +import React from 'react'; +import { Button, ButtonProps, Divider, Paper, Stack } from '@mui/material'; +import { Heading } from '@molecules/admin/layout/heading/Heading'; +import clsN from 'classnames'; +import { useRecoilState } from 'recoil'; +import { productRegisterAtom } from '@recoil/atoms/admin/product/register/ProductRegisterAtom'; +import { ModalEditor } from '@organisms/admin/modalEditor/ModalEditor'; +import product from '@routes/product/Product'; +import { ProductDetail } from '@molecules/admin/product/component/productDetail/ProductDetail'; +import { ProductLayerInterface } from '@interface/layer/ProductLayerInterface'; + +// TODO : 에디터의 내용을 미리볼 수 있게 하기 + +interface ProductContentProps extends ProductLayerInterface {} +export const ProductContent = ({ className, parentHeadlineCleN, sectionHeadlineClsN }: ProductContentProps) => { + // 상품 정보 전역 상태 사용, 그리고 컨텐츠 데이터 미리 로딩하기 + const [productData, setProductData] = useRecoilState(productRegisterAtom); + + // 모달 에디터 준비 상태 + const [editorModalState, setEditorModalState] = React.useState(false); + + // 모달 에디터 컨텐츠 상태 + const [editorContent, setEditorContent] = React.useState(''); + + // 리코일 상태 체크하면서 모달에디터 값 갱신 + React.useEffect(() => { + setEditorContent(productData.content); + }, [productData.content]); + + // 모달 에디터 활성화 + const handleModalChange = (e: Event, reason: 'backdropClick' | 'escapeKeyDown') => { + // 에디터 후면 블럭 클릭 이벤트 방지 + if (reason == 'backdropClick') { + // 뒷배경 클릭일 경우 활성화 유지 + setEditorModalState(true); + } else { + setEditorModalState((prevState) => !prevState); + } + // 활성화 할 때 해당 에디터 값 갱신하기 + setEditorContent(productData.content); + }; + // 모달 에디터 작성 저장 이벤트(상세 작성 버튼) + const handleEditorSave = (content: string) => { + // TODO : 정말로 저장할지 confirm 분기 설정하기 + // 에디터 내용 리코일에 갱신하기 + setProductData((prev) => ({ + ...prev, + content: content, + })); + // TODO : 저장하면서 gql 로 content 갱신 요청 + // 모달 에디터 종료 + setEditorModalState(false); + }; + // 모달 에디터 작성 취소 이벤트 + const handleEditorCancel = () => { + // TODO : 정말로 취소할지 confirm 분기 설정하기 + // 모달 에디터 상태 : off + setEditorModalState(false); + }; + + // 모달 버튼 컴포넌트 + const EditorCheckButton = () => { + // 모달 활성화 + setEditorModalState(true); + }; + + // 버튼 분기 생성 + const buttonItemProvider = (): ButtonProps[] => { + return [ + { + name: 'save', + children: 'Save', + // 마우스 이벤트 처리 + onClick: (e) => { + handleEditorSave(editorContent); + }, + }, + { + name: 'cancel', + children: 'Cancel', + onClick: handleEditorCancel, + }, + ]; + }; + + return ( + + + + + + + ); +}; diff --git a/src/components/organisms/admin/product/register/productContent/styles/ProductContent.module.scss b/src/components/organisms/admin/product/register/productContent/styles/ProductContent.module.scss new file mode 100644 index 00000000..2ed8b9c3 --- /dev/null +++ b/src/components/organisms/admin/product/register/productContent/styles/ProductContent.module.scss @@ -0,0 +1 @@ +@import "./index"; \ No newline at end of file diff --git a/src/components/organisms/admin/product/register/productContent/styles/index.scss b/src/components/organisms/admin/product/register/productContent/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/organisms/admin/product/register/productContent/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/organisms/admin/product/register/productImage/ProductImage.tsx b/src/components/organisms/admin/product/register/productImage/ProductImage.tsx new file mode 100644 index 00000000..87a1a98b --- /dev/null +++ b/src/components/organisms/admin/product/register/productImage/ProductImage.tsx @@ -0,0 +1,94 @@ +/* eslint-disable */ +import React from 'react'; +import { ProductLayerInterface } from '@interface/layer/ProductLayerInterface'; +import { Divider, Paper, Stack } from '@mui/material'; +import { Heading } from '@molecules/admin/layout/heading/Heading'; +import { ImageBoxManager } from '@organisms/imageBoxManager/ImageBoxManager'; +import { ThumbnailManager } from '@organisms/admin/thumbnailManager/ThumbnailManager'; +import clsN from 'classnames'; +import styles from './styles/ProductImage.module.scss'; +import { useRecoilState } from 'recoil'; +import { productRegisterAtom } from '@recoil/atoms/admin/product/register/ProductRegisterAtom'; +import { DndContext } from '@dnd-kit/core'; +import { Draggable } from '@commons/dropAndDrop/draggable/Draggable'; +import { Droppable } from '@commons/dropAndDrop/droppable/Droppable'; + +interface ProductImageProps extends ProductLayerInterface {} + +export const ProductImage = ({ className, parentHeadlineCleN, sectionHeadlineClsN }: ProductImageProps) => { + // 전역 상태 관리 리코일 + const [productData, setProductData] = useRecoilState(productRegisterAtom); + + // 썸네일 이미지 변경 이벤트 + const handleThumbnailImageChange = (value: string) => { + // modifiy imageUrl of index[0] + setProductData((prev) => { + if (!prev.imageUrls) { + // empty images case + return { + ...prev, + imageUrls: [value] as [string], + }; + } + + // previous ImageUrls + const newImageUrlStorage = [...prev.imageUrls]; + newImageUrlStorage[0] = value; + + return { + // has image[0] case + ...prev, + imageUrls: newImageUrlStorage as [string], + }; + }); + }; + + // thumbnail image remove event + const handleThumbnailImageRemove = () => { + setProductData((prev) => { + if (!prev.imageUrls) { + // empty image case + return { + ...prev, + }; + } + + const previousImages = [...prev.imageUrls]; + // set index 0 of image into '' + previousImages[0] = ''; + + return { + // has imageUrl[0] case + ...prev, + imageUrls: previousImages as [string], + }; + }); + }; + + return ( + + + + + + + + + + + + + ); +}; diff --git a/src/components/organisms/admin/product/register/productImage/styles/ProductImage.module.scss b/src/components/organisms/admin/product/register/productImage/styles/ProductImage.module.scss new file mode 100644 index 00000000..75db292c --- /dev/null +++ b/src/components/organisms/admin/product/register/productImage/styles/ProductImage.module.scss @@ -0,0 +1,11 @@ +@import "./index"; + +.imagebox{ + width: 116px; + &__image{ + + } + &__bar{ + + } +} \ No newline at end of file diff --git a/src/components/organisms/admin/product/register/productImage/styles/index.scss b/src/components/organisms/admin/product/register/productImage/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/organisms/admin/product/register/productImage/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/organisms/admin/product/register/productInfo/ProductInfo.tsx b/src/components/organisms/admin/product/register/productInfo/ProductInfo.tsx new file mode 100644 index 00000000..88674439 --- /dev/null +++ b/src/components/organisms/admin/product/register/productInfo/ProductInfo.tsx @@ -0,0 +1,97 @@ +/* eslint-disable */ +import React from 'react'; +import { Divider, FormControl, InputLabel, Paper, Stack } from '@mui/material'; +import { Heading } from '@molecules/admin/layout/heading/Heading'; +import { Input } from '@atoms/input/Input'; +import { useRecoilState } from 'recoil'; +import { productRegisterAtom } from '@recoil/atoms/admin/product/register/ProductRegisterAtom'; +import { ProductLayerInterface } from '@interface/layer/ProductLayerInterface'; +import clsN from 'classnames'; +import styles from './styles/ProductInfo.module.scss'; + +interface ProductInfoProps extends ProductLayerInterface {} + +export const ProductInfo = ({ className, parentHeadlineCleN, sectionHeadlineClsN }: ProductInfoProps) => { + /* 상태 */ + const [productData, setProductData] = useRecoilState(productRegisterAtom); // 상품 정보 전역 상태 사용 + + /* 핸들러 */ + + // 상품 정보 입력 이벤트 핸들러 + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; // 목표 Input 이벤트 객체에 속성 가져옴 + setProductData((prev) => ({ + ...prev, + [name]: value, // name 을 활용한 key 값 방식으로 속성 갱신 + })); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/organisms/admin/product/register/productInfo/styles/ProductInfo.module.scss b/src/components/organisms/admin/product/register/productInfo/styles/ProductInfo.module.scss new file mode 100644 index 00000000..1351d17c --- /dev/null +++ b/src/components/organisms/admin/product/register/productInfo/styles/ProductInfo.module.scss @@ -0,0 +1,2 @@ +@import "./index"; + diff --git a/src/components/organisms/admin/product/register/productInfo/styles/index.scss b/src/components/organisms/admin/product/register/productInfo/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/organisms/admin/product/register/productInfo/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/organisms/admin/product/register/productPrice/ProductPrice.tsx b/src/components/organisms/admin/product/register/productPrice/ProductPrice.tsx new file mode 100644 index 00000000..5a5e74d0 --- /dev/null +++ b/src/components/organisms/admin/product/register/productPrice/ProductPrice.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Divider, Paper } from '@mui/material'; +import { ProductLayerInterface } from '@interface/layer/ProductLayerInterface'; +import { Heading } from '@molecules/admin/layout/heading/Heading'; + +interface ProductPriceProps extends ProductLayerInterface {} +export const ProductPrice = ({ className, parentHeadlineCleN }: ProductPriceProps) => { + return ( + + + + + ); +}; diff --git a/src/components/organisms/admin/product/register/productPrice/styles/ProductPrice.module.scss b/src/components/organisms/admin/product/register/productPrice/styles/ProductPrice.module.scss new file mode 100644 index 00000000..2ed8b9c3 --- /dev/null +++ b/src/components/organisms/admin/product/register/productPrice/styles/ProductPrice.module.scss @@ -0,0 +1 @@ +@import "./index"; \ No newline at end of file diff --git a/src/components/organisms/admin/product/register/productPrice/styles/index.scss b/src/components/organisms/admin/product/register/productPrice/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/organisms/admin/product/register/productPrice/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/organisms/admin/searchBar/SearchBar.tsx b/src/components/organisms/admin/searchBar/SearchBar.tsx deleted file mode 100644 index b2396185..00000000 --- a/src/components/organisms/admin/searchBar/SearchBar.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable */ -import React from 'react'; -import { AutoComplete } from '@molecules/autoComplete/AutoComplete'; -import { Stack } from '@mui/material'; -import DateRange from '@molecules/dateRange/DateRange'; -import clsN from 'classnames'; -import styles from './styles/SearchBar.module.scss'; -import Category from '@routes/category/Category'; - -/** 제너릭을 통해 어떤 옵션인지 그리고 어떤 그룹타입인지 정해야함 */ -/** - * interface 대신 type 사용한 이유 - * 유니온, 인터섹션, 조건부 타입 등 더 복작한 타입연산을 쉽게 표현이 가능함 - * 인터페이스의 경우 extends 를 통한 확장이 가능하나, 유니온이나 인터섹션같은 타입연산의 경우 직접 표현하기 어려움 - */ - -/** - * @template Opt 옵션 타입 - * @template Group 그룹 타입 - * @template ShowClearable 선택 초기화 비활성화 여부 default : false - * @template ShowDateRange 날자범위 활성화 여부 default : false - * @template ShowCategories 카테고리 활성화 여부 default : false - * */ - -/** searchBarProp 에 쓰일 기본 인터페이스 */ -interface BaseSearchBarProps { - // AutoComplete 에 활용될 옵션 목록들 - options: Opt[]; - // 선택 초기화 비활성화 여부 default : false - showClear?: boolean; - // 날자범위 활성화 여부 default : false - showDateRange?: boolean; - // 카테고리 표시 여부 - showCategories?: boolean; - // AutoComplete 내부 옵션 그룹 정렬 여부 default : false - showOptionGroup?: boolean; - resetTrigger: boolean; // 리셋 트리거 -} -/** 카테고리 적용 타입 */ -interface WithCategories { - // 카테고리 표시 여부 - showCategories: true; - // 카테고리 목록들 - categories: Categories[]; -} -/** 카테고리 미적용 타입 */ -interface WithoutCategories { - // 카테고리 표시 여부 - showCategories?: false; - // 카테고리 목록들 - categories?: never; -} -/** 옵션그룹 적용 타입 */ -interface WithOptionGroup { - // 옵션 그룹 표시 여부 - showOptionGroup: true; - // 옵션그룹 목록 - optionGroup: Group; -} -/** 옵션그룹 미적용 타입 */ -interface WithoutOptionGroup { - // 옵션 그룹 표시 여부 - showOptionGroup?: false; - // 옵션그룹 목록 - optionGroup?: never; -} -/** SearchBarProps 인터페이스 - * interface 의 장점인 선언 병합과 객체타입 정의 등등이 있지만 유니온 타입확장이 안됨 - * 유니온 타입으로 확장할 수 없기 때문에 임시로 Type 로 선언 - * */ -type SearchBarProps = BaseSearchBarProps & - (WithCategories | WithoutCategories) & - (WithOptionGroup | WithoutOptionGroup); - -const SearchBar = ({ - showClear = false, - showDateRange = false, - ...props -}: SearchBarProps) => { - /* 상태 */ - /* 변수 */ - /** DateRange 활성화 여부 */ - - /** */ - /* 함수 */ - /* TSX */ - /** */ - /** AutoComplete 컴포넌트 */ - const AutoInput = ; - /** 카테고리 컴포넌트 */ - - /* 렌더 */ - return ( - - {showDateRange && } - {AutoInput} - - ); -}; diff --git a/src/components/organisms/admin/thumbnailManager/ThumbnailManager.tsx b/src/components/organisms/admin/thumbnailManager/ThumbnailManager.tsx new file mode 100644 index 00000000..01a48b2a --- /dev/null +++ b/src/components/organisms/admin/thumbnailManager/ThumbnailManager.tsx @@ -0,0 +1,126 @@ +/* eslint-disable */ +import React, { ChangeEvent } from 'react'; +import { Backdrop, IconButton, Paper, Stack } from '@mui/material'; +import { ImageBox } from '@molecules/imageBox/ImageBox'; +import { ImageBoxPropsInterface } from '@interface/image/ImageInterface'; +import Button from '@atoms/button/Button'; +import Text from '@atoms/text/Text'; +import pick from '@organisms/home/product/pick/Pick'; +import clsN from 'classnames'; +import styles from './styles/ThumbnailManager.module.scss'; +import { Cancel, MoreVert } from '@mui/icons-material'; + +interface ThumbnailMagagerProps { + // 이미지박스 속성 + imageBoxProps: ImageBoxPropsInterface; + // 콜백 이미지 변경 함수 + changeImage: (value: string) => void; + // image Remove Event + removeImage: () => void; +} + +export const ThumbnailManager = ({ changeImage, removeImage, ...props }: ThumbnailMagagerProps) => { + // 이미지 상태 + const [imageFile, setImageFile] = React.useState(null); + + // preview State + const [previewState, setPreviewState] = React.useState(false); + + // context + const imageGuidance = `이미지의 가로 세로 비율은 1:1 을 추천하며 + 규격은 px 단위로 최소 400 * 400, 최대 600 * 600 규격에 맞춰야 합니다.`; + + // 이미지 변경 이벤트 + const handleImageChange = (e: ChangeEvent) => { + // selected file, optional checked + const pickedFile = e.target.files?.[0]; + + // empty file case, files?.[0] are optional check + if (!pickedFile) return; + + // url Reader + const fileReader = new FileReader(); + + // file loaded case + fileReader.onloadend = () => { + // fileReader.result type => + // set as string type + setImageFile(fileReader.result as string); + }; + + // read as data url, optional check + fileReader.readAsDataURL(pickedFile); + + // if same file input value of 'event' and previous value are sames it't will onChange not works + // set event value '' + e.target.value = ''; + + // have optional check case + if (changeImage) { + // optional check + if (props.imageBoxProps.src) { + changeImage(props.imageBoxProps.src); + setImageFile(props.imageBoxProps.src); + } + } + }; + // 이미지 미리보기 이벤트 + const previewImageSource = () => { + if (!imageFile) { + setPreviewState(false); + return; + } + if (imageFile) { + setPreviewState(true); + } + // console.log(`current ImageUrl : ${imageFile}`); + }; + + // image remove + const handleImageRemove = () => { + // confirm check + removeImage(); + setImageFile(null); + }; + + // JSX 컴포넌트 + // 이미지 미리보기 및 변경 버튼 그룹 + const PreviewButton = ( + + + + + ); + + return ( + + + + + } + /> + + + {PreviewButton} + + { + setPreviewState(false); + }} + className={clsN(styles.backdrop)} + > + preview Image + + + ); +}; diff --git a/src/components/organisms/admin/thumbnailManager/styles/ThumbnailManager.module.scss b/src/components/organisms/admin/thumbnailManager/styles/ThumbnailManager.module.scss new file mode 100644 index 00000000..ee5cc026 --- /dev/null +++ b/src/components/organisms/admin/thumbnailManager/styles/ThumbnailManager.module.scss @@ -0,0 +1,13 @@ +@import "./index"; + +// 대표이미지 설명란 +.context{ + white-space: pre-line; +} + +// backdrop +.backdrop{ + &__image{ + max-width: 50%; + } +} \ No newline at end of file diff --git a/src/components/organisms/admin/thumbnailManager/styles/index.scss b/src/components/organisms/admin/thumbnailManager/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/organisms/admin/thumbnailManager/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/organisms/collapsibleTable/CollapsibleTable.tsx b/src/components/organisms/collapsibleTable/CollapsibleTable.tsx new file mode 100644 index 00000000..8417ac4d --- /dev/null +++ b/src/components/organisms/collapsibleTable/CollapsibleTable.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { Paper, Table, TableBody, TableCell, TableContainer, TableHead } from '@mui/material'; +import { CollapseHeader } from '@molecules/collapseHeader/CollapseHeader'; +import { TitleRow } from '@molecules/collapseBody/titleRow/TitleRow'; +import { ContextRow } from '@molecules/collapseBody/contentRow/ContextRow'; +import { TableDB } from '@interface/table/TableDB'; +import clsN from 'classnames'; +import styles from './styles/CollpasibleTable.module.scss'; + +interface CollapsibleTableProps { + tableLabel: string; // 테이블 레이블 명 + + tableData: TableDB[]; // 테이블 데이터 + tHeaders: string[]; // 테이블 헤더 +} + +export const CollapsibleTable = ({ ...props }: CollapsibleTableProps) => { + const { tableData, tHeaders } = props; + + const [collList, setCollList] = React.useState(() => Array(tableData.length).fill(false)); // 목록들 펼침 상태 + + const toggleCollapse = (index: number) => { + // 콜랩스 토글 이벤트 + setCollList((prevColl) => { + const newCollState = [...prevColl]; + newCollState[index] = !newCollState[index]; + return newCollState; + }); + }; + + const setCollapseArray = (index: number) => { + // index 를 통해 본문 열람 처리 + toggleCollapse(index); + }; + + const tHeadRender = (tHeadItems: string[]) => { + const tCells = tHeadItems.map((item) => ( + + {item} + + )); + return ; + }; + + const tCollpaseRender = (tableData: { tRowTitle: React.ReactNode[]; tCollContext: React.ReactNode }[]) => { + return tableData.map((item, index) => { + const { tRowTitle, tCollContext } = item; + const currentState = collList[index]; // 현재 인덱스 상태 + const onCollapseChange = () => { + // 콜랩스 토글 이벤트 + setCollapseArray(index); + }; + const collapseTitle = ( + + ); // 제목 컴포넌트 + const collaseContext = ; // 콜랩스 본문 컴포넌트 + + // Fragment 리턴 + return ( + <> + {collapseTitle} + {collaseContext} + + ); + }); + }; + + return ( + + + {tHeadRender(tHeaders)} + {tCollpaseRender(tableData)} +
+
+ ); +}; diff --git a/src/components/organisms/collapsibleTable/styles/CollpasibleTable.module.scss b/src/components/organisms/collapsibleTable/styles/CollpasibleTable.module.scss new file mode 100644 index 00000000..f4aaf478 --- /dev/null +++ b/src/components/organisms/collapsibleTable/styles/CollpasibleTable.module.scss @@ -0,0 +1,19 @@ +@import "../../../../index.css"; + +// 테이블 컨테이너 +.table{ + // 테이블 헤드 + &__head{ + &__cell{ + text-wrap: nowrap; + text-overflow: ellipsis; + &:first-child{ + + } + } + } + // 테이블 바디 + &__body{ + + } +} \ No newline at end of file diff --git a/src/components/organisms/collapsibleTable/styles/index.scss b/src/components/organisms/collapsibleTable/styles/index.scss new file mode 100644 index 00000000..86e5d894 --- /dev/null +++ b/src/components/organisms/collapsibleTable/styles/index.scss @@ -0,0 +1 @@ +@import "../../../../styles/scss/index"; \ No newline at end of file diff --git a/src/components/organisms/imageBoxManager/ImageBoxManager.tsx b/src/components/organisms/imageBoxManager/ImageBoxManager.tsx new file mode 100644 index 00000000..b509c438 --- /dev/null +++ b/src/components/organisms/imageBoxManager/ImageBoxManager.tsx @@ -0,0 +1,213 @@ +/* eslint-disable */ +import React from 'react'; +import { Box, Button, IconButton, Paper } from '@mui/material'; +import { + closestCenter, + DndContext, + DragEndEvent, + KeyboardSensor, + Modifier, + MouseSensor, + PointerSensor, + TouchSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable'; +import { Draggable } from '@commons/dropAndDrop/draggable/Draggable'; +import { Droppable } from '@commons/dropAndDrop/droppable/Droppable'; +import { useRecoilState } from 'recoil'; +import { productRegisterAtom } from '@recoil/atoms/admin/product/register/ProductRegisterAtom'; +import { ImageBox } from '@molecules/imageBox/ImageBox'; +import { AddToPhotos, DisabledByDefaultRounded } from '@mui/icons-material'; +import { SortableItem } from '@commons/dropAndDrop/sortableItem/SortableItem'; +import clsN from 'classnames'; +import styles from './styles/ImageBoxManager.module.scss'; +import { ActionIcon } from '@molecules/actionIcon/ActionIcon'; + +export const ImageBoxManager = () => { + // recoil Product State + const [productData, setProductData] = useRecoilState(productRegisterAtom); + + // Dnd-Kit draggable check + const [isDraggable, setIsDraggable] = React.useState(false); + + // sortable Items : Additional image : 5 + const [sortableItems, setSortableItems] = React.useState( + // _ : unused value : underScore, from 0 ... index + 1 + // mapfc : i + 1 ... i++ + Array.from({ length: 5 }, (_, i) => i + 1), // 1,2,3,4,5 + ); + // file input ref + const fileInputRef = React.useRef<(HTMLInputElement | null)[]>([]); + + // sensor for pointer and keyboard + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + // drag start min distance + distance: 8, + delay: 100, + tolerance: 5, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 5, + }, + }), + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + // image alt context + const imageAlt = '추가 이미지'; + + // Event + + // image sort mode state + const handleDragModeToggle = () => { + setIsDraggable((prev) => !prev); + }; + + // imageItemsChangeEvent + const handleItemImageChange = (id: string) => { + // from ref activate input change event + fileInputRef.current[Number(id)]?.click(); + }; + + // imageItemsRemoveEvent + const handleItemImageRemove = (id: string) => { + setProductData((prev) => { + const newImageUrls = [...(prev.imageUrls || [])]; + // index 0 : thumbnail + newImageUrls[Number(id) + 1] = ''; + return { + ...prev, + imageUrls: newImageUrls, + }; + }); + }; + + // Drag Event + const handleDragEnd = (e: DragEndEvent) => { + const { active, over } = e; + + if (over && active.id !== over.id) { + setSortableItems((items) => { + const oldIndex = items.indexOf(Number(active.id)); + + const newIndex = items.indexOf(Number(over.id)); + + return arrayMove(items, oldIndex, newIndex); + }); + } + }; + + // JSX Element + + // element for action icon + const actionIconGen = (id: number | string) => { + return ( +
+ {/* file input area */} +
+ ); + }; + + // Draggable Element + const draggableMarkup = Drage Me; + + // extra image upload handler + const extraImageUpload = (index: number) => {}; + + // Generate Droppable Element + const GenerateDroppable = (elementSize: number, defaultContent: React.ReactNode) => { + Array.from([elementSize], () => { + return {defaultContent}; + }); + }; + + // Generate Draggable Element + + return ( + + + + + {sortableItems.map((id) => ( + + {}} + position="bottom" + actionIcon={actionIconGen(id)} + /> + + ))} + + + + + + ); +}; diff --git a/src/components/organisms/imageBoxManager/styles/ImageBoxManager.module.scss b/src/components/organisms/imageBoxManager/styles/ImageBoxManager.module.scss new file mode 100644 index 00000000..63e26bca --- /dev/null +++ b/src/components/organisms/imageBoxManager/styles/ImageBoxManager.module.scss @@ -0,0 +1,57 @@ +@import "./index"; + +.dndpaper{ + width: 100%; +} + +.sortablewrapper{ + display: flex; + gap: 8px; + justify-content: space-between; + padding-bottom: 16px; +} + +.sortablebox{ + width: 116px; + border-radius: 4px; + +} + +.imagebox{ + animation-iteration-count: inherit; + + &-jiggle{ + animation-delay: -.75s; + animation-duration: .25s; + animation-name: jiggle1; + animation-iteration-count: infinite; + animation-direction: alternate; + transform-origin: 30% 5%; + } + &__img{ + + } + &__bar{ + display: block; + &__title{ + display: none; + } + } + +} +.extra-imagebox{ + &__bar{ + width: 100%; + display: flex; + justify-content: center; + pointer-events: all; + position: relative; + } + &__input{ + + } +} + +.toggle-button{ + +} diff --git a/src/components/organisms/imageBoxManager/styles/index.scss b/src/components/organisms/imageBoxManager/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/organisms/imageBoxManager/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/templates/admin/Admin.tsx b/src/components/templates/admin/Admin.tsx index 190c5519..e3821c4a 100644 --- a/src/components/templates/admin/Admin.tsx +++ b/src/components/templates/admin/Admin.tsx @@ -56,7 +56,7 @@ const AdminTemplate = ({ children }: AdminTemplateProps) => { /> - {children} + {children} ); diff --git a/src/components/templates/admin/styles/Admin.module.scss b/src/components/templates/admin/styles/Admin.module.scss index cf470e61..1b1a988a 100644 --- a/src/components/templates/admin/styles/Admin.module.scss +++ b/src/components/templates/admin/styles/Admin.module.scss @@ -156,6 +156,15 @@ height: 100vh; } +/* 컨텐츠 블럭 */ +.content{ + box-sizing: border-box; + padding: 16px; + width: 100%; + max-width: 1140px; + margin: 0 auto; +} + /* child-padding */ .child-padding{ padding:16px; diff --git a/src/components/templates/inquiry/notices/NoticeRevising.tsx b/src/components/templates/inquiry/notices/NoticeRevising.tsx new file mode 100644 index 00000000..4fc1a890 --- /dev/null +++ b/src/components/templates/inquiry/notices/NoticeRevising.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Stack } from '@mui/material'; +import { Heading } from '@molecules/admin/layout/heading/Heading'; +import { FilterSearchRevising } from '@organisms/admin/filteredSearch/FilterSearchRevising'; +import { FilteredList } from '@organisms/admin/filteredList/FilteredList'; +import clsN from 'classnames'; +import styles from './styles/NoticeRevising.module.scss'; + +export const NoticeRevising = () => { + /* TSX 모듈 */ + /** 제목 헤드라인 */ + const Headline = ( + + ); + + /* 렌더 */ + return ( + + {Headline} + + + + ); +}; diff --git a/src/components/templates/inquiry/notices/Notices.tsx b/src/components/templates/inquiry/notices/Notices.tsx index 42060586..059ab753 100644 --- a/src/components/templates/inquiry/notices/Notices.tsx +++ b/src/components/templates/inquiry/notices/Notices.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { formatDate } from '@util/common/FormatDate'; import { Notification, NotificationProps } from '@util/test/data/admin/notification/Notification'; import { Heading } from '@molecules/admin/layout/heading/Heading'; -import { ButtonProps, SelectChangeEvent, Stack } from '@mui/material'; +import { ButtonProps, IconButton, SelectChangeEvent, Stack } from '@mui/material'; import { FilteredSearch } from '@organisms/admin/filteredSearch/FilteredSearch'; import { CollapsedListResult } from '@organisms/collapsedListResult/collapsedListResult'; import Text from '@atoms/text/Text'; @@ -14,16 +14,31 @@ import { noticesFilterStateAtom } from '@recoil/atoms/admin/inquiry/notices/noti import { filteringNotices } from '@util/test/data/admin/notification/NoticeFilter'; import { useSearchChange } from '@hooks/search/useSearchChange.hook'; import { NotificationButtonGroup } from '@util/test/data/admin/buttonGroup/notification/notificationButtonGroup'; -import { useDebounce } from '@hooks/input/useDebounce.hook'; -import clsN from 'classnames'; -import styles from './styles/Notices.module.scss'; import { ModalEditor } from '@organisms/admin/modalEditor/ModalEditor'; import { TRowTitleArea } from '@molecules/admin/notice/collapseForm/tRowTitle/TRowTitle'; import Button from '@atoms/button/Button'; import { pcickedCollapsedButton, pickedPostButton } from '@util/common/admin/data/button/buttonItems'; +import { ButtonGroup } from '@molecules/button/buttonGroup/ButtonGroup'; +import { Cancel } from '@mui/icons-material'; +import clsN from 'classnames'; +import styles from './styles/Notices.module.scss'; export const NoticesTemplate = () => { /* 상태 */ + const [anchorEl, setAnchorEl] = React.useState(null); + const [popState, setPopState] = React.useState<{ [key: string]: boolean }>({}); + const onPopChange = (uid: string) => (e: React.MouseEvent) => { + setPopState((prevState) => ({ + ...prevState, + [uid]: !prevState[uid], + })); + setAnchorEl(e.currentTarget); + }; + const onPopBackClick = () => { + setPopState({}); + setAnchorEl(null); + }; + // 커스텀 훅을 통한 상태관리 : 검색 입력 const { searchVal, handleChange, resetSearch } = useSearchChange(); // 필터 리코일 상태 @@ -37,11 +52,10 @@ export const NoticesTemplate = () => { // selectBtn 아이템들, NoticeFilterAtom 에 맞춰 가져오기 const [selectBtnState, setSelectBtnState] = React.useState(NotificationButtonGroup[0]); // Debounced 입력 상태 : 2초 제한 - const debounceSearchValue = useDebounce(searchVal, 200); - // 에디터 상태 - const [editorContent, setEditorContent] = React.useState(''); - // 에디터 ref - const editorRef = React.useRef(null); + // const debounceSearchValue = useDebounce(searchVal, 200); + // 에디터 제목 상태 + const [editorTitle, setEditorTitle] = React.useState(''); + // 모달 상태 const [modalState, setModalState] = React.useState(false); // 현재 선택된 uid 값 @@ -97,7 +111,7 @@ export const NoticesTemplate = () => { setFilterState((prev) => ({ ...prev, // 스패밍 방지 입력중인 searchVal 이 아닌 디바운스 상태값 적용 - keyword: debounceSearchValue, + // keyword: debounceSearchValue, })); }; // 페이지 전환 이벤트 @@ -111,10 +125,12 @@ export const NoticesTemplate = () => { // 페이지 초기화 setTPage(0); }; - // 에디터 입력 이벤트 - const handleEditorChange = (newConetnt: string) => { - setEditorContent(newConetnt); - }; + + // 에디터 제목 입력란 이벤트 + const handleTitleChange = React.useCallback((e: React.ChangeEvent) => { + setEditorTitle(e.target.value); + }, []); + // 에디터 활성화 : 모달 상태변경 const handleModalChange = (e: Event, reason: 'backdropClick' | 'escapeKeyDown') => { if (reason == 'backdropClick') { @@ -161,7 +177,7 @@ export const NoticesTemplate = () => { // 분기별 버튼요소 설정 const buttonItemProvider = (action: 'post' | 'edit' | undefined): ButtonProps[] => { const appendClsN = 'modal__button-'; - if (action == 'edit') { + if (action === 'edit') { return pcickedCollapsedButton.map((button) => ({ ...button, onClick: @@ -186,16 +202,18 @@ export const NoticesTemplate = () => { : undefined, })); } - if (action == 'post') { + if (action === 'post') { return pickedPostButton.map((button) => ({ ...button, onClick: + // eslint-disable-next-line no-nested-ternary button['aria-label'] === 'cancel' ? handleCancel : button['aria-label'] === 'post' ? handlePost : undefined, className: + // eslint-disable-next-line no-nested-ternary button['aria-label'] === 'cancel' ? styles[`${appendClsN}cancel`] : button['aria-label'] === 'post' @@ -213,7 +231,7 @@ export const NoticesTemplate = () => { return filteringNotices(Notification, filterState); }, [filterState]); /* JSX 모듈 */ - const headline = ; + const Headline = ; // ts 유틸 데이터 tData 에 전달하기 // TODO : GQL 적용 해야됨, 임시로 .ts 파일을 활용해 데이터 불러오기 @@ -229,14 +247,35 @@ export const NoticesTemplate = () => { // tableTitle 영역 const tRowTitle = [ handleEditClick(uid)} + onClick={onPopChange(uid)} + onClose={onPopBackClick} + innerContent={ + { + handleEditClick(uid); + }} + > + Edit + , + + + , + ]} + /> + } /* popover 컨텐츠 */ />, , , @@ -262,7 +301,7 @@ export const NoticesTemplate = () => { return ( - {headline} + {Headline} { Post { + return ( + + + + + + + + + + ); +}; diff --git a/src/components/templates/product/register/ModalProductRegister/styles/ModalProductRegister.module.scss b/src/components/templates/product/register/ModalProductRegister/styles/ModalProductRegister.module.scss new file mode 100644 index 00000000..57cbb3d5 --- /dev/null +++ b/src/components/templates/product/register/ModalProductRegister/styles/ModalProductRegister.module.scss @@ -0,0 +1,31 @@ +@import './index'; + +.modal{ + display: flex; + justify-content: center; + vertical-align: center; + &-box{ + padding: 40px; + overflow-y: scroll; + &::-webkit-scrollbar { + width: 8px; + background-color: #E8E8E8; + } + &::-webkit-scrollbar-thumb{ + border-radius: 4px; + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + background-color: #585858; + } + &__content{ + width: 864px; + padding: 24px; + } + } +} + +.component-headline{ + @include font-size(headline1) +} +.section-headline{ + @include font-size(headline2) +} \ No newline at end of file diff --git a/src/components/templates/product/register/ModalProductRegister/styles/index.scss b/src/components/templates/product/register/ModalProductRegister/styles/index.scss new file mode 100644 index 00000000..7ddf5d48 --- /dev/null +++ b/src/components/templates/product/register/ModalProductRegister/styles/index.scss @@ -0,0 +1 @@ +@import "src/styles/scss/index"; \ No newline at end of file diff --git a/src/components/templates/product/register/ProductRegister/ProductRegisterTemplate.tsx b/src/components/templates/product/register/ProductRegister/ProductRegisterTemplate.tsx new file mode 100644 index 00000000..036816db --- /dev/null +++ b/src/components/templates/product/register/ProductRegister/ProductRegisterTemplate.tsx @@ -0,0 +1,30 @@ +/* eslint-disable */ +import React from 'react'; +import { Heading } from '@molecules/admin/layout/heading/Heading'; +import { Stack } from '@mui/material'; +import { ProductRegisterButtonGroup } from '@molecules/admin/product/register/ProductRegisterButtonGroup'; +import clsN from 'classnames'; +import styles from './styles/ProductRegisterTemplate.module.scss'; + +interface ProductManagementProps { + className?: string; +} + +export const ProductRegisterTemplate = ({ className }: ProductManagementProps) => { + const Headline = ( + + ); + + return ( + + {Headline} + + + ); +}; diff --git a/src/components/templates/product/register/ProductRegister/styles/ProductRegisterTemplate.module.scss b/src/components/templates/product/register/ProductRegister/styles/ProductRegisterTemplate.module.scss new file mode 100644 index 00000000..5cbb37d2 --- /dev/null +++ b/src/components/templates/product/register/ProductRegister/styles/ProductRegisterTemplate.module.scss @@ -0,0 +1,15 @@ +@import "index"; + +.template{ + &__headline{ + display: flex; + flex-direction: column; + gap: 8px; + &__heading{ + text-align: center; + } + &__subtitle{ + text-align: center; + } + } +} \ No newline at end of file diff --git a/src/components/templates/product/register/ProductRegister/styles/index.scss b/src/components/templates/product/register/ProductRegister/styles/index.scss new file mode 100644 index 00000000..a2b479d4 --- /dev/null +++ b/src/components/templates/product/register/ProductRegister/styles/index.scss @@ -0,0 +1 @@ +@import "../../../../../../styles/scss/index"; \ No newline at end of file diff --git a/src/hooks/useDomSizeCheck.hook.ts b/src/hooks/useDomSizeCheck.hook.ts index f7bf7fee..3b01372e 100644 --- a/src/hooks/useDomSizeCheck.hook.ts +++ b/src/hooks/useDomSizeCheck.hook.ts @@ -23,8 +23,8 @@ export const useDomSizeCheckHook = (activateMenuWidth: number) => { /** * 리코일 이벤트가 발생할 때 리액트가 상태변경을 확인할 수 있도록 useEffect 활용하는 방식으로 수정 */ - useEffect(()=>{ + useEffect(() => { setIsMobile(currentDom); - },[currentDom, setIsMobile]); + }, [currentDom, setIsMobile]); return isMobile; -}; \ No newline at end of file +}; diff --git a/src/hooks/useNoticeData.ts b/src/hooks/useNoticeData.ts new file mode 100644 index 00000000..a86386e1 --- /dev/null +++ b/src/hooks/useNoticeData.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; +import { useRecoilState } from 'recoil'; +import { noticeFilterAtom } from '@recoil/atoms/admin/inquiry/notices/filter/noticeFilterAtom'; +import { EventInfoResponse } from '@interface/evnet/response/EventItemResponse'; + +export const useNoticeData = () => { + const [filterState] = useRecoilState(noticeFilterAtom); // + const [data, setData] = useState([]); // 공지글 데이터 목록 + const [loading, setLoading] = useState(false); // 로딩 현황 + + // 필터 리코일 상태가 변경될때마다... + useEffect(() => { + const fetchData = async () => { + setLoading(true); + }; + }, []); +}; diff --git a/src/interface/button/admin/product/register/ProductRegisterInterface.ts b/src/interface/button/admin/product/register/ProductRegisterInterface.ts new file mode 100644 index 00000000..0a336d87 --- /dev/null +++ b/src/interface/button/admin/product/register/ProductRegisterInterface.ts @@ -0,0 +1,69 @@ +import React from 'react'; + +export interface ProductEventType { + event: 'regist' | 'load' | 'template'; +} + +export interface ProductRegisterButtonProps { + className?: string; // 클래스명 + items: T[]; // 제너릭 컨텐츠 + itemFactor: (item: T, index: number) => React.ReactNode; // 제너릭 타입 렌더 + event: ProductEventType['event']; + onClick: () => void; +} + +// 아이템 상태 : 예약 | 중단 | 삭제 | 마감 | 이용가능 +type ItemStatus = 'RESERVED' | 'SUSPENDED' | 'DELETED' | 'SOLDOUT' | 'AVAILABLE'; + +export interface ProductRegiterItemResponse { + // 퍼블릭 ID + publicId: string; + // 카테고리 퍼블릭 ID + categoryPublicId?: string; + // 제목 + title: string; + // 상세 내용 + content: string; + // 상품 간략 설명 (소제목) + summary: string; + // 가격 + price: number; + // 판매 수 + sellCnt: number; + // 즐겨찾기 수 + wishCnt: number; + // 재고 수 + stockCnt: number; + // 조회 수 + clickCnt: number; + // 평점 + avgReview: number; + // 리뷰 수 + reviewCnt: number; + // qna 문서 수 + qnaCnt: number; + // 아이템 상태 + status: ItemStatus; + // 상점 퍼블릭 ID + storeId: string; + // 무료배송 여부 + freeDelivery: boolean; + // 상품 이미지 url 목록 + imageUrls?: string[]; + // 옵션 품목 + option?: [string]; + // 상품번호 + productNumber: string; + // 종료일 + deadline?: Date; + // 원작 + originalWork: string; + // 재질 + material: string; + // 크기 + size: string; + // 무게 + weight: string; + // 배송비 + shippingCost: number; +} diff --git a/src/interface/dndKit/DndKit.interface.ts b/src/interface/dndKit/DndKit.interface.ts new file mode 100644 index 00000000..7d7bdf38 --- /dev/null +++ b/src/interface/dndKit/DndKit.interface.ts @@ -0,0 +1,9 @@ +import { Transform } from '@dnd-kit/utilities'; +import type { Active, DragEndEvent } from '@dnd-kit/core'; + +export interface ModifierArgs { + transform: Transform; + acrive: Active; + dragOveray?: boolean; + event?: DragEndEvent; +} diff --git a/src/interface/evnet/filter/EventFilterType.ts b/src/interface/evnet/filter/EventFilterType.ts new file mode 100644 index 00000000..75cdeb61 --- /dev/null +++ b/src/interface/evnet/filter/EventFilterType.ts @@ -0,0 +1,26 @@ +export interface EventFilterType { + keyword: string; // 키워드 + eventType?: string; // 이벤트 타입 + postStatus: string; // 공개 여부 + startedAt?: Date; // 이벤트 시작일 + endedAt?: Date; // 이벤트 종료일 + postedAt?: Date; // 개시일 + fixedAt?: Date; // 수정일 +} + +// 이벤트 페이지 요청 인터페이스 +export interface EventPageRequest { + eventId: string; // 이벤트 ID + page: string; // 페이지 번호 + pageSize: string; // 페이지당 크기 +} + +// enum 을 사용하는 EventType 인터페이스 +export interface EnumValue { + name: string; +} +export interface TypenameDataResponse { + __typename: { + enumValues: EnumValue[]; + }; +} diff --git a/src/interface/evnet/label/EventLabelType.ts b/src/interface/evnet/label/EventLabelType.ts new file mode 100644 index 00000000..9323e6a3 --- /dev/null +++ b/src/interface/evnet/label/EventLabelType.ts @@ -0,0 +1,5 @@ +export interface EventLabelType { + evnetValue: string; // 이벤트 타입 Enum Value(Enum 은 string 으로 전송됨) + eng?: string; // 영어 + kor?: string; // 한국어 +} diff --git a/src/interface/evnet/response/EventItemResponse.ts b/src/interface/evnet/response/EventItemResponse.ts new file mode 100644 index 00000000..db4e6cfd --- /dev/null +++ b/src/interface/evnet/response/EventItemResponse.ts @@ -0,0 +1,16 @@ +// 이벤트 정보 조회(간략) +export interface EventItemResponse { + id: string; + startedAt: Date; + endedAt: Date; + title: string; + content?: string; + thumbnail?: string; + // eventType : EventType +} +// eventId 이용해서 클릭 시 상세정보 조회 +export interface EventInfoResponse extends EventItemResponse { + items?: string[]; + images?: string[]; + link?: string; +} diff --git a/src/interface/image/ImageInterface.ts b/src/interface/image/ImageInterface.ts new file mode 100644 index 00000000..8267cd37 --- /dev/null +++ b/src/interface/image/ImageInterface.ts @@ -0,0 +1,20 @@ +// imageBoxClasses 관련 인터페이스 +export interface imageBoxClasses { + // 이미지 Box Root 클래스명 + imageBox?: string; + // 이미지 Box 이미지 소스 클래스명 + imageBoxImg?: string; + // 이미지 Box 이미지 바 클래스명 + iamgeBoxBar?: string; +} + +export interface ImageBoxPropsInterface { + // 이미지 소스 + src?: string; + // 이미지 설명 + alt?: string; + // 클래스요소 + classes?: imageBoxClasses; + // 이미지 영역 클릭 이벤트 + onImageClick?: () => void; +} diff --git a/src/interface/layer/ProductLayerInterface.ts b/src/interface/layer/ProductLayerInterface.ts new file mode 100644 index 00000000..3796724c --- /dev/null +++ b/src/interface/layer/ProductLayerInterface.ts @@ -0,0 +1,5 @@ +export interface ProductLayerInterface { + className?: string; + parentHeadlineCleN?: string; + sectionHeadlineClsN?: string; +} diff --git a/src/pages/manager/inquiry/notices/Notices.tsx b/src/pages/manager/inquiry/notices/Notices.tsx index 19aab643..ce50f122 100644 --- a/src/pages/manager/inquiry/notices/Notices.tsx +++ b/src/pages/manager/inquiry/notices/Notices.tsx @@ -1,6 +1,7 @@ import React from 'react'; import AdminTemplate from '@templates/admin/Admin'; -import { NoticesTemplate } from '@templates/inquiry/notices/Notices'; +// import { NoticesTemplate } from '@templates/inquiry/notices/Notices'; +import { NoticeRevising } from '@templates/inquiry/notices/NoticeRevising'; /* interface NoticesProps { @@ -13,7 +14,8 @@ interface NoticesProps { export const Notices = () => { return ( - + {/* */} + ); }; diff --git a/src/pages/product/register/ProductRegister.tsx b/src/pages/product/register/ProductRegister.tsx new file mode 100644 index 00000000..71e70ddf --- /dev/null +++ b/src/pages/product/register/ProductRegister.tsx @@ -0,0 +1,17 @@ +/* eslint-disable */ +import React from 'react'; +import AdminTemplate from '@templates/admin/Admin'; +import { ProductRegisterTemplate } from '@templates/product/register/ProductRegister/ProductRegisterTemplate'; +import clsN from 'classnames'; + +// interface CreateProductProps{ +// +// } + +export const ProductRegister = () => { + return ( + + + + ); +}; diff --git a/src/recoil/atoms/admin/drawer/drawerAdminNavAtom.ts b/src/recoil/atoms/admin/drawer/drawerAdminNavAtom.ts new file mode 100644 index 00000000..f0bc2d0e --- /dev/null +++ b/src/recoil/atoms/admin/drawer/drawerAdminNavAtom.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const drawerAdminNavAtom = atom({ + key: 'drawerAdminNav', + default: false, +}); diff --git a/src/recoil/atoms/admin/inquiry/notices/filter/noticeFilterAtom.ts b/src/recoil/atoms/admin/inquiry/notices/filter/noticeFilterAtom.ts new file mode 100644 index 00000000..90147aac --- /dev/null +++ b/src/recoil/atoms/admin/inquiry/notices/filter/noticeFilterAtom.ts @@ -0,0 +1,11 @@ +import { atom } from 'recoil'; +import { EventFilterType } from '@interface/evnet/filter/EventFilterType'; + +export const noticeFilterAtom = atom({ + key: 'noticeFilter', + default: { + keyword: '', + eventType: 'all', + postStatus: 'all', + }, +}); diff --git a/src/recoil/atoms/admin/inquiry/notices/noticesFilterAtom.ts b/src/recoil/atoms/admin/inquiry/notices/noticesFilterAtom.ts index dcfb67a7..4abd46f2 100644 --- a/src/recoil/atoms/admin/inquiry/notices/noticesFilterAtom.ts +++ b/src/recoil/atoms/admin/inquiry/notices/noticesFilterAtom.ts @@ -24,3 +24,8 @@ export const noticesFilterStateAtom = atom({ keyword: undefined, }, }); + +export const eventIdAtom = atom({ + key: 'eventId', + default: '', +}); diff --git a/src/recoil/atoms/admin/product/register/ProductRegisterAtom.ts b/src/recoil/atoms/admin/product/register/ProductRegisterAtom.ts new file mode 100644 index 00000000..e9425f41 --- /dev/null +++ b/src/recoil/atoms/admin/product/register/ProductRegisterAtom.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil'; +import { ProductRegiterItemResponse } from '@interface/button/admin/product/register/ProductRegisterInterface'; +import { INITIAL_PRODUCT_STATE } from '@util/common/admin/product/ProductStateSetup'; + +export const productRegisterAtom = atom({ + key: 'productRegisterAtom', + default: INITIAL_PRODUCT_STATE, +}); diff --git a/src/routes/adminRouter/AdminRouter.tsx b/src/routes/adminRouter/AdminRouter.tsx index 16b2fa54..cd797a50 100644 --- a/src/routes/adminRouter/AdminRouter.tsx +++ b/src/routes/adminRouter/AdminRouter.tsx @@ -5,6 +5,7 @@ import Dashboard from '@pages/manager/dashboard/Dashboard'; import { Notices } from '@pages/manager/inquiry/notices/Notices'; import { AdminFaq } from '@pages/manager/inquiry/faq/Faq'; import { Management } from '@pages/product/managment/Management'; +import { ProductRegister } from '@pages/product/register/ProductRegister'; // 관리자 페이지 모듈화 export const AdminRouter = () => { @@ -15,6 +16,7 @@ export const AdminRouter = () => { } /> } /> } /> + } /> ); }; diff --git a/src/styles/scss/variables/_animations.scss b/src/styles/scss/variables/_animations.scss new file mode 100644 index 00000000..8a4c5a30 --- /dev/null +++ b/src/styles/scss/variables/_animations.scss @@ -0,0 +1,21 @@ +@keyframes jiggle1 { + 0%{ + transform: rotate(-1deg); + animation-timing-function: ease-in; + } + 50%{ + transform: rotate(1.5deg); + animation-timing-function: ease-out; + } +} + +@keyframes jiggle2 { + 0%{ + transform: rotate(1deg); + animation-timing-function: ease-in; + } + 50%{ + transform: rotate(-1.5deg); + animation-timing-function: ease-out; + } +} \ No newline at end of file diff --git a/src/styles/scss/variables/_variables.scss b/src/styles/scss/variables/_variables.scss index 31538ddc..cc87305e 100644 --- a/src/styles/scss/variables/_variables.scss +++ b/src/styles/scss/variables/_variables.scss @@ -214,6 +214,9 @@ $naver-background : #03C75A; $kakao-background : #FEE500; $google-background: #F2F2F2; +// mui default border color +$border-default-color : rgba(0,0,0,0.12); + :root { --breakpoint-desktop: #{$breakpoint-desktop}; --breakpoint-tablet: #{$breakpoint-tablet}; diff --git a/src/styles/scss/variables/mixin.scss b/src/styles/scss/variables/mixin.scss index a6fa1d89..0e3068f0 100644 --- a/src/styles/scss/variables/mixin.scss +++ b/src/styles/scss/variables/mixin.scss @@ -1,4 +1,5 @@ @import 'variables'; +@import "animations"; /* mixin 은 함수처럼 다른곳에서 사용할 수 있음 재사용성이 좋고 모듈화가 가능 */ @@ -18,6 +19,18 @@ transform: translate(-50%, -50%); } +// alt-visibility-hidden +@mixin alt-hidden{ + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + white-space: nowrap; + border: 0; +} /* portrait(초상화) - 세로모드 diff --git a/src/util/common/admin/product/ProductStateSetup.ts b/src/util/common/admin/product/ProductStateSetup.ts new file mode 100644 index 00000000..da572f0d --- /dev/null +++ b/src/util/common/admin/product/ProductStateSetup.ts @@ -0,0 +1,30 @@ +import { ProductRegiterItemResponse } from '@interface/button/admin/product/register/ProductRegisterInterface'; + +// 상품 상세 초기값 +export const INITIAL_PRODUCT_STATE: ProductRegiterItemResponse = { + publicId: '', // 퍼블릭 ID + categoryPublicId: '', // 카테고리 퍼블릭 ID + title: '', // 제목 + content: '', // 상세 내용 + summary: '', // 상품 간략 설명 (소제목) + price: 0, // 가격 + sellCnt: 0, // 판매 수 + wishCnt: 0, // 즐겨찾기 수 + stockCnt: 0, // 재고 수 + clickCnt: 0, // 조회 수 + avgReview: 0, // 평점 + reviewCnt: 0, // 리뷰 수 + qnaCnt: 0, // qna 문서 수 + status: 'AVAILABLE', // 아이템 상태 + storeId: '', // 상점 퍼블릭 ID + freeDelivery: true, // 무료배송 여부 + // imageUrls: [],// 상품 이미지 url 목록 + // option: [], // 옵션 품목 + productNumber: '', // 상품번호 + // deadline: ; // 종료일 + originalWork: '', // 원작 + material: '', // 재질 + size: '', // 크기 + weight: '', // 무게 + shippingCost: 0, // 배송비 +}; diff --git a/src/util/dayjs/DayJsUtill.ts b/src/util/dayjs/DayJsUtill.ts new file mode 100644 index 00000000..1fbe8366 --- /dev/null +++ b/src/util/dayjs/DayJsUtill.ts @@ -0,0 +1,38 @@ +import { Dayjs } from 'dayjs'; + +// 날짜 비교 -1, 0, 1 +export const compareDate = (date1: Dayjs | null, date2: Dayjs | null): number | null => { + // 인자가 null 이 아닐 경우만 + if (date1 && date2) { + if (date1.isBefore(date2)) return -1; // 인자1 이 인자2 보다 작음 (-1) + if (date1.isAfter(date2)) return 1; // 인자1 이 인자2 보다 큼 (+1) + if (date1.isSame(date2)) return 0; // 두 인자값이 같을 경우 (0) + } + if (date1 == null && date2 == null) return null; // 둘다 null 일 경우 + return 0; // 인자중 하나라도 null 일 경우 (0) +}; + +export const swapDateCheck = (fromDate: Dayjs | null, endDate: Dayjs | null): [Dayjs | null, Dayjs | null] => { + const comparedResult = compareDate(fromDate, endDate); // 날짜비교 값 상태 + switch (comparedResult) { + case -1: // 건드릴 필요가 없음 + return [fromDate, endDate]; + case null: // 둘다 null일 경우 + return [null, null]; + case 0: { + // 하나가 null 혹은 같은 상황 + const tempEndDate = fromDate?.isAfter(endDate) ? fromDate : endDate; // 종료일을... 시작일이 종료일을 넘길 경우 : 아니면 종료일 유지 + const tempStartDate = tempEndDate?.subtract(1, 'day') ?? null; // 시작을을 종료일 기준 1일 뺌, 만약 연산 실해할 경우 null 반환 + return [tempStartDate, tempEndDate]; + } + case 1: // 시작일과 종료일의 순서를 교체 + return [endDate, fromDate]; + default: + return [fromDate, endDate]; + } +}; + +// Dayjs 를 받아 YYYY.MM.DD 형태로 포맷하기 +export const formatDayjs = (date: Dayjs | null) => { + return date ? date.format('YYYY.MM.DD') : ''; +}; diff --git a/src/util/test/data/admin/buttonGroup/notification/notificationButtonGroup.ts b/src/util/test/data/admin/buttonGroup/notification/notificationButtonGroup.ts index 0693777a..88909b86 100644 --- a/src/util/test/data/admin/buttonGroup/notification/notificationButtonGroup.ts +++ b/src/util/test/data/admin/buttonGroup/notification/notificationButtonGroup.ts @@ -3,14 +3,17 @@ import { ButtonProps } from '@mui/material'; export const NotificationButtonGroup: ButtonProps[] = [ { value: '0', + title: '전체', children: 'all', }, { value: '1', + title: '공개', children: 'posted', }, { value: '2', + title: '비공개', children: 'private', }, ]; diff --git a/src/util/tinyMCE/tinyEditorPlugins.init.js b/src/util/tinyMCE/tinyEditorPlugins.init.js new file mode 100644 index 00000000..460da815 --- /dev/null +++ b/src/util/tinyMCE/tinyEditorPlugins.init.js @@ -0,0 +1,27 @@ +export const plugins = [ + 'advlist', + 'autolink', + 'lists', + 'link', + 'image', + 'charmap', + 'preview', + 'anchor', + 'searchreplace', + 'visualblocks', + 'code', + 'fullscreen', + 'insertdatetime', + 'media', + 'table', + 'code', + 'help', + 'wordcount', +]; + +export const toolbar = [ + 'undo redo | blocks | ' + + 'bold italic forecolor | alignleft aligncenter ' + + 'alignright alignjustify | bullist numlist outdent indent | link image ' + + 'removeformat | help', +]; diff --git a/yarn.lock b/yarn.lock index 61559468..2e08c86a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1310,6 +1310,37 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@dnd-kit/accessibility@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz#3b4202bd6bb370a0730f6734867785919beac6af" + integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw== + dependencies: + tslib "^2.0.0" + +"@dnd-kit/core@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.3.1.tgz#4c36406a62c7baac499726f899935f93f0e6d003" + integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ== + dependencies: + "@dnd-kit/accessibility" "^3.1.1" + "@dnd-kit/utilities" "^3.2.2" + tslib "^2.0.0" + +"@dnd-kit/sortable@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz#1f9382b90d835cd5c65d92824fa9dafb78c4c3e8" + integrity sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg== + dependencies: + "@dnd-kit/utilities" "^3.2.2" + tslib "^2.0.0" + +"@dnd-kit/utilities@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b" + integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg== + dependencies: + tslib "^2.0.0" + "@emotion/babel-plugin@^11.12.0": version "11.12.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz#7b43debb250c313101b3f885eba634f1d723fcc2"