-
+
@@ -13,7 +15,7 @@ export default function PreferenceFinishPage() {
축하해요!
- {likedWorks.length}개의 새로운 관심 작품이 등록됐어요!
+ {likedSuccessCount}개의 새로운 관심 작품이 등록됐어요!
피드에서 작품의 소식을 확인해봐요!
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/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,
From adfd0f17b104d1863b70d7ed82f60e8da806d438 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EA=B9=80=EC=9C=A4=EC=84=B1?=
Date: Thu, 5 Feb 2026 22:46:21 +0900
Subject: [PATCH 3/5] =?UTF-8?q?design:=20=EC=9E=91=ED=92=88=20=EC=83=81?=
=?UTF-8?q?=EC=84=B8=ED=99=88=20=EB=B2=84=ED=8A=BC=20=EB=94=94=EC=9E=90?=
=?UTF-8?q?=EC=9D=B8=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
storix-fe/public/icons/icon-share.svg | 3 ++
storix-fe/src/app/library/works/[id]/page.tsx | 4 +-
.../library/works/WorkHeaderCover.tsx | 35 ----------------
.../library/works/WorkTapContent.tsx | 41 ++++++++++++++++++-
4 files changed, 46 insertions(+), 37 deletions(-)
create mode 100644 storix-fe/public/icons/icon-share.svg
diff --git a/storix-fe/public/icons/icon-share.svg b/storix-fe/public/icons/icon-share.svg
new file mode 100644
index 0000000..63aaa89
--- /dev/null
+++ b/storix-fe/public/icons/icon-share.svg
@@ -0,0 +1,3 @@
+
diff --git a/storix-fe/src/app/library/works/[id]/page.tsx b/storix-fe/src/app/library/works/[id]/page.tsx
index e6572c7..06afcf7 100644
--- a/storix-fe/src/app/library/works/[id]/page.tsx
+++ b/storix-fe/src/app/library/works/[id]/page.tsx
@@ -219,7 +219,9 @@ export default function LibraryWorkHomePage() {
keywords: ui.keywords,
platform: ui.platform,
}}
- onReviewWrite={handleReviewWrite} // (라우팅 변경/토스트)
+ isCheckingRoom={isCheckingRoom}
+ onReviewWrite={handleReviewWrite} //(라우팅 변경/토스트)
+ onTopicroomEnter={handleTopicroomEnter}
/>
-
-
-
-
-
-
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 Content */}
-