Skip to content
Merged
2 changes: 1 addition & 1 deletion Mine/src/api/image.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { axiosInstance } from './axios'

export const uploadImage = async (file: File): Promise<string> => {
export const uploadImage = async (file: File): Promise<{ imageUrl: string }> => {
const formData = new FormData()
formData.append('file', file)
const res = await axiosInstance.post('api/images', formData)
Expand Down
5 changes: 5 additions & 0 deletions Mine/src/api/magazine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,9 @@ export const getMagazineFeed = async ({ cursorId, limit = 10 }: FeedDto): Promis
export const createMoodboard = async (magazineId: number) => {
const res = await axiosInstance.post(`api/magazines/${magazineId}/moodboards`)
return res.data
}

export const patchMagazineCover = async (id: number, coverImageUrl: string) => {
const res = await axiosInstance.patch(`api/magazines/${id}/cover`, { coverImageUrl })
return res.data
}
Binary file added Mine/src/assets/explorebg.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Mine/src/assets/savedbg.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 8 additions & 3 deletions Mine/src/components/hamburgerModal/ParagraphHamburgerModal.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,7 +13,6 @@ interface ParagraphHamburgerModalProps {
paragraphId?: number
children?: React.ReactNode
handleClose: () => void
// onEdit: (id?: number) => void
}

export default function ParagraphHamburgerModal({
Expand All @@ -22,6 +23,9 @@ export default function ParagraphHamburgerModal({
left,
handleClose,
}: ParagraphHamburgerModalProps) {
const modalRef = useRef<HTMLDivElement>(null)
useClickOutside(modalRef, handleClose)

const deleteParagraphMutation = useDeleteParagraph()
const onDeleteClick: React.MouseEventHandler<HTMLDivElement> = () => {
deleteParagraphMutation.mutate({
Expand All @@ -34,12 +38,13 @@ export default function ParagraphHamburgerModal({

return (
<div
ref={modalRef}
key={sectionId}
className="fixed flex flex-col px-1 py-1 rounded-lg bg-gray-500-op70 shadow-[0 4px 4px 0 rgba(0, 0, 0, 0.25)] z-100"
className="fixed flex flex-col px-1 py-1 rounded-lg bg-gray-500-op70 shadow-[0 4px 4px 0 rgba(0, 0, 0, 0.25)] z-100"
style={{ top: `${top}px`, left: `${left}px` }}
>
<HamburgerSection title="이름 변경" icon={<Edit />} />
<HamburgerSection title="삭제" icon={<Delete />} onClick={onDeleteClick} />
</div>
)
}
}
7 changes: 6 additions & 1 deletion Mine/src/components/hamburgerModal/SectionHamburgerModal.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,6 +16,9 @@ interface SectionHamburgerModalProps {
}

export default function SectionHamburgerModal({ sectionId, magazineId, top, left, handleClose }: SectionHamburgerModalProps) {
const modalRef = useRef<HTMLDivElement>(null)
useClickOutside(modalRef, handleClose)

const deleteSectionMutation = useDeleteSection()
const [showShareModal, setShowShareModal] = useState(false)

Expand All @@ -27,6 +31,7 @@ export default function SectionHamburgerModal({ sectionId, magazineId, top, left
<>
{!showShareModal && (
<div
ref={modalRef}
className="fixed flex flex-col px-1 py-1 rounded-lg bg-gray-500-op70 shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] z-100"
style={{ top: `${top}px`, left: `${left}px` }}
>
Expand Down
38 changes: 28 additions & 10 deletions Mine/src/components/hamburgerModal/SidebarHamburgerModal.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,7 +16,12 @@ interface SidebarHamburgerModalProps {
}

export default function SidebarHamburgerModal({ id, top, left, handleClose, onEdit }: SidebarHamburgerModalProps) {
const modalRef = useRef<HTMLDivElement>(null)
useClickOutside(modalRef, handleClose)

const deleteMutation = useDeleteMagazine()
const [showShareModal, setShowShareModal] = useState(false)

const onDeleteClick: React.MouseEventHandler<HTMLDivElement> = () => {
deleteMutation.mutate({ id: id })
handleClose()
Expand All @@ -22,15 +30,25 @@ export default function SidebarHamburgerModal({ id, top, left, handleClose, onEd
onEdit(id)
handleClose()
}

return (
<div
key={id}
className="fixed flex flex-col px-1 py-2 rounded-lg bg-gray-500-op70 shadow-[0 4px 4px 0 rgba(0, 0, 0, 0.25)] z-100"
style={{ top: `${top}px`, left: `${left}px` }}
>
<HamburgerSection title="공유" icon={<Share />} />
<HamburgerSection title="이름 변경" icon={<Edit />} onClick={onEditClick} />
<HamburgerSection title="삭제" icon={<Delete />} onClick={onDeleteClick} />
</div>
<>
{!showShareModal && (
<div
ref={modalRef}
key={id}
className="fixed flex flex-col px-1 py-2 rounded-lg bg-gray-500-op70 shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] z-100"
style={{ top: `${top}px`, left: `${left}px` }}
>
<HamburgerSection title="공유" icon={<Share />} onClick={() => setShowShareModal(true)} />
<HamburgerSection title="이름 변경" icon={<Edit />} onClick={onEditClick} />
<HamburgerSection title="삭제" icon={<Delete />} onClick={onDeleteClick} />
</div>
)}

{showShareModal && (
<ShareModal onClose={handleClose} />
)}
</>
)
}
}
13 changes: 8 additions & 5 deletions Mine/src/components/settings/ScreenSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import IconWandStars from '../../icon/wand_stars.svg?react'
import IconAddPhoto from '../../icon/add_photo_alternate.svg?react'
import useCreateMoodboard from '../../hooks/useCreateMoodboard'
import useUploadImage from '../../hooks/useUploadImage'
import usePatchMagazineCover from '../../hooks/usePatchMagazineCover'
import ConfirmModal from '../common/ConfirmModal'
import Toast from '../common/Toast'

Expand All @@ -15,6 +16,7 @@ export default function ScreenSettings() {
const fileInputRef = useRef<HTMLInputElement>(null)
const { mutateAsync: uploadImage } = useUploadImage()
const { mutateAsync: createMoodboard } = useCreateMoodboard()
const { mutateAsync: patchCover } = usePatchMagazineCover()

const handleOpenFile = () => {
fileInputRef.current?.click()
Expand All @@ -24,7 +26,8 @@ export default function ScreenSettings() {
const file = e.target.files?.[0]
if (!file) return
try {
await uploadImage(file)
const { imageUrl } = await uploadImage(file)
await patchCover({ id: Number(magazineId), coverImageUrl: imageUrl })
setShowToast(true)
setTimeout(() => setShowToast(false), 3000)
} catch (error) {
Expand Down Expand Up @@ -52,15 +55,15 @@ export default function ScreenSettings() {
<div className="w-136.5 h-36.5 flex gap-6.5">
<button
onClick={() => setIsConfirmOpen(true)}
className="flex flex-col w-65 h-full rounded-2xl border border-white/30 bg-white/10 items-center justify-center gap-3 text-white/70 transition-colors duration-150 hover:bg-white/20 hover:border-white/50 hover:text-white"
className="flex flex-col w-65 h-full rounded-2xl border border-gray-100-op40 bg-gray-500-op40 items-center justify-center gap-3 text-gray-100-op70 transition-colors duration-150 hover:bg-white/20 hover:border-gray-100-op40 hover:text-gray-100-op70"
>
<IconWandStars className="w-6 h-6 aspect-square **:stroke-current **:fill-current" />
<IconWandStars className="w-6 h-6 aspect-square **:fill-current" />
<span className="font-regular14">AI로 무드보드 생성하기</span>
</button>

<button
onClick={handleOpenFile}
className="flex flex-col w-65 h-full rounded-2xl border border-white/30 bg-white/10 items-center justify-center gap-3 text-white/70 transition-colors duration-150 hover:bg-white/20 hover:border-white/50 hover:text-white"
className="flex flex-col w-65 h-full rounded-2xl border border-gray-100-op40 bg-gray-500-op40 items-center justify-center gap-3 text-gray-100-op70 transition-colors duration-150 hover:bg-white/20 hover:border-gray-100-op40 hover:text-gray-100-op70"
>
<IconAddPhoto className="w-6 h-6 aspect-square **:stroke-current **:fill-current" />
<span className="font-regular14">컴퓨터에서 이미지 가져오기</span>
Expand All @@ -85,4 +88,4 @@ export default function ScreenSettings() {
)}
</>
)
}
}
24 changes: 24 additions & 0 deletions Mine/src/components/settings/ScreenSettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import ScreenSettings from './ScreenSettings'

interface ScreenSettingsModalProps {
isOpen: boolean
onClose: () => void
}

export default function ScreenSettingsModal({ isOpen, onClose }: ScreenSettingsModalProps) {
if (!isOpen) return null

return (
<div
className="fixed inset-0 bg-black/40 flex items-center justify-center z-999"
onClick={onClose}
>
<div
className="relative bg-gray-600-op70 rounded-2xl p-10 shadow-[0px_4px_4px_rgba(0,0,0,0.25)]"
onClick={(e) => e.stopPropagation()}
>
<ScreenSettings />
</div>
</div>
)
}
15 changes: 2 additions & 13 deletions Mine/src/components/settings/SettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useRef, useState } from 'react'
import ProfileSettings from './ProfileSettings'
import ScreenSettings from './ScreenSettings'
import InterestSettings from './InterestSettings'

import X from '../../icon/X.svg?react'
Expand All @@ -15,7 +14,7 @@ interface SettingsProps {
}

export default function SettingsModal({ onClose }: SettingsProps) {
const [activeTab, setActiveTab] = useState<'profile' | 'interest' | 'screen'>('profile')
const [activeTab, setActiveTab] = useState<'profile' | 'interest'>('profile')
const [editMode, setEditMode] = useState(false)
const [showLogoutToast, setShowLogoutToast] = useState(false)
const [showSaveToast, setShowSaveToast] = useState(false)
Expand Down Expand Up @@ -88,15 +87,6 @@ export default function SettingsModal({ onClose }: SettingsProps) {
>
관심사 설정
</button>
<button
onClick={() => {
setActiveTab('screen')
setEditMode(false)
}}
className={`text-left transition-all ${activeTab === 'screen' ? 'text-[20px] text-white font-semibold20' : 'text-[16px] text-white/50'}`}
>
화면 설정
</button>
</div>

<div className="flex-1 py-14 pr-10 overflow-hidden">
Expand All @@ -112,7 +102,6 @@ export default function SettingsModal({ onClose }: SettingsProps) {
{activeTab === 'interest' && (
<InterestSettings interests={selectedInterests} onChange={setSelectedInterests} />
)}
{activeTab === 'screen' && <ScreenSettings />}
</div>

{activeTab === 'profile' && (
Expand Down Expand Up @@ -168,4 +157,4 @@ export default function SettingsModal({ onClose }: SettingsProps) {
</div>
</div>
)
}
}
17 changes: 17 additions & 0 deletions Mine/src/hooks/useClickOutside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, type RefObject } from 'react'

export default function useClickOutside<T extends HTMLElement>(
ref: RefObject<T>,
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])
}
13 changes: 13 additions & 0 deletions Mine/src/hooks/usePatchMagazineCover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { patchMagazineCover } from '../api/magazine'

export default function usePatchMagazineCover() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, coverImageUrl }: { id: number; coverImageUrl: string }) =>
patchMagazineCover(id, coverImageUrl),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['magazineDetail'] })
},
})
}
6 changes: 3 additions & 3 deletions Mine/src/icon/wand_stars.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 32 additions & 4 deletions Mine/src/pages/magazine/ExplorePage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useEffect, useRef } from 'react'
import ExploreGrid from './components/ExploreGrid'
import useGetMagazineFeed from '../../hooks/useGetMagazineFeed'
import explorebg from '../../assets/explorebg.jpg'
import useSidebarStore from '../../stores/sidebar'

export default function ExplorePage() {
const { isOpen } = useSidebarStore()
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetMagazineFeed()
const observerRef = useRef<HTMLDivElement>(null)

Expand All @@ -23,10 +26,35 @@ export default function ExplorePage() {
}, [hasNextPage, isFetchingNextPage, fetchNextPage])

return (
<div className="min-h-screen pt-39.25 pb-10 px-32.75 relative">
<ExploreGrid magazines={magazines} />
<div ref={observerRef} className="h-10" />
{isFetchingNextPage && <div className="text-center py-4">로딩중...</div>}
<div
className="min-h-screen pt-39.25 pb-10 relative"
style={{
backgroundImage: `url(${explorebg})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundAttachment: 'fixed',
minWidth: '1200px',
}}
>
{/* 어두운 오버레이 */}
<div className="absolute inset-0 bg-gray-600-op30 pointer-events-none" />

{/* 상단 흰색 그라디언트 */}
<div
className="fixed top-0 left-0 w-full pointer-events-none z-10"
style={{
height: '244px',
background: 'linear-gradient(180deg, rgba(255, 255, 255, 0.40) 29.51%, rgba(255, 255, 255, 0.00) 93.65%)',
}}
/>

<div className="relative z-20 flex justify-center">
<div className={`transition-all duration-200 ${isOpen ? 'ml-60' : 'ml-15'}`}>
<ExploreGrid magazines={magazines} />
<div ref={observerRef} className="h-10" />
{isFetchingNextPage && <div className="text-center py-4">로딩중...</div>}
</div>
</div>
</div>
)
}
Loading
Loading