diff --git a/frontend/src/apis/writings.ts b/frontend/src/apis/writings.ts index 471fcaa89..6995fcdea 100644 --- a/frontend/src/apis/writings.ts +++ b/frontend/src/apis/writings.ts @@ -6,10 +6,11 @@ import type { GetDetailWritingsResponse, GetWritingPropertiesResponse, GetWritingResponse, - PublishWritingArgs, UpdateWritingTitleArgs, UpdateWritingOrderArgs, GetHomeWritingsResponse, + PublishWritingToTistoryArgs, + PublishWritingToMediumArgs, } from 'types/apis/writings'; // 글 생성(글 업로드): POST @@ -38,9 +39,11 @@ export const getWriting = (writingId: number): Promise => export const getWritingProperties = (writingId: number): Promise => http.get(`${writingURL}/${writingId}/properties`); -// 글 발행하기: POST -export const publishWriting = ({ writingId, body }: PublishWritingArgs) => - http.post(`${writingURL}/${writingId}/publish`, { json: body }); +export const publishWritingToTistory = ({ writingId, body }: PublishWritingToTistoryArgs) => + http.post(`${writingURL}/${writingId}/publish/tistory`, { json: body }); + +export const publishWritingToMedium = ({ writingId, body }: PublishWritingToMediumArgs) => + http.post(`${writingURL}/${writingId}/publish/medium`, { json: body }); // 카테고리 글 상세 목록 조회 : GET export const getDetailWritings = (categoryId: number): Promise => diff --git a/frontend/src/assets/icons/index.ts b/frontend/src/assets/icons/index.ts index 56cf0ed01..4df980262 100644 --- a/frontend/src/assets/icons/index.ts +++ b/frontend/src/assets/icons/index.ts @@ -27,3 +27,6 @@ export { ReactComponent as DonggleIcon } from './donggle-logo.svg'; export { ReactComponent as BlurBackgroundIcon } from './blur-background.svg'; export { ReactComponent as HyperlinkIcon } from './hyperlink.svg'; export { ReactComponent as HomeBorderIcon } from './home-border.svg'; +export { ReactComponent as TimeIcon } from './time.svg'; +export { ReactComponent as PasswordIcon } from './password.svg'; +export { ReactComponent as PublishIcon } from './publish.svg'; diff --git a/frontend/src/assets/icons/password.svg b/frontend/src/assets/icons/password.svg new file mode 100644 index 000000000..b296f164d --- /dev/null +++ b/frontend/src/assets/icons/password.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/icons/publish.svg b/frontend/src/assets/icons/publish.svg new file mode 100644 index 000000000..8fd420e74 --- /dev/null +++ b/frontend/src/assets/icons/publish.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/frontend/src/assets/icons/time.svg b/frontend/src/assets/icons/time.svg new file mode 100644 index 000000000..74ef7584c --- /dev/null +++ b/frontend/src/assets/icons/time.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/@common/Menu/Menu.stories.tsx b/frontend/src/components/@common/Menu/Menu.stories.tsx index 94d5386c7..aaa6e62ff 100644 --- a/frontend/src/components/@common/Menu/Menu.stories.tsx +++ b/frontend/src/components/@common/Menu/Menu.stories.tsx @@ -7,7 +7,6 @@ const meta = { title: 'common/Menu', args: { isOpen: true, - closeMenu: () => {}, verticalDirection: 'down', horizonDirection: 'left', }, @@ -15,9 +14,6 @@ const meta = { isOpen: { description: '메뉴의 상태입니다.', }, - closeMenu: { - description: '메뉴를 닫는 핸들러 함수입니다.', - }, verticalDirection: { description: '메뉴가 수직을 기준으로 렌더링 되는 위치입니다.', }, @@ -45,7 +41,6 @@ export const Playground: Story = { diff --git a/frontend/src/components/@common/Menu/Menu.tsx b/frontend/src/components/@common/Menu/Menu.tsx index fd2cbf363..77487d34b 100644 --- a/frontend/src/components/@common/Menu/Menu.tsx +++ b/frontend/src/components/@common/Menu/Menu.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren } from 'react'; +import { PropsWithChildren, forwardRef } from 'react'; import styled from 'styled-components'; import Item from './Item'; @@ -7,14 +7,12 @@ type HorizonDirection = 'left' | 'right'; type Props = { isOpen: boolean; - closeMenu: () => void; verticalDirection?: VerticalDirection; horizonDirection?: HorizonDirection; } & PropsWithChildren; const Menu = ({ isOpen, - closeMenu, verticalDirection = 'down', horizonDirection = 'left', children, @@ -23,7 +21,6 @@ const Menu = ({ return ( - {children} @@ -38,18 +35,6 @@ export default Menu; const S = { Menu: styled.div``, - Backdrop: styled.button` - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 100vw; - height: 100vh; - - cursor: default; - opacity: 0.1; - `, - MenuList: styled.ul<{ $verticalDirection: VerticalDirection; $horizonDirection: HorizonDirection; diff --git a/frontend/src/components/HelpMenu/HelpMenu.tsx b/frontend/src/components/HelpMenu/HelpMenu.tsx index 6ed63ab56..cd07da605 100644 --- a/frontend/src/components/HelpMenu/HelpMenu.tsx +++ b/frontend/src/components/HelpMenu/HelpMenu.tsx @@ -1,9 +1,15 @@ import Menu from 'components/@common/Menu/Menu'; -import { useState } from 'react'; +import useOutsideClickEffect from 'hooks/@common/useOutsideClickEffect'; +import { useRef, useState } from 'react'; import styled from 'styled-components'; const HelpMenu = () => { const [isOpen, setIsOpen] = useState(false); + const menuRef = useRef(null); + + const closeMenu = () => setIsOpen(false); + useOutsideClickEffect(menuRef, closeMenu); + const helpMenus = [ { title: '동글 위키보러 가기', @@ -18,17 +24,19 @@ const HelpMenu = () => { }, ]; - const closeMenu = () => setIsOpen(false); - return ( - setIsOpen(!isOpen)}> - ? - - {helpMenus.map(({ title, handleMenuItemClick }) => { - return ; - })} - - +
+ setIsOpen(!isOpen)}> + ? + + {helpMenus.map(({ title, handleMenuItemClick }) => { + return ( + + ); + })} + + +
); }; diff --git a/frontend/src/components/PublishingPropertySection/MediumPublishingPropertySection.tsx b/frontend/src/components/PublishingPropertySection/MediumPublishingPropertySection.tsx new file mode 100644 index 000000000..f4911597f --- /dev/null +++ b/frontend/src/components/PublishingPropertySection/MediumPublishingPropertySection.tsx @@ -0,0 +1,85 @@ +import { css, styled } from 'styled-components'; +import TagInput from '../@common/TagInput/TagInput'; +import Button from '../@common/Button/Button'; +import Spinner from 'components/@common/Spinner/Spinner'; +import { LeftArrowHeadIcon, TagIcon } from 'assets/icons'; +import { slideToLeft } from 'styles/animation'; +import { TabKeys } from 'components/WritingSideBar/WritingSideBar'; +import { useMediumPublishingPropertySection } from './useMediumPublishingPropertySection'; +import { default as S } from './PublishingPropertyStyle'; +import type { Blog } from 'types/domain'; + +type Props = { + writingId: number; + publishTo: Blog; + selectCurrentTab: (tabKey: TabKeys) => void; +}; + +enum MediumPublishStatus { + 'PUBLIC' = 'Public', + 'PRIVATE' = 'Draft', + 'PROTECT' = 'Unlisted', +} + +const MediumPublishStatusList = Object.keys( + MediumPublishStatus, +) as (keyof typeof MediumPublishStatus)[]; + +const MediumPublishingPropertySection = ({ writingId, publishTo, selectCurrentTab }: Props) => { + const { isLoading, setTags, setPublishStatus, publishWritingToMedium } = + useMediumPublishingPropertySection({ + selectCurrentTab, + }); + + if (isLoading) + return ( + + 글을 발행하고 있어요 + + + ); + + return ( + + + + 발행 정보 + + + + 발행 방식 +
+ +
+
+ + + + 태그 + +
+ +
+
+
+ +
+ ); +}; + +export default MediumPublishingPropertySection; diff --git a/frontend/src/components/PublishingPropertySection/PublishingPropertySection.stories.tsx b/frontend/src/components/PublishingPropertySection/PublishingPropertySection.stories.tsx index 0205a17f0..560ae74a8 100644 --- a/frontend/src/components/PublishingPropertySection/PublishingPropertySection.stories.tsx +++ b/frontend/src/components/PublishingPropertySection/PublishingPropertySection.stories.tsx @@ -1,10 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react'; import { StoryContainer } from 'styles/storybook'; -import PublishingPropertySection from './PublishingPropertySection'; +import MediumPublishingPropertySection from './MediumPublishingPropertySection'; -const meta: Meta = { +const meta: Meta = { title: 'publishing/PublishingPropertySection', - component: PublishingPropertySection, + component: MediumPublishingPropertySection, }; export default meta; @@ -13,7 +13,11 @@ type Story = StoryObj; export const Primary: Story = { render: () => ( - {}} /> + {}} + /> ), }; diff --git a/frontend/src/components/PublishingPropertySection/PublishingPropertySection.tsx b/frontend/src/components/PublishingPropertySection/PublishingPropertySection.tsx deleted file mode 100644 index 6de0eb178..000000000 --- a/frontend/src/components/PublishingPropertySection/PublishingPropertySection.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { css, styled } from 'styled-components'; -import TagInput from '../@common/TagInput/TagInput'; -import Button from '../@common/Button/Button'; -import Spinner from 'components/@common/Spinner/Spinner'; -import { LeftArrowHeadIcon, TagIcon } from 'assets/icons'; -import { slideToLeft } from 'styles/animation'; -import { TabKeys } from 'components/WritingSideBar/WritingSideBar'; -import { usePublishingPropertySection } from './usePublishingPropertySection'; -import type { Blog } from 'types/domain'; - -type Props = { - writingId: number; - publishTo: Blog; - selectCurrentTab: (tabKey: TabKeys) => void; -}; - -const PublishingPropertySection = ({ writingId, publishTo, selectCurrentTab }: Props) => { - const { isLoading, setTags, publishWritingToBlog } = usePublishingPropertySection({ - selectCurrentTab, - }); - - if (isLoading) - return ( - - 글을 발행하고 있어요 - - - ); - - return ( - - - - 발행 정보 - - - - - - Tags - -
- -
-
-
- -
- ); -}; - -export default PublishingPropertySection; - -const S = { - PublishingPropertySection: styled.section<{ $blog: Blog }>` - display: flex; - flex-direction: column; - gap: 2rem; - animation: ${slideToLeft} 0.5s; - - ${({ theme, $blog }) => css` - & > button { - outline-color: ${theme.color[$blog.toLowerCase()]}; - background-color: ${theme.color[$blog.toLowerCase()]}; - - &:hover { - background-color: ${theme.color[$blog.toLowerCase()]}; - } - } - `}; - `, - SectionHeader: styled.h1` - display: flex; - gap: 1.5rem; - font-size: 1.5rem; - font-weight: 700; - line-height: 1.5rem; - `, - Properties: styled.div` - padding: 0 0 1rem 0.9rem; - `, - PropertyRow: styled.div` - display: flex; - align-items: flex-start; - `, - PropertyName: styled.div` - display: flex; - align-items: center; - flex-shrink: 0; - width: 9.5rem; - height: 2.3rem; - color: ${({ theme }) => theme.color.gray8}; - font-size: 1.3rem; - font-weight: 600; - `, - LoadingWrapper: styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 4rem; - font-size: 1.3rem; - `, -}; diff --git a/frontend/src/components/PublishingPropertySection/PublishingPropertyStyle.ts b/frontend/src/components/PublishingPropertySection/PublishingPropertyStyle.ts new file mode 100644 index 000000000..45c485e69 --- /dev/null +++ b/frontend/src/components/PublishingPropertySection/PublishingPropertyStyle.ts @@ -0,0 +1,83 @@ +import { css, styled } from 'styled-components'; +import { slideToLeft } from 'styles/animation'; +import { Blog } from 'types/domain'; + +const PublishingPropertyStyle = { + PublishingPropertySection: styled.section<{ $blog: Blog }>` + display: flex; + flex-direction: column; + gap: 2rem; + animation: ${slideToLeft} 0.5s; + + ${({ theme, $blog }) => css` + & > button { + outline-color: ${theme.color[$blog.toLowerCase()]}; + background-color: ${theme.color[$blog.toLowerCase()]}; + + &:hover { + background-color: ${theme.color[$blog.toLowerCase()]}; + } + } + `}; + `, + SectionHeader: styled.h1` + display: flex; + gap: 1.5rem; + font-size: 1.5rem; + font-weight: 700; + line-height: 1.5rem; + `, + Properties: styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + padding: 0 0 1rem 0.9rem; + `, + PropertyRow: styled.div` + display: flex; + align-items: center; + + select, + input { + padding: 0.6rem; + } + `, + PropertyName: styled.div` + display: flex; + align-items: center; + gap: 0.6rem; + flex-shrink: 0; + width: 9.5rem; + color: ${({ theme }) => theme.color.gray8}; + font-size: 1.3rem; + font-weight: 600; + `, + LoadingWrapper: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4rem; + font-size: 1.3rem; + `, + PublishTimeInputContainer: styled.div` + display: flex; + flex-direction: column; + gap: 0.4rem; + `, + PublishButtonContainer: styled.div` + display: flex; + align-items: center; + gap: 0.6rem; + `, + PublishButton: styled.button<{ selected: boolean }>` + color: ${({ theme, selected }) => !selected && theme.color.gray5}; + `, + PublishButtonAndTimeInputContainer: styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + `, +}; + +export default PublishingPropertyStyle; diff --git a/frontend/src/components/PublishingPropertySection/TistoryPublishingPropertySection.tsx b/frontend/src/components/PublishingPropertySection/TistoryPublishingPropertySection.tsx new file mode 100644 index 000000000..8d0ed4389 --- /dev/null +++ b/frontend/src/components/PublishingPropertySection/TistoryPublishingPropertySection.tsx @@ -0,0 +1,157 @@ +import { css, styled } from 'styled-components'; +import TagInput from '../@common/TagInput/TagInput'; +import Button from '../@common/Button/Button'; +import Spinner from 'components/@common/Spinner/Spinner'; +import { LeftArrowHeadIcon, PasswordIcon, PublishIcon, TagIcon, TimeIcon } from 'assets/icons'; +import { slideToLeft } from 'styles/animation'; +import { TabKeys } from 'components/WritingSideBar/WritingSideBar'; +import { useTistoryPublishingPropertySection } from './useTistoryPublishingPropertySection'; +import { default as S } from './PublishingPropertyStyle'; +import type { Blog } from 'types/domain'; +import Input from 'components/@common/Input/Input'; +import { dateFormatter } from 'utils/date'; +import { useState } from 'react'; +import Divider from 'components/@common/Divider/Divider'; + +type Props = { + writingId: number; + publishTo: Blog; + selectCurrentTab: (tabKey: TabKeys) => void; +}; + +enum TistoryPublishStatus { + 'PUBLIC' = '공개', + 'PRIVATE' = '비공개', + 'PROTECT' = '보호', +} + +const TistoryPublishStatusList = Object.keys( + TistoryPublishStatus, +) as (keyof typeof TistoryPublishStatus)[]; + +const TistoryPublishingPropertySection = ({ writingId, publishTo, selectCurrentTab }: Props) => { + const { + isLoading, + propertyFormInfo, + setTags, + setPublishStatus, + passwordRef, + dateRef, + timeRef, + publishWritingToTistory, + } = useTistoryPublishingPropertySection({ + selectCurrentTab, + }); + const [isPublishTimeInputOpen, setIsPublishTimeInputOpen] = useState(false); + + const { publishStatus } = propertyFormInfo; + + const openPublishTimeInput = () => { + setIsPublishTimeInputOpen(true); + }; + + const closePublishTimeInput = () => { + setIsPublishTimeInputOpen(false); + }; + + if (isLoading) + return ( + + 글을 발행하고 있어요 + + + ); + + return ( + + + + 발행 정보 + + + + + + + 발행 방식 + +
+ +
+
+ {publishStatus === 'PROTECT' && ( + + + + 비밀번호 + +
+ +
+
+ )} + + + + 발행 시간 + + + + + 현재 + + + + 예약 + + + {isPublishTimeInputOpen && ( + + + + + )} + + + + + + 태그 + +
+ +
+
+
+ +
+ ); +}; + +export default TistoryPublishingPropertySection; diff --git a/frontend/src/components/PublishingPropertySection/useMediumPublishingPropertySection.ts b/frontend/src/components/PublishingPropertySection/useMediumPublishingPropertySection.ts new file mode 100644 index 000000000..db3381b59 --- /dev/null +++ b/frontend/src/components/PublishingPropertySection/useMediumPublishingPropertySection.ts @@ -0,0 +1,41 @@ +import { useMutation } from '@tanstack/react-query'; +import { publishWritingToMedium as publishWritingToMediumRequest } from 'apis/writings'; +import { TabKeys } from 'components/WritingSideBar/WritingSideBar'; +import { useToast } from 'hooks/@common/useToast'; +import { useState } from 'react'; +import type { PublishWritingToMediumRequest } from 'types/apis/writings'; +import { HttpError } from 'utils/apis/HttpError'; + +type Args = { + selectCurrentTab: (tabKey: TabKeys) => void; +}; + +export const useMediumPublishingPropertySection = ({ selectCurrentTab }: Args) => { + const [propertyFormInfo, setPropertyFormInfo] = useState({ + tags: [], + publishStatus: 'PUBLIC', + }); + const toast = useToast(); + const { mutate: publishWritingToMedium, isLoading } = useMutation( + (writingId: number) => publishWritingToMediumRequest({ writingId, body: propertyFormInfo }), + { + onSuccess: () => { + selectCurrentTab(TabKeys.WritingProperty); + toast.show({ type: 'success', message: '글 발행에 성공했습니다.' }); + }, + onError: (error) => { + if (error instanceof HttpError) toast.show({ type: 'error', message: error.message }); + }, + }, + ); + + const setTags = (tags: PublishWritingToMediumRequest['tags']) => { + setPropertyFormInfo((prev) => ({ ...prev, tags })); + }; + + const setPublishStatus = (publishStatus: PublishWritingToMediumRequest['publishStatus']) => { + setPropertyFormInfo((prev) => ({ ...prev, publishStatus })); + }; + + return { isLoading, setTags, setPublishStatus, publishWritingToMedium }; +}; diff --git a/frontend/src/components/PublishingPropertySection/usePublishingPropertySection.ts b/frontend/src/components/PublishingPropertySection/usePublishingPropertySection.ts deleted file mode 100644 index 8b9001121..000000000 --- a/frontend/src/components/PublishingPropertySection/usePublishingPropertySection.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; -import { publishWriting as PublishWritingRequest } from 'apis/writings'; -import { TabKeys } from 'components/WritingSideBar/WritingSideBar'; -import { useToast } from 'hooks/@common/useToast'; -import { useState } from 'react'; -import { PublishWritingArgs } from 'types/apis/writings'; -import { Blog, PublishingPropertyData } from 'types/domain'; -import { HttpError } from 'utils/apis/HttpError'; - -type Args = { - selectCurrentTab: (tabKey: TabKeys) => void; -}; - -type PublishWritingToBlogArgs = { - writingId: number; - publishTo: Blog; -}; - -export const usePublishingPropertySection = ({ selectCurrentTab }: Args) => { - const [propertyFormInfo, setPropertyFormInfo] = useState({ tags: [] }); - const toast = useToast(); - const { mutate: publishWriting, isLoading } = useMutation(PublishWritingRequest, { - onSuccess: () => { - selectCurrentTab(TabKeys.WritingProperty); - toast.show({ type: 'success', message: '글 발행에 성공했습니다.' }); - }, - onError: (error) => { - if (error instanceof HttpError) toast.show({ type: 'error', message: error.message }); - }, - }); - - const publishWritingToBlog = ({ writingId, publishTo }: PublishWritingToBlogArgs) => { - const body = { - publishTo, - tags: propertyFormInfo.tags, - }; - - publishWriting({ writingId, body }); - }; - - const setTags = (tags: string[]) => { - setPropertyFormInfo((prev) => ({ ...prev, tags })); - }; - - return { isLoading, setTags, publishWritingToBlog }; -}; diff --git a/frontend/src/components/PublishingPropertySection/useTistoryPublishingPropertySection.ts b/frontend/src/components/PublishingPropertySection/useTistoryPublishingPropertySection.ts new file mode 100644 index 000000000..6ba9712ce --- /dev/null +++ b/frontend/src/components/PublishingPropertySection/useTistoryPublishingPropertySection.ts @@ -0,0 +1,70 @@ +import { useMutation } from '@tanstack/react-query'; +import { publishWritingToTistory as publishWritingToTistoryRequest } from 'apis/writings'; +import { TabKeys } from 'components/WritingSideBar/WritingSideBar'; +import { useToast } from 'hooks/@common/useToast'; +import { useRef, useState } from 'react'; +import type { PublishWritingToTistoryRequest } from 'types/apis/writings'; +import { HttpError } from 'utils/apis/HttpError'; + +type Args = { + selectCurrentTab: (tabKey: TabKeys) => void; +}; + +export const useTistoryPublishingPropertySection = ({ selectCurrentTab }: Args) => { + const passwordRef = useRef(null); + const dateRef = useRef(null); + const timeRef = useRef(null); + + const [propertyFormInfo, setPropertyFormInfo] = useState({ + tags: [], + publishStatus: 'PUBLIC', + password: '', + categoryId: '0', // TODO: 카테고리 선택 기능 추가 + publishTime: '', + }); + const toast = useToast(); + const { mutate: publishWritingToTistory, isLoading } = useMutation( + (writingId: number) => { + const publishTime = + dateRef.current && timeRef.current + ? `${dateRef.current.value} ${timeRef.current.value}:59.999` + : ''; + return publishWritingToTistoryRequest({ + writingId, + body: { + ...propertyFormInfo, + password: passwordRef.current?.value ?? '', + publishTime, + }, + }); + }, + { + onSuccess: () => { + selectCurrentTab(TabKeys.WritingProperty); + toast.show({ type: 'success', message: '글 발행에 성공했습니다.' }); + }, + onError: (error) => { + if (error instanceof HttpError) toast.show({ type: 'error', message: error.message }); + }, + }, + ); + + const setTags = (tags: PublishWritingToTistoryRequest['tags']) => { + setPropertyFormInfo((prev) => ({ ...prev, tags })); + }; + + const setPublishStatus = (publishStatus: PublishWritingToTistoryRequest['publishStatus']) => { + setPropertyFormInfo((prev) => ({ ...prev, publishStatus })); + }; + + return { + isLoading, + propertyFormInfo, + setTags, + setPublishStatus, + passwordRef, + dateRef, + timeRef, + publishWritingToTistory, + }; +}; diff --git a/frontend/src/components/PublishingSection/PublishingSection.tsx b/frontend/src/components/PublishingSection/PublishingSection.tsx index 98c9104d3..690005c1e 100644 --- a/frontend/src/components/PublishingSection/PublishingSection.tsx +++ b/frontend/src/components/PublishingSection/PublishingSection.tsx @@ -12,7 +12,16 @@ type Props = { const PublishingSection = ({ onTabClick, onBlogButtonClick }: Props) => { const openPublishingPropertySection = (blog: Blog) => { onBlogButtonClick(blog); - onTabClick(TabKeys.PublishingProperty); + + switch (blog) { + case 'MEDIUM': + onTabClick(TabKeys.MediumPublishingProperty); + break; + + case 'TISTORY': + onTabClick(TabKeys.TistoryPublishingProperty); + break; + } }; return ( diff --git a/frontend/src/components/WritingPropertySection/WritingPropertySection.tsx b/frontend/src/components/WritingPropertySection/WritingPropertySection.tsx index b20e212aa..0e37feeaa 100644 --- a/frontend/src/components/WritingPropertySection/WritingPropertySection.tsx +++ b/frontend/src/components/WritingPropertySection/WritingPropertySection.tsx @@ -129,7 +129,7 @@ const S = { InfoContent: styled.div` display: flex; flex-direction: column; - gap: 0.8rem; + gap: 1.8rem; padding: 1.6rem 0.9rem; font-size: 1.3rem; line-height: 1.3rem; @@ -141,11 +141,9 @@ const S = { PropertyName: styled.div` display: flex; align-items: center; - align-self: flex-start; flex-shrink: 0; gap: 0.4rem; width: 9.5rem; - height: 2.3rem; color: ${({ theme }) => theme.color.gray8}; font-size: 1.3rem; font-weight: 600; diff --git a/frontend/src/components/WritingSideBar/WritingSideBar.tsx b/frontend/src/components/WritingSideBar/WritingSideBar.tsx index c6e8768cd..855ad3879 100644 --- a/frontend/src/components/WritingSideBar/WritingSideBar.tsx +++ b/frontend/src/components/WritingSideBar/WritingSideBar.tsx @@ -1,4 +1,4 @@ -import PublishingPropertySection from 'components/PublishingPropertySection/PublishingPropertySection'; +import MediumPublishingPropertySection from 'components/PublishingPropertySection/MediumPublishingPropertySection'; import PublishingSection from 'components/PublishingSection/PublishingSection'; import { css, styled } from 'styled-components'; import { useCurrentTab } from './useCurrentTab'; @@ -8,17 +8,21 @@ import { Blog } from 'types/domain'; import WritingPropertySection from 'components/WritingPropertySection/WritingPropertySection'; import { useGlobalStateValue } from '@yogjin/react-global-state'; import { activeWritingInfoState } from 'globalState'; +import Button from 'components/@common/Button/Button'; +import TistoryPublishingPropertySection from 'components/PublishingPropertySection/TistoryPublishingPropertySection'; export enum TabKeys { WritingProperty = 'WritingProperty', Publishing = 'Publishing', - PublishingProperty = 'PublishingProperty', + MediumPublishingProperty = 'MediumPublishingProperty', + TistoryPublishingProperty = 'TistoryPublishingProperty', } const ariaLabelFromTabKeys = { [TabKeys.WritingProperty]: '글 정보', [TabKeys.Publishing]: '발행 하기', - [TabKeys.PublishingProperty]: '발행 정보', + [TabKeys.MediumPublishingProperty]: '미디엄 발행 정보', + [TabKeys.TistoryPublishingProperty]: '티스토리 발행 정보', }; type Props = { @@ -60,10 +64,21 @@ const WritingSideBar = ({ isPublishingSectionActive = true }: Props) => { ), }, { - key: TabKeys.PublishingProperty, - label: 'PublishingProperty', + key: TabKeys.MediumPublishingProperty, + label: 'MediumPublishingProperty', content: publishTo && ( - + ), + }, + { + key: TabKeys.TistoryPublishingProperty, + label: 'TistoryPublishingProperty', + content: publishTo && ( + { {menus - .filter((menu) => menu.key !== TabKeys.PublishingProperty) + .filter( + (menu) => + ![TabKeys.TistoryPublishingProperty, TabKeys.MediumPublishingProperty].includes( + menu.key, + ), + ) .map((menu) => ( , + onClickOutside: (e: React.MouseEvent) => void, +) => { + useEffect(() => { + const handleClickOutside = (e: any) => { + if (ref.current && !ref.current.contains(e.target)) { + onClickOutside(e); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [ref, onClickOutside]); +}; + +export default useOutsideClickEffect; diff --git a/frontend/src/types/apis/writings.ts b/frontend/src/types/apis/writings.ts index 9524201af..cebe0aa1c 100644 --- a/frontend/src/types/apis/writings.ts +++ b/frontend/src/types/apis/writings.ts @@ -1,4 +1,4 @@ -import { Blog, PublishingPropertyData } from 'types/domain'; +import { Blog } from 'types/domain'; import { CategoryResponse } from './category'; export type AddWritingRequest = FormData; @@ -19,13 +19,27 @@ export type GetWritingPropertiesResponse = { publishedDetails: PublishedDetail[]; }; -export type PublishWritingRequest = { - publishTo: Blog; -} & PublishingPropertyData; +export type PublishWritingToTistoryRequest = { + tags: string[]; + publishStatus: 'PUBLIC' | 'PRIVATE' | 'PROTECT'; + password: string; + categoryId: string; + publishTime: string; // "yyyy-MM-dd HH:mm:ss.SSS" 형식 +}; + +export type PublishWritingToTistoryArgs = { + writingId: number; + body: PublishWritingToTistoryRequest; +}; + +export type PublishWritingToMediumRequest = { + tags: string[]; + publishStatus: 'PUBLIC' | 'PRIVATE' | 'PROTECT'; +}; -export type PublishWritingArgs = { +export type PublishWritingToMediumArgs = { writingId: number; - body: PublishWritingRequest; + body: PublishWritingToMediumRequest; }; export type PublishedDetail = { diff --git a/frontend/src/types/domain.ts b/frontend/src/types/domain.ts index b68b01ca1..afd92724b 100644 --- a/frontend/src/types/domain.ts +++ b/frontend/src/types/domain.ts @@ -1,7 +1,3 @@ import { BLOG_LIST } from 'constants/blog'; export type Blog = (typeof BLOG_LIST)[keyof typeof BLOG_LIST]; - -export type PublishingPropertyData = { - tags: string[]; -}; diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts index eb5c41de4..97a9a1a0c 100644 --- a/frontend/src/utils/date.ts +++ b/frontend/src/utils/date.ts @@ -1,7 +1,13 @@ -type dateFormate = 'YYYY.MM.DD.' | 'YYYY/MM/DD HH:MM'; +type dateFormate = 'YYYY-MM-DD' | 'YYYY.MM.DD.' | 'YYYY/MM/DD HH:MM' | 'HH:MM'; export const dateFormatter = (date: Date, format: dateFormate) => { switch (format) { + case 'YYYY-MM-DD': + return new Intl.DateTimeFormat('en-CA', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }).format(date); case 'YYYY.MM.DD.': return new Intl.DateTimeFormat('ko-KR').format(new Date(date)); case 'YYYY/MM/DD HH:MM': @@ -13,5 +19,12 @@ export const dateFormatter = (date: Date, format: dateFormate) => { const minutes = String(d.getMinutes()).padStart(2, '0'); return `${year}/${month}/${day} ${hours}:${minutes}`; + case 'HH:MM': + const today = new Date(date); + + return `${String(today.getHours()).padStart(2, '0')}:${String(today.getMinutes()).padStart( + 2, + '0', + )}`; } };