@@ -13,7 +13,7 @@ export default function PreferenceFinishPage() {
축하해요!
- {likedWorks.length}개의 새로운 관심 작품이 등록됐어요!
+ {likedSuccessCount}개의 새로운 관심 작품이 등록됐어요!
피드에서 작품의 소식을 확인해봐요!
diff --git a/storix-fe/src/app/home/topicroom/page.tsx b/storix-fe/src/app/home/topicroom/page.tsx
index 5e6f7b3..1716df3 100644
--- a/storix-fe/src/app/home/topicroom/page.tsx
+++ b/storix-fe/src/app/home/topicroom/page.tsx
@@ -8,6 +8,7 @@ import ParticipationChat, {
ParticipationChatItem,
} from '@/components/topicroom/ParticipationChat'
import { formatTopicRoomSubtitle } from '@/lib/api/topicroom/formatTopicRoomSubtitle'
+import { formatTimeAgo } from '@/lib/utils/formatTimeAgo'
import { CardTopicroomInsideCoverSlider } from '@/components/topicroom/CardTopicroomInsideCoverSlider'
import { TopicRoomData } from '@/components/home/todayTopicRoom/TopicroomCoverCard'
import { useMyTopicRoomsAll } from '@/hooks/topicroom/useMyTopicRoomsAll' //
@@ -16,20 +17,6 @@ import { useProfileStore } from '@/store/profile.store'
import { getMyProfile } from '@/lib/api/profile/profile.api'
export default function TopicRoom() {
- const formatTimeAgo = (iso?: string | null) => {
- if (!iso) return ''
- const t = new Date(iso).getTime()
- if (Number.isNaN(t)) return ''
- const diff = Date.now() - t
- if (diff < 60_000) return '방금 전'
- const min = Math.floor(diff / 60_000)
- if (min < 60) return `${min}분 전`
- const hour = Math.floor(min / 60)
- if (hour < 24) return `${hour}시간 전`
- const day = Math.floor(hour / 24)
- return `${day}일 전`
- }
-
const router = useRouter() //
const goSearch = (raw: string) => {
diff --git a/storix-fe/src/app/home/topicroom/search/ResultClient.tsx b/storix-fe/src/app/home/topicroom/search/ResultClient.tsx
index bde4ce5..10be59e 100644
--- a/storix-fe/src/app/home/topicroom/search/ResultClient.tsx
+++ b/storix-fe/src/app/home/topicroom/search/ResultClient.tsx
@@ -10,6 +10,7 @@ import TopicRoomSearchList from '@/components/topicroom/TopicRoomSearchList'
import { useTopicRoomSearchInfinite } from '@/hooks/topicroom/useTopicRoomSearchInfinite'
import { useJoinTopicRoom } from '@/hooks/topicroom/useJoinTopicRoom'
import { formatTopicRoomSubtitle } from '@/lib/api/topicroom/formatTopicRoomSubtitle'
+import { formatTimeAgo } from '@/lib/utils/formatTimeAgo'
export default function ResultClient() {
const router = useRouter()
@@ -25,26 +26,13 @@ export default function ResultClient() {
isFetchingNextPage,
} = useTopicRoomSearchInfinite(keyword, 20)
- const formatTimeAgo = (iso?: string | null) => {
- if (!iso) return ''
- const t = new Date(iso).getTime()
- if (Number.isNaN(t)) return ''
- const diff = Date.now() - t
- if (diff < 60_000) return '방금 전'
- const min = Math.floor(diff / 60_000)
- if (min < 60) return `${min}분 전`
- const hour = Math.floor(min / 60)
- if (hour < 24) return `${hour}시간 전`
- const day = Math.floor(hour / 24)
- return `${day}일 전`
- }
-
// isJoined 체크 + join 후 이동
const joinMut = useJoinTopicRoom() //
const pendingJoinRef = useRef<{
roomId: number
worksName: string
} | null>(null) //
+ const sentinelRef = useRef
(null)
const items = useMemo(() => {
const pages = data?.pages ?? [] //
@@ -117,10 +105,6 @@ export default function ResultClient() {
pushToRoom(roomId, worksName)
return
}
- if (!found.isJoined) {
- pushToRoom(roomId, worksName)
- return
- }
// join 진행 중이면 중복 클릭 방지
if (joinMut.isPending) return
@@ -130,9 +114,6 @@ export default function ResultClient() {
joinMut.mutate(roomId) //
}
- // 무한스크롤 트리거
- const sentinelRef = useRef(null) //
-
return (
router.push(href)}
className="h-6 w-6 cursor-pointer"
>
-
+
diff --git a/storix-fe/src/components/home/search/SearchResultWorks.tsx b/storix-fe/src/components/home/search/SearchResultWorks.tsx
index deb4879..e94c9e6 100644
--- a/storix-fe/src/components/home/search/SearchResultWorks.tsx
+++ b/storix-fe/src/components/home/search/SearchResultWorks.tsx
@@ -44,6 +44,7 @@ export default function SearchResultWorks({
src={w.thumbnailUrl}
alt={w.worksName}
fill
+ sizes="87px"
className="object-cover"
/>
) : null}
@@ -67,7 +68,6 @@ export default function SearchResultWorks({
width={9}
height={10}
className="inline-block mr-1 mb-0.5"
- priority
/>
{Number(w.avgRating).toFixed(1)}
diff --git a/storix-fe/src/components/home/todayTopicRoom/CardNav.tsx b/storix-fe/src/components/home/todayTopicRoom/CardNav.tsx
index 4e7837a..e5e7610 100644
--- a/storix-fe/src/components/home/todayTopicRoom/CardNav.tsx
+++ b/storix-fe/src/components/home/todayTopicRoom/CardNav.tsx
@@ -7,9 +7,12 @@ import Image from 'next/image'
interface CardNavProps {
header?: string
roomName?: string
+ onNavigate?: () => void | Promise
}
-export const CardNav = ({ header, roomName }: CardNavProps) => {
+export const CardNav = ({ header, roomName, onNavigate }: CardNavProps) => {
+ const href = roomName ?? '#'
+
return (
@@ -19,9 +22,14 @@ export const CardNav = ({ header, roomName }: CardNavProps) => {
{/* 오른쪽 아이콘 그룹 */}
{
+ if (!onNavigate) return
+ e.preventDefault()
+ onNavigate()
+ }}
>
-
-
-
-
-
-
diff --git a/storix-fe/src/components/library/works/WorkTapContent.tsx b/storix-fe/src/components/library/works/WorkTapContent.tsx
index 4e34a78..b0fbf59 100644
--- a/storix-fe/src/components/library/works/WorkTapContent.tsx
+++ b/storix-fe/src/components/library/works/WorkTapContent.tsx
@@ -39,7 +39,9 @@ type Props = {
tab: TabKey
onChangeTab: (tab: TabKey) => void
ui: UIData
+ isCheckingRoom: boolean
onReviewWrite: () => void
+ onTopicroomEnter: () => void
}
export default function WorkTabContent({
@@ -47,7 +49,9 @@ export default function WorkTabContent({
tab,
onChangeTab,
ui,
+ isCheckingRoom,
onReviewWrite,
+ onTopicroomEnter,
}: Props) {
const router = useRouter()
const { data: myReview } = useWorksMyReview(worksId)
@@ -116,7 +120,7 @@ export default function WorkTabContent({
+
{tab === 'info' ? (
@@ -208,6 +212,41 @@ export default function WorkTabContent({
)}
+
+
+
+
+
+
+
>
)
}
diff --git a/storix-fe/src/components/preference/PreferenceList.tsx b/storix-fe/src/components/preference/PreferenceList.tsx
index ce96f19..988ac59 100644
--- a/storix-fe/src/components/preference/PreferenceList.tsx
+++ b/storix-fe/src/components/preference/PreferenceList.tsx
@@ -4,9 +4,14 @@
import Image from 'next/image'
import type { PreferenceWork } from './PreferenceProvider'
import { useFavoriteWork } from '@/hooks/favorite/useFavoriteWork'
+import { usePreference } from './PreferenceProvider'
function PreferenceListRow({ w }: { w: PreferenceWork }) {
- const { isFavorite, isMutating, toggleFavorite } = useFavoriteWork(w.id)
+ const { onFavoriteAdded, onFavoriteRemoved } = usePreference()
+ const { isFavorite, isMutating, toggleFavorite } = useFavoriteWork(w.id, {
+ onAdded: onFavoriteAdded, // addMutation 성공만 카운트
+ onRemoved: onFavoriteRemoved, // 취소하면 카운트에서 제외
+ })
return (
diff --git a/storix-fe/src/components/preference/PreferenceProvider.tsx b/storix-fe/src/components/preference/PreferenceProvider.tsx
index e323b73..a2f2530 100644
--- a/storix-fe/src/components/preference/PreferenceProvider.tsx
+++ b/storix-fe/src/components/preference/PreferenceProvider.tsx
@@ -9,6 +9,7 @@ import React, {
useRef,
useState,
} from 'react'
+import { useQueryClient } from '@tanstack/react-query'
import {
usePreferenceAnalyze,
usePreferenceExploration,
@@ -19,7 +20,6 @@ import type {
PreferenceResultWork,
} from '@/lib/api/preference'
import { z } from 'zod'
-import axios from 'axios'
export type PreferenceWork = {
id: number
@@ -45,6 +45,9 @@ type PreferenceContextValue = {
reset: () => void
likedWorks: PreferenceWork[]
dislikedWorks: PreferenceWork[]
+ likedSuccessCount: number
+ onFavoriteAdded: (worksId: number) => void
+ onFavoriteRemoved: (worksId: number) => void
isDone: boolean
}
@@ -93,6 +96,7 @@ export default function PreferenceProvider({
}: {
children: React.ReactNode
}) {
+ const queryClient = useQueryClient()
const explorationQuery = usePreferenceExploration()
const resultsQuery = usePreferenceResults(true)
const analyzeMutation = usePreferenceAnalyze()
@@ -150,8 +154,8 @@ export default function PreferenceProvider({
const didInitRef = useRef(false)
+ // 최초 1회 저장값 읽기(리마운트/리로드 대비)
useEffect(() => {
- // 최초 1회 저장값 읽기(리마운트/리로드 대비)
if (savedStateRef.current) return
try {
const raw = window.localStorage.getItem(STORAGE_KEY)
@@ -162,8 +166,8 @@ export default function PreferenceProvider({
}
}, [])
+ // works가 처음 채워질 때만 초기화 + 저장된 진행상태 merge
useEffect(() => {
- // works가 처음 채워질 때만 초기화 + 저장된 진행상태 merge
if (didInitRef.current) return
if (!works || works.length === 0) return
@@ -181,8 +185,8 @@ export default function PreferenceProvider({
didInitRef.current = true
}, [works])
+ //: 진행상태 persist (리마운트돼도 복구 가능)
useEffect(() => {
- //: 진행상태 persist (리마운트돼도 복구 가능)
if (!didInitRef.current) return
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
@@ -199,18 +203,6 @@ export default function PreferenceProvider({
const isDone = currentWork == null
// 결과 API 있으면 결과 기준으로 리스트 구성(제한 걸린 날에도 리스트 보여줄 수 있음)
- const likedWorksFromResult = useMemo(() => {
- const r = resultsQuery.data?.result
- if (!r) return null
- return r.likedWorks.map(mapResultToWork)
- }, [resultsQuery.data])
-
- const dislikedWorksFromResult = useMemo(() => {
- const r = resultsQuery.data?.result
- if (!r) return null
- return r.dislikedWorks.map(mapResultToWork)
- }, [resultsQuery.data])
-
const likedWorksLocal = useMemo(
() => works.filter((w) => state[w.id] === 'like'),
[works, state],
@@ -220,8 +212,90 @@ export default function PreferenceProvider({
[works, state],
)
- const likedWorks = likedWorksFromResult ?? likedWorksLocal
- const dislikedWorks = dislikedWorksFromResult ?? dislikedWorksLocal
+ // results가 "존재"만 한다고 바로 쓰면(빈 배열이어도) 로컬 결과를 덮어써서 리스트가 비어 보일 수 있음
+ const result = resultsQuery.data?.result
+
+ // results가 실제로 의미 있는 결과를 가진 경우에만 results를 사용
+ const hasResultLists =
+ !!result && result.likedWorks.length + result.dislikedWorks.length > 0
+
+ // "하루 1회 제한"으로 exploration이 빈 리스트인 날엔 results를 우선 사용(과거 결과 보여주기)
+ const explorationList = explorationQuery.data?.result ?? []
+ const isLimitedDay =
+ explorationQuery.isSuccess &&
+ Array.isArray(explorationList) &&
+ explorationList.length === 0
+
+ const useServerResults = hasResultLists || isLimitedDay
+
+ // DEBUG: "2개 눌렀는데 12로 뜨는" 원인 추적 로그
+ useEffect(() => {
+ // 필요하면 false로 꺼도 됨
+ const DEBUG = true
+ if (!DEBUG) return
+
+ const resultLikedLen = result?.likedWorks?.length ?? 0
+ const resultDislikedLen = result?.dislikedWorks?.length ?? 0
+
+ console.group('[Preference DEBUG] source check')
+ console.log('explorationQuery.isSuccess:', explorationQuery.isSuccess)
+ console.log('exploration works length:', works.length)
+ console.log('likedWorksLocal length:', likedWorksLocal.length)
+ console.log('dislikedWorksLocal length:', dislikedWorksLocal.length)
+
+ console.log('resultsQuery.status:', resultsQuery.status)
+ console.log('result exists:', !!result)
+ console.log('result.likedWorks length:', resultLikedLen)
+ console.log('result.dislikedWorks length:', resultDislikedLen)
+
+ console.log('hasResultLists:', hasResultLists)
+ console.log('isLimitedDay:', isLimitedDay)
+ console.log('useServerResults:', useServerResults)
+ console.groupEnd()
+ }, [
+ explorationQuery.isSuccess,
+ works.length,
+ likedWorksLocal.length,
+ dislikedWorksLocal.length,
+ resultsQuery.status,
+ result,
+ hasResultLists,
+ isLimitedDay,
+ useServerResults,
+ ])
+
+ const [likedSuccessIds, setLikedSuccessIds] = useState>(
+ () => new Set(),
+ )
+
+ const onFavoriteAdded = (worksId: number) => {
+ setLikedSuccessIds((prev) => {
+ const next = new Set(prev)
+ next.add(worksId)
+ return next
+ })
+ }
+
+ const onFavoriteRemoved = (worksId: number) => {
+ setLikedSuccessIds((prev) => {
+ if (!prev.has(worksId)) return prev
+ const next = new Set(prev)
+ next.delete(worksId)
+ return next
+ })
+ }
+
+ const likedWorks = useMemo(() => {
+ if (useServerResults && result)
+ return result.likedWorks.map(mapResultToWork)
+ return likedWorksLocal
+ }, [useServerResults, result, likedWorksLocal])
+
+ const dislikedWorks = useMemo(() => {
+ if (useServerResults && result)
+ return result.dislikedWorks.map(mapResultToWork)
+ return dislikedWorksLocal
+ }, [useServerResults, result, dislikedWorksLocal])
const submitChoice = async (choice: Choice) => {
if (!currentWork) return
@@ -237,12 +311,25 @@ export default function PreferenceProvider({
worksId,
isLiked: choice === 'like',
})
+
+ // "관심작품 등록(POST) 성공"한 like만 카운트
+ if (choice === 'like') {
+ setLikedSuccessIds((prev) => {
+ const next = new Set(prev)
+ next.add(worksId)
+ return next
+ })
+ }
} catch (e) {
- // 서버 동기화 실패 시에도 진행은 유지하고 토스트만 표시
- const serverMsg =
- (e as any)?.response?.data?.message ??
- (e as any)?.message ??
- '요청 처리 중 오류가 발생했어요.'
+ // 실패 시: 진행은 유지하되(정책 유지), 성공 카운트에는 포함되면 안 됨
+ if (choice === 'like') {
+ setLikedSuccessIds((prev) => {
+ if (!prev.has(worksId)) return prev
+ const next = new Set(prev)
+ next.delete(worksId)
+ return next
+ })
+ }
}
}
@@ -251,6 +338,7 @@ export default function PreferenceProvider({
const reset = () => {
setState(buildInitialState(works))
+ setLikedSuccessIds(new Set())
}
const value: PreferenceContextValue = {
@@ -263,6 +351,9 @@ export default function PreferenceProvider({
reset,
likedWorks,
dislikedWorks,
+ likedSuccessCount: likedSuccessIds.size,
+ onFavoriteAdded,
+ onFavoriteRemoved,
isDone,
}
diff --git a/storix-fe/src/hooks/favorite/useFavoriteWork.ts b/storix-fe/src/hooks/favorite/useFavoriteWork.ts
index 116891b..e460c8d 100644
--- a/storix-fe/src/hooks/favorite/useFavoriteWork.ts
+++ b/storix-fe/src/hooks/favorite/useFavoriteWork.ts
@@ -9,7 +9,15 @@ import {
unfavoriteWork,
} from '@/lib/api/favorite'
-export function useFavoriteWork(worksId?: number) {
+type UseFavoriteWorkOptions = {
+ onAdded?: (worksId: number) => void
+ onRemoved?: (worksId: number) => void
+}
+
+export function useFavoriteWork(
+ worksId?: number,
+ options?: UseFavoriteWorkOptions,
+) {
const queryClient = useQueryClient()
const enabled =
@@ -22,26 +30,38 @@ export function useFavoriteWork(worksId?: number) {
const statusQuery = useQuery({
queryKey,
- queryFn: () => getFavoriteWorkStatus(worksId!), // enabled로 가드
+ queryFn: () => getFavoriteWorkStatus(worksId!), // enabled로 가드
enabled: !!enabled,
})
const addMutation = useMutation({
- mutationFn: () => favoriteWork(worksId!), // enabled로 가드
+ mutationFn: () => favoriteWork(worksId!), // enabled로 가드
})
const removeMutation = useMutation({
- mutationFn: () => unfavoriteWork(worksId!), // enabled로 가드
+ mutationFn: () => unfavoriteWork(worksId!), // enabled로 가드
})
- // mutation 성공 시 invalidateQueries (onSuccess 금지 규칙 준수)
+ // mutation 성공 감지 (onSuccess 금지 규칙 준수: useEffect로 처리)
useEffect(() => {
+ if (!enabled) return
+
+ if (addMutation.isSuccess) {
+ options?.onAdded?.(worksId!)
+ }
+ if (removeMutation.isSuccess) {
+ options?.onRemoved?.(worksId!)
+ }
+
if (addMutation.isSuccess || removeMutation.isSuccess) {
- queryClient.invalidateQueries({ queryKey }) //
- addMutation.reset() //
- removeMutation.reset() //
+ queryClient.invalidateQueries({ queryKey })
+ addMutation.reset()
+ removeMutation.reset()
}
}, [
+ enabled,
+ worksId,
+ options,
addMutation.isSuccess,
removeMutation.isSuccess,
queryClient,
diff --git a/storix-fe/src/lib/utils/formatTimeAgo.ts b/storix-fe/src/lib/utils/formatTimeAgo.ts
new file mode 100644
index 0000000..61b7a53
--- /dev/null
+++ b/storix-fe/src/lib/utils/formatTimeAgo.ts
@@ -0,0 +1,13 @@
+export function formatTimeAgo(iso?: string | null): string {
+ if (!iso) return ''
+ const t = new Date(iso).getTime()
+ if (Number.isNaN(t)) return ''
+ const diff = Date.now() - t
+ if (diff < 60_000) return '방금 전'
+ const min = Math.floor(diff / 60_000)
+ if (min < 60) return `${min}분 전`
+ const hour = Math.floor(min / 60)
+ if (hour < 24) return `${hour}시간 전`
+ const day = Math.floor(hour / 24)
+ return `${day}일 전`
+}