diff --git a/src/assets/svgs/editorDropIcnActive.svg b/src/assets/svgs/editorDropIcnActive.svg new file mode 100644 index 00000000..04689d03 --- /dev/null +++ b/src/assets/svgs/editorDropIcnActive.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svgs/editorDropIcnActiveopen.svg b/src/assets/svgs/editorDropIcnActiveopen.svg new file mode 100644 index 00000000..9955823e --- /dev/null +++ b/src/assets/svgs/editorDropIcnActiveopen.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svgs/index.tsx b/src/assets/svgs/index.tsx index 50adf66c..2617b593 100644 --- a/src/assets/svgs/index.tsx +++ b/src/assets/svgs/index.tsx @@ -47,4 +47,7 @@ export { default as CheckboxIc } from './postDetailCheckIc.svg?react'; export { default as QuestionDefaultIc } from './questionDefaultIc.svg?react'; export { default as QuestioHoverIc } from './questionHoverIc.svg?react'; +export { default as EditorDropIcnActiveIc } from './editorDropIcnActive.svg?react'; +export { default as EditorDropIcnActiveOpenIc } from './editorDropIcnActiveopen.svg?react'; export { default as GroupThumnailImgIc } from './groupThumnailImg.svg?react'; + diff --git a/src/hooks/useClickOutside.tsx b/src/hooks/useClickOutside.tsx new file mode 100644 index 00000000..e9eeba61 --- /dev/null +++ b/src/hooks/useClickOutside.tsx @@ -0,0 +1,25 @@ +import React, { useEffect } from 'react'; + +const useClickOutside = (ref: React.RefObject, callback: () => void) => { + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + // 드롭다운이 열려있고, 드롭다운 외부가 클릭됐을 경우 + if (ref.current && !ref.current.contains(event.target as Node)) { + // 실행할 함수를 인자로 받아와서 실행시켜 줌 + callback(); + } + }; + + // 클릭 이벤트가 발생하면 handleClickOutside를 실행시킨다 + document.addEventListener('click', handleClickOutside); + + // 이벤트 지워주기 + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, [ref, callback]); + + return useClickOutside; +}; + +export default useClickOutside; diff --git a/src/pages/postPage/PostPage.tsx b/src/pages/postPage/PostPage.tsx index f3cde829..03613f11 100644 --- a/src/pages/postPage/PostPage.tsx +++ b/src/pages/postPage/PostPage.tsx @@ -1,10 +1,15 @@ import styled from '@emotion/styled'; +import DropDown from './components/DropDown'; import Editor from './components/Editor'; +import Spacing from '../../components/commons/Spacing'; const PostPage = () => { return ( + + + ); @@ -14,5 +19,6 @@ export default PostPage; const PostPageWrapper = styled.div` display: flex; - justify-content: center; + flex-direction: column; + align-items: center; `; diff --git a/src/pages/postPage/components/DropDown.tsx b/src/pages/postPage/components/DropDown.tsx new file mode 100644 index 00000000..a8614ab0 --- /dev/null +++ b/src/pages/postPage/components/DropDown.tsx @@ -0,0 +1,73 @@ +/* eslint-disable no-unused-vars */ +import { useState, useRef } from 'react'; + +import styled from '@emotion/styled'; + +import TopicDropDown from './TopicDropDown'; +import WriterDropDown from './WriterDropDown'; + +export interface DropDownPropsType { + onClickListItem: (key: string, value: string) => void; + selectedValue: string; +} + +const DropDown = () => { + // 드롭다운에서 선택된 값 저장 state + // 글감ID, 익명여부 저장 필요 + // 가장 최신값으로 초기값 업데이트 + const [selectedValues, setSelectedValues] = useState({ + topic: '필명에 대하여', + writer: '작자미상', + }); + + const dropDownRef = useRef(null); + + // 드롭다운 리스트 중 선택된 값 저장 이벤트 핸들러 + const handleListItem = (key: string, value: string) => { + setSelectedValues((prev) => ({ ...prev, [key]: value })); + }; + + return ( + + + + + ); +}; + +export default DropDown; + +const DropDownWrapper = styled.div` + display: flex; + flex-direction: row; + gap: 1.2rem; + align-items: center; + justify-content: flex-start; + width: 82.6rem; +`; + +export const DropDownToggle = styled.div` + position: relative; + display: flex; + align-items: center; + justify-content: flex-end; + height: 4.4rem; + padding: 0.8rem 1.6rem; + + background-color: ${({ theme }) => theme.colors.mileViolet}; + cursor: pointer; + border: 1px solid transparent; + border-radius: 8px; + + &:hover { + border: 1px solid ${({ theme }) => theme.colors.mainViolet}; + } +`; + +export const DropDownContent = styled.span<{ $contentWidth: number }>` + width: ${({ $contentWidth }) => `${$contentWidth}rem`}; + margin-right: 1rem; + + color: ${({ theme }) => theme.colors.mainViolet}; + ${({ theme }) => theme.fonts.button2}; +`; diff --git a/src/pages/postPage/components/Topic.tsx b/src/pages/postPage/components/Topic.tsx new file mode 100644 index 00000000..adb77157 --- /dev/null +++ b/src/pages/postPage/components/Topic.tsx @@ -0,0 +1,95 @@ +/* eslint-disable no-unused-vars */ +import styled from '@emotion/styled'; + +import React from 'react'; + +interface TopicPropTypes { + topicId: number; + topicName: string; + onClickHandler: (key: string, value: string) => void; + selected: boolean; + onClickClose: (state: boolean) => void; +} + +const ThisWeekTopic = (props: TopicPropTypes) => { + const { topicName, onClickHandler, selected, onClickClose } = props; + const handleListClick = (e: React.MouseEvent) => { + onClickHandler('topic', e.currentTarget.innerText); + onClickClose(false); + }; + return ( + <> + 최신 글감 + + {topicName} + + + ); +}; + +const PrevFirstTopic = (props: TopicPropTypes) => { + const { topicName, onClickHandler, selected, onClickClose } = props; + const handleListClick = (e: React.MouseEvent) => { + onClickHandler('topic', e.currentTarget.innerText); + onClickClose(false); + }; + return ( + <> + + 이전 글감 + + {topicName} + + + ); +}; + +const PrevTopic = (props: TopicPropTypes) => { + const { topicName, onClickHandler, selected, onClickClose } = props; + const handleListClick = (e: React.MouseEvent) => { + onClickHandler('topic', e.currentTarget.innerText); + onClickClose(false); + }; + return ( + + {topicName} + + ); +}; + +export { ThisWeekTopic, PrevFirstTopic, PrevTopic }; + +// 최신 글감, 이전 글감 +const TopicLog = styled.span` + width: 100%; + + color: ${({ theme }) => theme.colors.gray70}; + ${({ theme }) => theme.fonts.body7}; +`; + +// 글감 +const Topic = styled.div<{ $selected: boolean }>` + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + padding: 0.8rem 0 0.8rem 0.8rem; + + color: ${({ $selected, theme }) => ($selected ? theme.colors.mainViolet : theme.colors.gray90)}; + + cursor: pointer; + border-radius: 6px; + + &:hover { + background-color: ${({ theme }) => theme.colors.gray20}; + } + + ${({ theme }) => theme.fonts.button2}; +`; + +const Divider = styled.div` + width: 100%; + margin-bottom: 0.6rem; + + border: 1px solid ${({ theme }) => theme.colors.gray20}; +`; diff --git a/src/pages/postPage/components/TopicDropDown.tsx b/src/pages/postPage/components/TopicDropDown.tsx new file mode 100644 index 00000000..349fb8eb --- /dev/null +++ b/src/pages/postPage/components/TopicDropDown.tsx @@ -0,0 +1,119 @@ +import styled from '@emotion/styled'; + +import { useRef, useState } from 'react'; + +import { DropDownToggle, DropDownContent, DropDownPropsType } from './DropDown'; +import { ThisWeekTopic, PrevFirstTopic, PrevTopic } from './Topic'; + +import { TOPIC_DUMMY_DATA } from '../constants/topicConstants'; + +import { EditorDropIcnActiveIc, EditorDropIcnActiveOpenIc } from '../../../assets/svgs'; +import useClickOutside from '../../../hooks/useClickOutside'; + +const TopicDropDown = (props: DropDownPropsType) => { + const { onClickListItem, selectedValue } = props; + + const [topicIsOpen, setTopicIsOpen] = useState(false); + + // 드롭다운 리스트 부분 잡아오기 + const dropDownRef = useRef(null); + + // 토글 열림 닫힘만 핸들링하는 함수 + const handleOnClick = () => { + setTopicIsOpen(!topicIsOpen); + }; + // 커스텀 훅 전달 콜백 함수 + const handleOutSideClick = () => { + setTopicIsOpen(false); + }; + //커스텀 훅 사용 + useClickOutside(dropDownRef, handleOutSideClick); + + return ( + + + {selectedValue} + {topicIsOpen ? : } + + + {TOPIC_DUMMY_DATA.map((item, idx) => { + if (idx === 0) { + return ( + + ); + } else if (idx === 1) { + return ( + + ); + } else { + return ( + + ); + } + })} + + + ); +}; + +export default TopicDropDown; + +const TopicDropDownWrapper = styled.div` + position: relative; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; +`; + +const TopicListWrapper = styled.div<{ $isOpen: boolean }>` + position: absolute; + top: 4.4rem; + z-index: 3; + + display: ${({ $isOpen }) => ($isOpen ? 'flex' : 'none')}; + flex-direction: column; + gap: 0.6rem; + align-items: center; + justify-content: flex-start; + width: 36rem; + max-height: 37.1rem; + padding: 2rem; + overflow: hidden scroll; + + background-color: ${({ theme }) => theme.colors.white}; + border: 1px solid ${({ theme }) => theme.colors.gray50}; + border-radius: 10px; + + &::-webkit-scrollbar { + width: 0.4rem; + } + + &::-webkit-scrollbar-thumb { + background: ${({ theme }) => theme.colors.gray20}; + background-clip: padding-box; + border: 20px solid ${({ theme }) => theme.colors.gray20}; + border-radius: 4px; + } +`; diff --git a/src/pages/postPage/components/WriterDropDown.tsx b/src/pages/postPage/components/WriterDropDown.tsx new file mode 100644 index 00000000..f0744314 --- /dev/null +++ b/src/pages/postPage/components/WriterDropDown.tsx @@ -0,0 +1,92 @@ +import styled from '@emotion/styled'; + +import React, { useRef, useState } from 'react'; + +import { DropDownToggle, DropDownContent, DropDownPropsType } from './DropDown'; + +import { EditorDropIcnActiveIc, EditorDropIcnActiveOpenIc } from '../../../assets/svgs'; +import useClickOutside from '../../../hooks/useClickOutside'; + +const WriterDropDown = (props: DropDownPropsType) => { + const { onClickListItem, selectedValue } = props; + const [writerIsOpen, setWriterIsOpen] = useState(false); + + // 드롭다운 리스트 부분 잡아오기 + const dropDownRef = useRef(null); + // 선택된 값 저장 + const handleListClick = (e: React.MouseEvent) => { + onClickListItem('writer', e.currentTarget.innerText); + setWriterIsOpen(false); + }; + // 필명 드롭다운 버튼 누르면 열림/닫힘 + const handleOnClick = () => { + setWriterIsOpen(!writerIsOpen); + }; + // 커스텀 훅 전달 함수 + const handleOutSideClick = () => { + setWriterIsOpen(false); + }; + // 커스텀 훅 사용 + useClickOutside(dropDownRef, handleOutSideClick); + + return ( + + + {selectedValue} + {writerIsOpen ? : } + + + + 작자미상 + + + 필명 + + + + ); +}; + +export default WriterDropDown; + +const WriterDropDownWrapper = styled.div` + position: relative; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; +`; + +const WriterListWrapper = styled.div<{ $isOpen: boolean }>` + position: absolute; + top: 4.3rem; + right: 7rem; + z-index: 3; + display: ${({ $isOpen }) => ($isOpen ? 'flex' : 'none')}; + flex-direction: column; + gap: 0.6rem; + align-items: center; + justify-content: center; + width: 14.8rem; + padding: 2rem; + + background-color: ${({ theme }) => theme.colors.white}; + border: 1px solid ${({ theme }) => theme.colors.gray50}; + border-radius: 8px; +`; + +const WriterList = styled.div<{ $selected: boolean }>` + width: 10.8rem; + padding: 0.6rem 0 0.6rem 1rem; + + color: ${({ $selected, theme }) => ($selected ? theme.colors.mainViolet : theme.colors.gray90)}; + + background-color: ${({ theme }) => theme.colors.white}; + cursor: pointer; + border-radius: 6px; + ${({ theme }) => theme.fonts.button2}; + + &:hover { + background-color: ${({ theme }) => theme.colors.gray20}; + } +`; diff --git a/src/pages/postPage/constants/topicConstants.ts b/src/pages/postPage/constants/topicConstants.ts new file mode 100644 index 00000000..46acfe48 --- /dev/null +++ b/src/pages/postPage/constants/topicConstants.ts @@ -0,0 +1,29 @@ +import { topicType } from '../types/topicType'; + +// data.data.topics +export const TOPIC_DUMMY_DATA: topicType[] = [ + { + topicId: 1, + topicName: '필명에 대하여', + }, + { + topicId: 2, + topicName: '다현이에 대하여', + }, + { + topicId: 3, + topicName: '서진이에 대하여', + }, + { + topicId: 4, + topicName: '다은이에 대하여', + }, + { + topicId: 5, + topicName: '희정이에 대하여', + }, + { + topicId: 6, + topicName: '지원이에 대하여', + }, +]; diff --git a/src/pages/postPage/types/topicType.ts b/src/pages/postPage/types/topicType.ts new file mode 100644 index 00000000..da15ed60 --- /dev/null +++ b/src/pages/postPage/types/topicType.ts @@ -0,0 +1,4 @@ +export interface topicType { + topicId: number; + topicName: string; +}