Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
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)
Comment on lines +29 to 32

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

이미지 업로드 후 표시되는 토스트 메시지가 '무드보드가 생성되었습니다.'로 고정되어 있어 사용자에게 혼란을 줄 수 있습니다. 실제 동작은 커버 이미지를 변경하는 것이므로, '커버 이미지가 변경되었습니다.'와 같이 보다 정확한 메시지를 보여주는 것이 좋습니다.

이를 위해 토스트 메시지를 상태로 관리하는 것을 고려해 보세요. 예를 들어, const [toastMessage, setToastMessage] = useState('')와 같이 상태를 추가하고, 각 작업에 맞는 메시지를 설정한 후 Toast 컴포넌트에 전달할 수 있습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정해야할까요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

setTimeout을 사용하여 토스트를 숨기는 방식은 컴포넌트가 언마운트된 후에도 타이머가 실행되어 메모리 누수나 에러를 유발할 수 있습니다. useEffect 훅을 사용하여 컴포넌트가 언마운트될 때 타이머를 정리(cleanup)하는 것이 더 안전하고 권장되는 방법입니다. 이 패턴은 handleConfirm 함수(45행)에서도 사용되고 있으니 함께 개선하는 것을 고려해 보세요.

다음은 useEffect를 사용한 예시입니다:

useEffect(() => {
  if (showToast) {
    const timerId = setTimeout(() => {
      setShowToast(false);
    }, 3000);
    return () => clearTimeout(timerId);
  }
}, [showToast]);

이렇게 수정하면 각 핸들러 함수에서는 setShowToast(true)만 호출하면 됩니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정해야할까요?

} 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>
)
}
}
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 style={{ marginLeft: isOpen ? '240px' : '60px' }}>
<ExploreGrid magazines={magazines} />
<div ref={observerRef} className="h-10" />
{isFetchingNextPage && <div className="text-center py-4">로딩중...</div>}
</div>
</div>
</div>
)
}
99 changes: 55 additions & 44 deletions Mine/src/pages/magazine/SavedMagazinePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,76 @@ import { useState } from 'react'
import SavedMagazineItem from './components/SavedMagazineItem'
import ArrowPagination from './components/ArrowPagination'
import useGetLikedMagazineList from '../../hooks/useGetLikedMagazineList'
import savedbg from '../../assets/savedbg.jpg'

const CARD_WIDTH = 476
const GAP = 16
const COLUMN_STEP = CARD_WIDTH + GAP
const COLUMNS_PER_VIEW = 2
const COLUMN_STEP = (CARD_WIDTH + GAP) * COLUMNS_PER_VIEW
const ITEMS_PER_COLUMN = 2

export default function SavedMagazinePage() {
const [columnIndex, setColumnIndex] = useState(0)
const [columnIndex, setColumnIndex] = useState(0)

const { data, isLoading, isError } = useGetLikedMagazineList({
page: 0,
size: 20,
sort: ['createdAt,desc'],
})
const { data, isLoading, isError } = useGetLikedMagazineList({
page: 0,
size: 20,
sort: ['createdAt,desc'],
})

if (isLoading) return <div>로딩중...</div>
if (isError) return <div>불러오기 실패</div>
if (isLoading) return <div>로딩중...</div>
if (isError) return <div>불러오기 실패</div>

const magazines = data?.content ?? []
const magazines = data?.content ?? []

if (magazines.length === 0) {
return <div className="p-10">찜한 매거진이 아직 없어요.</div>
}
if (magazines.length === 0) {
return <div className="p-10">찜한 매거진이 아직 없어요.</div>
}

const columns = Array.from({ length: Math.ceil(magazines.length / ITEMS_PER_COLUMN) }, (_, i) =>
magazines.slice(i * ITEMS_PER_COLUMN, i * ITEMS_PER_COLUMN + ITEMS_PER_COLUMN),
)
const columns = Array.from({ length: Math.ceil(magazines.length / ITEMS_PER_COLUMN) }, (_, i) =>
magazines.slice(i * ITEMS_PER_COLUMN, i * ITEMS_PER_COLUMN + ITEMS_PER_COLUMN),
)

const TOTAL_COLUMNS = columns.length
const TOTAL_COLUMNS = columns.length

return (
<div className="relative min-h-screen bg-white overflow-x-hidden">
<div className="relative pt-25 pb-40 px-51.5">
<div className="relative">
<div className="pointer-events-none absolute left-0 top-0 bottom-0 w-51.5 z-10" />
return (
<div
className="relative min-h-screen overflow-x-hidden flex items-center"
style={{
backgroundImage: `url(${savedbg})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundAttachment: 'fixed',
}}
>
{/* 어두운 오버레이 */}
<div className="absolute inset-0 bg-gray-600-op30 pointer-events-none" />

<div className="relative overflow-x-visible overflow-y-visible w-full">
<div
className="flex gap-4 transition-transform duration-500 ease-in-out will-change-transform"
style={{ transform: `translateX(-${columnIndex * COLUMN_STEP}px)` }}
>
{columns.map((col, colIdx) => (
<div key={colIdx} className="flex flex-col gap-4 shrink-0">
{col.map((magazine) => (
<SavedMagazineItem key={magazine.magazineId} magazine={magazine} />
))}
<div className="relative w-full py-[133px] pl-[266px]">
<div className="relative">
<div className="relative overflow-x-visible overflow-y-visible w-full">
<div
className="flex gap-4 transition-transform duration-500 ease-in-out will-change-transform"
style={{ transform: `translateX(-${columnIndex * COLUMN_STEP}px)` }}
>
{columns.map((col, colIdx) => (
<div key={colIdx} className="flex flex-col gap-4 shrink-0">
{col.map((magazine) => (
<SavedMagazineItem key={magazine.magazineId} magazine={magazine} />
))}
</div>
))}
</div>
</div>
</div>
))}

<ArrowPagination
currentPage={Math.min(columnIndex + 1, TOTAL_COLUMNS)}
totalPages={TOTAL_COLUMNS}
onNext={() => setColumnIndex((prev) => Math.min(prev + 1, TOTAL_COLUMNS - 1))}
onPrev={() => setColumnIndex((prev) => Math.max(prev - 1, 0))}
/>
</div>
</div>
</div>

<ArrowPagination
currentPage={Math.min(columnIndex + 1, TOTAL_COLUMNS)}
totalPages={TOTAL_COLUMNS}
onNext={() => setColumnIndex((prev) => Math.min(prev + 1, TOTAL_COLUMNS - 1))}
onPrev={() => setColumnIndex((prev) => Math.max(prev - 1, 0))}
/>
</div>
</div>
)
)
}
8 changes: 7 additions & 1 deletion Mine/src/pages/magazine/components/ExploreGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ interface ExploreGridProps {

export default function ExploreGrid({ magazines }: ExploreGridProps) {
return (
<div className="grid grid-cols-3 gap-4">
<div
className="grid"
style={{
gridTemplateColumns: 'repeat(3, 362px)',
gap: '8px',
}}
>
{magazines.map((magazine) => (
<ExploreItem key={magazine.magazineId} magazine={magazine} />
))}
Expand Down
4 changes: 2 additions & 2 deletions Mine/src/pages/magazine/components/ExploreItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function ExploreItem({ magazine }: Props) {

return (
<div
className="flex w-90.5 h-60 px-5 py-4 overflow-hidden cursor-pointer"
className="flex justify-end items-end w-90.5 h-60 px-5 py-4 overflow-hidden cursor-pointer"
style={{
backgroundImage: safeImageUrl ? `url(${safeImageUrl})` : 'none',
backgroundSize: 'cover',
Expand All @@ -21,7 +21,7 @@ export default function ExploreItem({ magazine }: Props) {
}}
onClick={() => navigate(`/magazine/${magazine.magazineId}`)}
>
<p className="flex mt-auto ml-auto font-notoserif font-semibold24 leading-none text-white text-right">
<p className="font-notoserif font-semibold24 leading-none text-white text-right">
{magazine.title}
</p>
</div>
Expand Down
4 changes: 2 additions & 2 deletions Mine/src/pages/magazine/components/SavedMagazineItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ export default function SavedMagazineItem({ magazine }: Props) {

return (
<div
className="flex w-119 h-67 p-[16px_20px] justify-end items-end gap-2.5 shrink-0 cursor-pointer"
className="flex justify-end items-end shrink-0 cursor-pointer w-[476px] h-[268px] p-[16px_28px]"
style={{
backgroundImage: safeImageUrl ? `url(${safeImageUrl})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
onClick={() => navigate(`/magazine/${magazine.magazineId}`)}
>
<span className="text-white text-right leading-normal font-[MaruBuri] font-semibold24">
<span className="text-white text-right leading-normal font-notoserif font-semibold24">
{magazine.title}
</span>
</div>
Expand Down
Loading
Loading