Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6,618 changes: 6,618 additions & 0 deletions frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"react": "^19.1.2",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.1.2",
"react-ga4": "^2.1.0",
"react-hook-form": "^7.60.0",
"react-markdown": "^10.1.0",
"react-responsive": "^10.0.1",
Expand Down
31 changes: 31 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function App() {
<>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />

<RouterProvider router={router} />
<GlobalModal />
{/* 전역 폴러 */}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ export const fetchMyProfile = async () => {
const { data } = await axiosInstance.get('/members')
return data
}

export const withdrawMember = async () => {
const { data } = await axiosInstance.delete('/members/withdraw')
return data
}
3 changes: 3 additions & 0 deletions frontend/src/assets/icons/feedback.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions frontend/src/components/GoogleAnalytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { initGA, trackPageView } from '../utils/analytics'

/**
* Google Analytics 초기화 및 페이지뷰 트래킹을 담당하는 컴포넌트
*/
export const GoogleAnalytics = () => {
const location = useLocation()

// GA 초기화 (최초 1회만 실행)
useEffect(() => {
initGA()
}, [])

// 페이지 이동 시 페이지뷰 트래킹
useEffect(() => {
trackPageView(location.pathname + location.search)
}, [location])

return null
}
1 change: 1 addition & 0 deletions frontend/src/hooks/idea/usePatchIdeaBookmark.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { patchReportIdeaBookmark } from '../../api/idea'
import type { PatchIdeaBookmarkDto, ResponseGetGeneratedIdea, ResponsePatchIdeaBookmark } from '../../types/idea'

interface OptimisticUpdateContext {
previousIdeasResponse?: ResponseGetGeneratedIdea
}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/hooks/idea/usePostIdea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ export default function usePostIdea() {
},
onError: (error) => {
const state = error.response?.status
const errorMessage = error.response?.data?.message || 'Unknown error'

if (state === 400) {
openModal('GENERATING_LIMIT')
return
}
const errorMessage = error.response?.data?.message
console.error('아이디어 생성 실패:', errorMessage)
},
})
Expand Down
37 changes: 36 additions & 1 deletion frontend/src/hooks/main/useUrlInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ import { useQueryClient } from '@tanstack/react-query'

const PENDING_KEY = 'pending-url'

export const useUrlInput = (onRequestUrlSuccess?: (reportId: number, videoId: number) => void) => {
interface UseUrlInputCallbacks {
onRequestUrlSuccess?: (reportId: number, videoId: number) => void
onTrackEvent?: (event: {
category: string
action: string
label: string
}) => void
}

export const useUrlInput = ({ onRequestUrlSuccess, onTrackEvent }: UseUrlInputCallbacks = {}) => {
const [isActive, setIsActive] = useState(false)
const [error, setError] = useState<string | null>(null)

Expand All @@ -36,10 +45,22 @@ export const useUrlInput = (onRequestUrlSuccess?: (reportId: number, videoId: nu
})
}

onTrackEvent?.({
category: 'Report',
action: 'Generate Report Success',
label: 'Main Page URL Input',
})

onRequestUrlSuccess?.(reportId, videoId)
setError(null)
},
onError: ({ code, message }) => {
onTrackEvent?.({
category: 'Report',
action: 'Generate Report Error',
label: code,
})

if (code === 'YOUTUBE400') {
setError('유효한 유튜브 URL을 입력해주세요.')
} else if (code === 'VIDEO403') {
Expand All @@ -59,6 +80,7 @@ export const useUrlInput = (onRequestUrlSuccess?: (reportId: number, videoId: nu
const url = watch('url')

// URL이 변경될 때만 에러 상태를 초기화

useEffect(() => {
if (error) {
setError(null)
Expand Down Expand Up @@ -88,10 +110,23 @@ export const useUrlInput = (onRequestUrlSuccess?: (reportId: number, videoId: nu
} catch {
alert('URL 임시 저장 실패')
}

onTrackEvent?.({
category: 'User',
action: 'Login Required',
label: 'URL Input - Not Authenticated',
})

openLoginFlow() // 비로그인 상태에서 요청할 경우 로그인 플로우를 시작
return
}

onTrackEvent?.({
category: 'Report',
action: 'Generate Report Request',
label: 'Main Page URL Input',
})

setError(null)
requestNewReport({ url }) // 리포트 생성 요청
}
Expand Down
41 changes: 35 additions & 6 deletions frontend/src/hooks/report/useDeleteMyReport.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,49 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { DeleteMyReport, ResponseDeleteMyReport } from '../../types/report/all'
import type { BriefReport, DeleteMyReport, ResponseDeleteMyReport } from '../../types/report/all'
import { deleteMyReport } from '../../api/report'

type MyReportQueryData = {
reportList: BriefReport[]
totalElements: number
}

type DeleteReportContext = {
previousData?: MyReportQueryData
}

export const useDeleteMyReport = ({ channelId }: { channelId: number | undefined }) => {
const queryClient = useQueryClient()

return useMutation<ResponseDeleteMyReport, Error, DeleteMyReport>({
return useMutation<ResponseDeleteMyReport, Error, DeleteMyReport, DeleteReportContext>({
mutationFn: deleteMyReport,
onSuccess: () => {

onMutate: async ({ reportId }) => {
if (typeof channelId !== 'number') return {}
const queryKey = ['my', 'report', channelId]
await queryClient.cancelQueries({ queryKey })
const previousData = queryClient.getQueryData<MyReportQueryData>(queryKey)
queryClient.setQueryData<MyReportQueryData>(queryKey, (old) => {
if (!old) return old
return {
...old,
reportList: old.reportList.filter((report) => report.reportId !== reportId),
totalElements: old.totalElements - 1,
}
})
return { previousData }
},
onError: (_error, _variables, context) => {
if (typeof channelId === 'number' && context?.previousData) {
queryClient.setQueryData(['my', 'report', channelId], context.previousData)
}
alert('리포트 삭제 중 오류가 발생했습니다.')
},
Comment on lines +20 to +40
Copy link
Contributor

Choose a reason for hiding this comment

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

high

리포트 목록에 대한 낙관적 업데이트를 구현하신 점이 좋습니다. 하지만 현재 onMutateonError에서 사용된 setQueryDatagetQueryData는 정확한 쿼리 키를 필요로 합니다. 리포트 목록 쿼리는 페이징 및 필터링(type)을 포함하므로, ['my', 'report', channelId]와 같은 부분적인 키로는 캐시된 데이터를 정확히 찾아 업데이트하거나 롤백하기 어렵습니다. 이로 인해 낙관적 업데이트가 UI에 반영되지 않을 가능성이 높습니다.

모든 관련 페이지 캐시에 대해 업데이트를 적용하려면 setQueriesData를 필터와 함께 사용하는 것을 고려해 보세요. 이 로직이 의도한 대로 동작하는지 다시 한번 확인해 보시는 것을 추천합니다.


onSettled: () => {
if (typeof channelId === 'number') {
queryClient.invalidateQueries({ queryKey: ['my', 'report', channelId] })
queryClient.invalidateQueries({ queryKey: ['recommendedVideos'] })
}
},
onError: () => {
alert('리포트 삭제 중 오류가 발생했습니다.')
},
})
}
Loading