diff --git a/Mine/src/api/magazine.ts b/Mine/src/api/magazine.ts index c05f09d..ae11a46 100644 --- a/Mine/src/api/magazine.ts +++ b/Mine/src/api/magazine.ts @@ -97,6 +97,11 @@ export const createMoodboard = async (magazineId: number) => { return res.data } +export const patchMagazineCover = async (id: number, coverImageUrl: string) => { + const res = await axiosInstance.patch(`api/magazines/${id}/cover`, { coverImageUrl }) + return res.data +} + export const postAddSection = async ({ magazineId, message }: RequestAddSection): Promise => { const res = await axiosInstance.post(`api/magazines/${magazineId}/interact`, { message }) return res.data @@ -110,8 +115,3 @@ export const postAddSectionInSectionPage = async ({ const res = await axiosInstance.post(`api/magazines/${magazineId}/sections/${sectionId}/interact`, { message }) return res.data } - -export const patchMagazineCover = async (id: number, coverImageUrl: string) => { - const res = await axiosInstance.patch(`api/magazines/${id}/cover`, { coverImageUrl }) - return res.data -} diff --git a/Mine/src/components/hamburgerModal/ParagraphHamburgerModal.tsx b/Mine/src/components/hamburgerModal/ParagraphHamburgerModal.tsx index 491cbd8..cc88d00 100644 --- a/Mine/src/components/hamburgerModal/ParagraphHamburgerModal.tsx +++ b/Mine/src/components/hamburgerModal/ParagraphHamburgerModal.tsx @@ -1,7 +1,9 @@ +import { useRef } from 'react' import Edit from '../../icon/edit.svg?react' import Delete from '../../icon/delete.svg?react' import { HamburgerSection } from './HamburgerSection' import useDeleteParagraph from '../../hooks/useDeleteParagraph' +import useClickOutside from '../../hooks/useClickOutside' interface ParagraphHamburgerModalProps { top: number @@ -11,7 +13,6 @@ interface ParagraphHamburgerModalProps { paragraphId?: number children?: React.ReactNode handleClose: () => void - // onEdit: (id?: number) => void } export default function ParagraphHamburgerModal({ @@ -22,6 +23,9 @@ export default function ParagraphHamburgerModal({ left, handleClose, }: ParagraphHamburgerModalProps) { + const modalRef = useRef(null) + useClickOutside(modalRef, handleClose) + const deleteParagraphMutation = useDeleteParagraph() const onDeleteClick: React.MouseEventHandler = () => { deleteParagraphMutation.mutate({ @@ -34,12 +38,13 @@ export default function ParagraphHamburgerModal({ return (
} /> } onClick={onDeleteClick} />
) -} +} \ No newline at end of file diff --git a/Mine/src/components/hamburgerModal/SectionHamburgerModal.tsx b/Mine/src/components/hamburgerModal/SectionHamburgerModal.tsx index 491a53f..f5bf27c 100644 --- a/Mine/src/components/hamburgerModal/SectionHamburgerModal.tsx +++ b/Mine/src/components/hamburgerModal/SectionHamburgerModal.tsx @@ -1,10 +1,11 @@ +import { useRef, useState } from 'react' import Edit from '../../icon/edit.svg?react' import Share from '../../icon/share.svg?react' import Delete from '../../icon/delete.svg?react' import { HamburgerSection } from './HamburgerSection' import useDeleteSection from '../../hooks/useDeleteSection' -import { useState } from 'react' import ShareModal from './ShareModal' +import useClickOutside from '../../hooks/useClickOutside' interface SectionHamburgerModalProps { top: number @@ -15,6 +16,9 @@ interface SectionHamburgerModalProps { } export default function SectionHamburgerModal({ sectionId, magazineId, top, left, handleClose }: SectionHamburgerModalProps) { + const modalRef = useRef(null) + useClickOutside(modalRef, handleClose) + const deleteSectionMutation = useDeleteSection() const [showShareModal, setShowShareModal] = useState(false) @@ -27,6 +31,7 @@ export default function SectionHamburgerModal({ sectionId, magazineId, top, left <> {!showShareModal && (
diff --git a/Mine/src/components/hamburgerModal/SidebarHamburgerModal.tsx b/Mine/src/components/hamburgerModal/SidebarHamburgerModal.tsx index 59269af..1038b30 100644 --- a/Mine/src/components/hamburgerModal/SidebarHamburgerModal.tsx +++ b/Mine/src/components/hamburgerModal/SidebarHamburgerModal.tsx @@ -1,8 +1,11 @@ +import { useState, useRef } from 'react' import Share from '../../icon/share.svg?react' import Edit from '../../icon/edit.svg?react' import Delete from '../../icon/delete.svg?react' import useDeleteMagazine from '../../hooks/useDeleteMagazine' import { HamburgerSection } from './HamburgerSection' +import useClickOutside from '../../hooks/useClickOutside' +import ShareModal from './ShareModal' interface SidebarHamburgerModalProps { top: number @@ -13,7 +16,12 @@ interface SidebarHamburgerModalProps { } export default function SidebarHamburgerModal({ id, top, left, handleClose, onEdit }: SidebarHamburgerModalProps) { + const modalRef = useRef(null) + useClickOutside(modalRef, handleClose) + const deleteMutation = useDeleteMagazine() + const [showShareModal, setShowShareModal] = useState(false) + const onDeleteClick: React.MouseEventHandler = () => { deleteMutation.mutate({ id: id }) handleClose() @@ -22,15 +30,25 @@ export default function SidebarHamburgerModal({ id, top, left, handleClose, onEd onEdit(id) handleClose() } + return ( -
- } /> - } onClick={onEditClick} /> - } onClick={onDeleteClick} /> -
+ <> + {!showShareModal && ( +
+ } onClick={() => setShowShareModal(true)} /> + } onClick={onEditClick} /> + } onClick={onDeleteClick} /> +
+ )} + + {showShareModal && ( + + )} + ) -} +} \ No newline at end of file diff --git a/Mine/src/components/settings/ScreenSettings.tsx b/Mine/src/components/settings/ScreenSettings.tsx index f3a162b..1f85c44 100644 --- a/Mine/src/components/settings/ScreenSettings.tsx +++ b/Mine/src/components/settings/ScreenSettings.tsx @@ -95,4 +95,4 @@ export default function ScreenSettings() { )} ) -} +} \ No newline at end of file diff --git a/Mine/src/hooks/useClickOutside.ts b/Mine/src/hooks/useClickOutside.ts new file mode 100644 index 0000000..88fe5a3 --- /dev/null +++ b/Mine/src/hooks/useClickOutside.ts @@ -0,0 +1,17 @@ +import { useEffect, type RefObject } from 'react' + +export default function useClickOutside( + ref: RefObject, + handler: () => void +) { + useEffect(() => { + const handleClick = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + handler() + } + } + + document.addEventListener('mousedown', handleClick) + return () => document.removeEventListener('mousedown', handleClick) + }, [ref, handler]) +} \ No newline at end of file diff --git a/Mine/src/pages/magazine/components/MagazineInfo.tsx b/Mine/src/pages/magazine/components/MagazineInfo.tsx index 02e186a..fcbeaf3 100644 --- a/Mine/src/pages/magazine/components/MagazineInfo.tsx +++ b/Mine/src/pages/magazine/components/MagazineInfo.tsx @@ -1,9 +1,12 @@ import Hamburger from '../../../icon/hamburger.svg?react' import ProfileBox from './ProfileBox' -import { useState } from 'react' +import { useRef, useState } from 'react' import SectionHamburgerModal from '../../../components/hamburgerModal/SectionHamburgerModal' +import SidebarHamburgerModal from '../../../components/hamburgerModal/SidebarHamburgerModal' import HeartCount from './HeartCount' import { useMagazine } from '../MagazineProvider' +import { createPortal } from 'react-dom' +import useUpdateMagazineTitle from '../../../hooks/useUpdateMagazineTitle' interface MagazineInfoProps { nickname?: string @@ -13,12 +16,19 @@ interface MagazineInfoProps { mode: 'section' | 'magazine' onClick?: (magazineId: number) => void } + export default function MagazineInfo({ nickname, profileImage, sectionId, mode, onClick }: MagazineInfoProps) { const magazinedata = useMagazine() const [modalPos, setModalPos] = useState({ top: 0, left: 0 }) const [isHamburgerOpen, setIsHamburgerOpen] = useState(false) + const [isEditing, setIsEditing] = useState(false) + const [draftTitle, setDraftTitle] = useState('') + const inputRef = useRef(null) + const updateTitleMutation = useUpdateMagazineTitle() + const openHamburger = () => setIsHamburgerOpen(true) const closeHamburger = () => setIsHamburgerOpen(false) + const handleHamburger = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect() setModalPos({ @@ -27,33 +37,99 @@ export default function MagazineInfo({ nickname, profileImage, sectionId, mode, }) openHamburger() } + + const beginEdit = () => { + setDraftTitle(magazinedata?.title ?? '') + setIsEditing(true) + requestAnimationFrame(() => { + inputRef.current?.focus() + }) + } + + const cancelEdit = () => { + setIsEditing(false) + setDraftTitle('') + } + + const commitEdit = () => { + const next = draftTitle.trim() + if (!next || next === magazinedata?.title) { + cancelEdit() + return + } + updateTitleMutation.mutate( + { id: magazinedata?.magazineId ?? 0, title: next, introduction: '' }, + { onSuccess: () => cancelEdit() } + ) + } + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault() + cancelEdit() + } + if (e.key === 'Enter') { + e.preventDefault() + commitEdit() + } + } + return (
-
magazinedata?.magazineId && onClick?.(magazinedata.magazineId)} - > - {magazinedata?.title} -
+ {!isEditing ? ( +
magazinedata?.magazineId && onClick?.(magazinedata.magazineId)} + > + {magazinedata?.title} +
+ ) : ( + setDraftTitle(e.target.value)} + onKeyDown={onKeyDown} + onBlur={commitEdit} + className={`bg-transparent outline-none ${mode === 'section' ? 'font-regular16 font-notoserif text-gray-600' : 'font-semibold20 font-pretendard text-gray-100'}`} + /> + )} + - {isHamburgerOpen && magazinedata?.magazineId !== undefined && sectionId !== undefined && ( + + {isHamburgerOpen && mode === 'section' && magazinedata?.magazineId !== undefined && sectionId !== undefined && ( )}
+ + + {isHamburgerOpen && mode === 'magazine' && magazinedata?.magazineId !== undefined && + createPortal( + { + closeHamburger() + beginEdit() + }} + />, + document.body + ) + }
) -} +} \ No newline at end of file diff --git a/Mine/src/pages/magazine/components/SectionContent.tsx b/Mine/src/pages/magazine/components/SectionContent.tsx index dafb2a9..77b382a 100644 --- a/Mine/src/pages/magazine/components/SectionContent.tsx +++ b/Mine/src/pages/magazine/components/SectionContent.tsx @@ -66,7 +66,6 @@ export default function SectionContent({ sectionId, magazineId }: SectionContent
- setIsScreenSettingsOpen(false)} /> )