diff --git a/apps/web/e2e/mocks/tournament.ts b/apps/web/e2e/mocks/tournament.ts index dc30591f..13489fd1 100644 --- a/apps/web/e2e/mocks/tournament.ts +++ b/apps/web/e2e/mocks/tournament.ts @@ -84,7 +84,7 @@ export const MOCK_TOURNAMENT_PENDING: GetTournamentPendingResponseT = { inviteCode: 'E2ECODE', /** 미래 시각으로 고정해 초대 만료 카운트다운이 항상 유효하도록 */ inviteExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - /** status 가 PENDING/PROCESSING 인 아이템을 두지 않아 SSE 폴백 폴링이 돌지 않게 함 */ + /** status 가 PARSING 인 아이템을 두지 않아 SSE 폴백 폴링이 돌지 않게 함 */ items: [], participants: [ { diff --git a/apps/web/e2e/mocks/wish.ts b/apps/web/e2e/mocks/wish.ts index 991d2969..dac020ef 100644 --- a/apps/web/e2e/mocks/wish.ts +++ b/apps/web/e2e/mocks/wish.ts @@ -20,9 +20,10 @@ export const MOCK_WISHLIST_ENTRIES: GetWishlistResponseT[] = MOCK_TOURNAMENT_ITE imageUrl: MOCK_IMAGE_URLS.product, sourceUrl: null, sourcePlatform: null, + method: 'STRUCTURED', + userInput: false, + refreshing: false, }, - reused: null, - refreshNeeded: null, winnerTournamentIds: [], }) ); @@ -41,7 +42,9 @@ export const MOCK_WISH_DETAIL: GetWishResponseT = { imageUrl: MOCK_IMAGE_URLS.product, sourceUrl: MOCK_WISH_SOURCE_URL, sourcePlatform: 'E2E몰', - source: 'SERVER', + method: 'STRUCTURED', + userInput: false, + refreshing: false, }, priceHistory: [], }; @@ -57,7 +60,8 @@ export const MOCK_WISH_ADDED_BY_LINK: PostWishLinkResponseT = { imageUrl: MOCK_IMAGE_URLS.product, sourceUrl: 'https://shop.example/items/105', sourcePlatform: 'E2E몰', + method: 'STRUCTURED', + userInput: false, + refreshing: false, }, - refreshNeeded: false, - reused: false, }; diff --git a/apps/web/e2e/specs/archive/wishAddByImage.spec.ts b/apps/web/e2e/specs/archive/wishAddByImage.spec.ts index 2ed6aabd..818e0bee 100644 --- a/apps/web/e2e/specs/archive/wishAddByImage.spec.ts +++ b/apps/web/e2e/specs/archive/wishAddByImage.spec.ts @@ -92,9 +92,7 @@ test('여러 장을 고르면 에디터가 열리고, 편집한 장만 JPEG 로 { imageKey: 'key-2', uploadUrl: `${MOCK_UPLOAD_URL_PREFIX}2`, contentType: 'image/svg+xml' }, ], }); - api.post(ENDPOINTS.WISH_IMAGE_CONFIRM, [ - { ...MOCK_WISH_ADDED_BY_LINK, refreshNeeded: null, reused: null }, - ]); + api.post(ENDPOINTS.WISH_IMAGE_CONFIRM, [MOCK_WISH_ADDED_BY_LINK]); const putBodies: Record = {}; await page.route(`${MOCK_UPLOAD_URL_PREFIX}*`, async route => { @@ -167,9 +165,7 @@ test('크롭 박스 모서리를 끌어 줄이면 그 영역만 JPEG 로 업로 { imageKey: 'key-1', uploadUrl: `${MOCK_UPLOAD_URL_PREFIX}1`, contentType: 'image/jpeg' }, ], }); - api.post(ENDPOINTS.WISH_IMAGE_CONFIRM, [ - { ...MOCK_WISH_ADDED_BY_LINK, refreshNeeded: null, reused: null }, - ]); + api.post(ENDPOINTS.WISH_IMAGE_CONFIRM, [MOCK_WISH_ADDED_BY_LINK]); let putBody: Buffer | null | undefined; await page.route(`${MOCK_UPLOAD_URL_PREFIX}*`, async route => { @@ -236,9 +232,7 @@ test('브라우저가 디코드하지 못하는 이미지는 편집 불가 안 { imageKey: 'key-1', uploadUrl: `${MOCK_UPLOAD_URL_PREFIX}1`, contentType: 'image/heic' }, ], }); - api.post(ENDPOINTS.WISH_IMAGE_CONFIRM, [ - { ...MOCK_WISH_ADDED_BY_LINK, refreshNeeded: null, reused: null }, - ]); + api.post(ENDPOINTS.WISH_IMAGE_CONFIRM, [MOCK_WISH_ADDED_BY_LINK]); await page.route(`${MOCK_UPLOAD_URL_PREFIX}*`, route => route.fulfill({ status: 200, body: '' })); const presignRequestPromise = page.waitForRequest( diff --git a/apps/web/e2e/specs/archive/wishDetail.spec.ts b/apps/web/e2e/specs/archive/wishDetail.spec.ts index 0f4a9c1a..38e472af 100644 --- a/apps/web/e2e/specs/archive/wishDetail.spec.ts +++ b/apps/web/e2e/specs/archive/wishDetail.spec.ts @@ -47,8 +47,6 @@ test('상세 정보를 확인하고 상품명을 수정해 저장하면 조회 api.patch(ENDPOINTS.WISHLIST(WISH_ID), { wish: MOCK_WISH_DETAIL.wish, item: updatedItem, - refreshNeeded: null, - reused: null, }); api.get(ENDPOINTS.WISHLIST(WISH_ID), { ...MOCK_WISH_DETAIL, item: updatedItem }); diff --git a/apps/web/src/app/archive/wish/[id]/_components/WishInfoScreen.tsx b/apps/web/src/app/archive/wish/[id]/_components/WishInfoScreen.tsx index 68145157..6edad009 100644 --- a/apps/web/src/app/archive/wish/[id]/_components/WishInfoScreen.tsx +++ b/apps/web/src/app/archive/wish/[id]/_components/WishInfoScreen.tsx @@ -18,7 +18,6 @@ function WishInfoScreen({ wishId }: WishInfoScreenProps) { const { item } = wishData; const { - wishItem, refreshWish, isPostWishRefreshPending, isPostWishRefreshFailed, @@ -32,11 +31,11 @@ function WishInfoScreen({ wishId }: WishInfoScreenProps) { patchWishMutation(data, { onSuccess })} isSavePending={isPatchWishPending} diff --git a/apps/web/src/app/archive/wish/[id]/_hooks/usePostWishRefresh.ts b/apps/web/src/app/archive/wish/[id]/_hooks/usePostWishRefresh.ts index 237dded3..1baf624c 100644 --- a/apps/web/src/app/archive/wish/[id]/_hooks/usePostWishRefresh.ts +++ b/apps/web/src/app/archive/wish/[id]/_hooks/usePostWishRefresh.ts @@ -2,28 +2,24 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { ITEM_STATUS } from '@/consts/item'; +import { useSSEFallback } from '@/hooks/useSSEFallback'; import { isGlobalNetError } from '@/utils/apiError'; import { postWishRefresh } from '../_apis/postWishRefresh'; import type { GetWishResponseT } from '../_types/wish'; -type WishItemT = GetWishResponseT['item']; - -/** 갱신은 서버에서 비동기로 진행되고 완료는 SSE 가 알려주므로, 로딩·성패를 응답에 상품 정보가 실렸는지로 판단한다 */ -export const usePostWishRefresh = (wishId: number, item: WishItemT) => { +export const usePostWishRefresh = (wishId: number, item: GetWishResponseT['item']) => { const queryClient = useQueryClient(); const [isPostWishRefreshFailed, setIsPostWishRefreshFailed] = useState(false); const [isRefreshRequested, setIsRefreshRequested] = useState(false); - const [itemBeforeRefresh, setItemBeforeRefresh] = useState(null); const isItemFailed = item.status === ITEM_STATUS.FAILED; - /** 갱신 중에는 상품 정보가 빈 응답으로 온다 */ - const isItemLoading = !item.imageUrl && !isItemFailed; + + useSSEFallback(['wish', wishId], item.refreshing); const { mutate: postWishRefreshMutation, isPending: isPostWishRefreshPending } = useMutation({ mutationFn: () => postWishRefresh(wishId), onSuccess: async () => { - /** PROCESSING 을 받을 때까지 pending 을 유지해 로딩이 끊기지 않게 한다 */ await queryClient.invalidateQueries({ queryKey: ['wish', wishId] }); queryClient.invalidateQueries({ queryKey: ['wishlists'] }); }, @@ -36,15 +32,12 @@ export const usePostWishRefresh = (wishId: number, item: WishItemT) => { const refreshWish = () => { setIsRefreshRequested(true); - setItemBeforeRefresh(item); postWishRefreshMutation(); }; return { - /** 갱신 직전 정보로 화면을 유지한다 */ - wishItem: item.imageUrl ? item : (itemBeforeRefresh ?? item), refreshWish, - isPostWishRefreshPending: isPostWishRefreshPending || isItemLoading, + isPostWishRefreshPending: isPostWishRefreshPending || item.refreshing, /** 원래 실패해 있던 위시와 구분해, 갱신으로 실패한 경우만 안내한다 */ isPostWishRefreshFailed: isPostWishRefreshFailed || (isRefreshRequested && isItemFailed), closePostWishRefreshFailed: () => { diff --git a/apps/web/src/app/archive/wish/[id]/_types/wish.ts b/apps/web/src/app/archive/wish/[id]/_types/wish.ts index b551c192..696c76d9 100644 --- a/apps/web/src/app/archive/wish/[id]/_types/wish.ts +++ b/apps/web/src/app/archive/wish/[id]/_types/wish.ts @@ -1,25 +1,25 @@ import type { ITEM_STATUS } from '@/consts/item'; -import type { PatchItemRequestT, PriceHistorySourceT, PriceHistoryT } from '@/types/item'; +import type { ItemExtractMethodT, PatchItemRequestT, PriceHistoryT } from '@/types/item'; import type { WishT } from '@/types/wish'; type ItemT = { - id: number; - source: PriceHistorySourceT | null; /** 이미지로 등록한 항목은 원본 URL 이 없어 null */ sourceUrl: string | null; sourcePlatform: string | null; + method: ItemExtractMethodT | null; + userInput: boolean; + refreshing: boolean; } & ( | { - status: - | (typeof ITEM_STATUS)['PENDING'] - | (typeof ITEM_STATUS)['PROCESSING'] - | (typeof ITEM_STATUS)['FAILED']; + id: null; + status: (typeof ITEM_STATUS)['PARSING'] | (typeof ITEM_STATUS)['FAILED']; name: null; imageUrl: null; price: null; currency: null; } | { + id: number; status: (typeof ITEM_STATUS)['INCOMPLETE']; name: string | null; imageUrl: string | null; @@ -27,6 +27,7 @@ type ItemT = { currency: string | null; } | { + id: number; status: (typeof ITEM_STATUS)['READY']; name: string; imageUrl: string; @@ -55,6 +56,4 @@ export type GetWishResponseT = { export type PatchWishResponseT = { wish: WishT; item: ItemT; - refreshNeeded: null; - reused: null; }; diff --git a/apps/web/src/app/archive/wish/[id]/layout.tsx b/apps/web/src/app/archive/wish/[id]/layout.tsx index c7f8b6d2..ca18e8ac 100644 --- a/apps/web/src/app/archive/wish/[id]/layout.tsx +++ b/apps/web/src/app/archive/wish/[id]/layout.tsx @@ -27,12 +27,8 @@ async function WishEditLayout({ children, params }: WishEditLayoutProps) { const wishData = await getWish(wishId); queryClient.setQueryData(['wish', wishId], wishData); - /** 아직 PROCESSING 상태인 경우에는 접근 불가 */ - if ( - wishData.item.status === ITEM_STATUS.PROCESSING || - wishData.item.status === ITEM_STATUS.PENDING - ) - redirect(ROUTES.WISHLIST); + /** 아직 PARSING 상태인 경우에는 접근 불가 */ + if (wishData.item.status === ITEM_STATUS.PARSING) redirect(ROUTES.WISHLIST); } catch (error) { if (!isAxiosError(error)) throw error; diff --git a/apps/web/src/app/archive/wish/_components/WishContent.tsx b/apps/web/src/app/archive/wish/_components/WishContent.tsx index 09321e7f..90927bf3 100644 --- a/apps/web/src/app/archive/wish/_components/WishContent.tsx +++ b/apps/web/src/app/archive/wish/_components/WishContent.tsx @@ -38,12 +38,7 @@ function WishContent() { const { wishlistData } = useGetWishlist(); const selectableIds = wishlistData - .filter( - ({ item }) => - item.status !== ITEM_STATUS.FAILED && - item.status !== ITEM_STATUS.PENDING && - item.status !== ITEM_STATUS.PROCESSING - ) + .filter(({ item }) => item.status !== ITEM_STATUS.FAILED && item.status !== ITEM_STATUS.PARSING) .map(({ wish }) => wish.id); const isAllSelected = selectableIds.length > 0 && selectableIds.every(id => selectedIds.has(id)); diff --git a/apps/web/src/app/archive/wish/_components/WishlistList.tsx b/apps/web/src/app/archive/wish/_components/WishlistList.tsx index ff750e6e..9d78ffce 100644 --- a/apps/web/src/app/archive/wish/_components/WishlistList.tsx +++ b/apps/web/src/app/archive/wish/_components/WishlistList.tsx @@ -2,10 +2,10 @@ import { useEffect, useRef } from 'react'; +import { ITEM_STATUS } from '@/consts/item'; import { useGetWishlist } from '@/hooks/useGetWishlist'; import { useSSEFallback } from '@/hooks/useSSEFallback'; import { useScrollRestoration } from '@/hooks/useScrollRestoration'; -import { hasParsingItems } from '@/utils/item'; import { SCROLL_NAMESPACE } from '@/utils/scrollRestoration'; import WishCardSkeleton from './WishCardSkeleton'; @@ -21,7 +21,9 @@ function WishlistList({ isDeleteMode, selectedIds, onToggleSelect }: WishlistLis const sentinelRef = useRef(null); const { wishlistData, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetWishlist(); - const hasPendingItem = hasParsingItems(wishlistData.map(({ item }) => item)); + const hasPendingItem = wishlistData.some( + ({ item }) => item.status === ITEM_STATUS.PARSING || item.refreshing + ); useSSEFallback(['wishlists'], hasPendingItem); useScrollRestoration({ namespace: SCROLL_NAMESPACE.ARCHIVE_WISH }); diff --git a/apps/web/src/app/archive/wish/_components/wish-grid/index.tsx b/apps/web/src/app/archive/wish/_components/wish-grid/index.tsx index 31083f23..30432083 100644 --- a/apps/web/src/app/archive/wish/_components/wish-grid/index.tsx +++ b/apps/web/src/app/archive/wish/_components/wish-grid/index.tsx @@ -39,9 +39,8 @@ function WishGrid({ items, isDeleteMode = false, selectedIds, onToggleSelect }: /> ); - /** 파싱 중인 위시는 진입할 곳이 없다 */ - if (item.status === ITEM_STATUS.PENDING || item.status === ITEM_STATUS.PROCESSING) - return
{card}
; + /** 파싱 중인 위시는 위시 상세 페이지 진입 불가능 */ + if (item.status === ITEM_STATUS.PARSING) return
{card}
; /** 삭제 모드는 READY 만 선택 가능 */ if (isDeleteMode && item.status === ITEM_STATUS.READY) { diff --git a/apps/web/src/app/tournament/[id]/_common/_types/tournamentResponse.ts b/apps/web/src/app/tournament/[id]/_common/_types/tournamentResponse.ts index 5622d6ec..cb5282f0 100644 --- a/apps/web/src/app/tournament/[id]/_common/_types/tournamentResponse.ts +++ b/apps/web/src/app/tournament/[id]/_common/_types/tournamentResponse.ts @@ -24,7 +24,7 @@ export type TournamentMatchT = { */ export type PendingTournamentItemT = Partial & { tournamentItemId: number; - itemId: number; + itemId: number | null; userId?: string; }; diff --git a/apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx b/apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx index 2bdf9f89..bd393cb8 100644 --- a/apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx +++ b/apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx @@ -12,7 +12,6 @@ import { useQueryAction } from '@/hooks/useQueryAction'; import { useQueryParamOnce } from '@/hooks/useQueryParamOnce'; import { useSSEFallback } from '@/hooks/useSSEFallback'; import { hasSentInvite } from '@/utils/inviteSentSession'; -import { hasParsingItems } from '@/utils/item'; import { parseIdParam } from '@/utils/parseIdParam'; import { useGetTournament } from '../../_common/_hooks/useGetTournament'; @@ -80,7 +79,7 @@ function TournamentCreateClient({ tournamentId }: TournamentCreateClientProps) { const pending = 'pending' in tournamentData ? tournamentData.pending : null; // 아이템 파싱 중이면 SSE 로 실시간 업데이트 (연결 실패 시 polling fallback). - const hasPendingItem = hasParsingItems(pending?.items ?? []); + const hasPendingItem = (pending?.items ?? []).some(item => item.status === ITEM_STATUS.PARSING); useSSEFallback(['tournament', tournamentId], hasPendingItem); const hasUnfinishedItem = (pending?.items ?? []).some( diff --git a/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/ProductImage.tsx b/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/ProductImage.tsx index a5f9b747..f1f297ad 100644 --- a/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/ProductImage.tsx +++ b/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/ProductImage.tsx @@ -34,7 +34,7 @@ const errorFallback = (status: ItemStatusT<'FAILED' | 'INCOMPLETE'> = 'FAILED') ); function ProductImage({ src, alt, status, className }: ProductImageProps) { - const isProcessing = status === ITEM_STATUS.PENDING || status === ITEM_STATUS.PROCESSING; + const isParsing = status === ITEM_STATUS.PARSING; const isError = status === ITEM_STATUS.FAILED || status === ITEM_STATUS.INCOMPLETE; return ( @@ -44,9 +44,9 @@ function ProductImage({ src, alt, status, className }: ProductImageProps) { className )} > - {isProcessing && loadingFallback} + {isParsing && loadingFallback} {isError && errorFallback(status)} - {!isProcessing && !isError && src && ( + {!isParsing && !isError && src && ( i.itemId) ?? []); const items = wishlistData.filter( ({ item }) => - item.status !== ITEM_STATUS.FAILED && - item.status !== ITEM_STATUS.PROCESSING && - item.status !== ITEM_STATUS.INCOMPLETE && - !existingItemIds.has(item.id) + item.status === ITEM_STATUS.READY && item.id !== null && !existingItemIds.has(item.id) ); const isWishlistLoaded = !hasNextPage && !isFetchingNextPage; @@ -64,7 +61,7 @@ function ByWishContent({ tournamentId }: ByWishContentProps) { const handleNext = () => { const itemIds = items .filter(({ wish }) => selectedIds.includes(wish.id)) - .map(({ item }) => item.id); + .flatMap(({ item }) => item.id ?? []); postTournamentItemsByWishMutation(itemIds); }; diff --git a/apps/web/src/app/tournament/[id]/item/[itemId]/_types/tournamentItem.ts b/apps/web/src/app/tournament/[id]/item/[itemId]/_types/tournamentItem.ts index 1b68bd07..e8c3520d 100644 --- a/apps/web/src/app/tournament/[id]/item/[itemId]/_types/tournamentItem.ts +++ b/apps/web/src/app/tournament/[id]/item/[itemId]/_types/tournamentItem.ts @@ -2,7 +2,7 @@ import type { ITEM_STATUS } from '@/consts/item'; export type GetTournamentItemResponseT = { tournamentItemId: number; - itemId: number; + itemId: number | null; /** 이미지로 등록한 경우 undefined */ sourceUrl?: string; currency?: string; @@ -10,10 +10,7 @@ export type GetTournamentItemResponseT = { memo?: string; } & ( | { - status: - | (typeof ITEM_STATUS)['PENDING'] - | (typeof ITEM_STATUS)['PROCESSING'] - | (typeof ITEM_STATUS)['FAILED']; + status: (typeof ITEM_STATUS)['PARSING'] | (typeof ITEM_STATUS)['FAILED']; name?: undefined; imageUrl?: undefined; price?: undefined; diff --git a/apps/web/src/app/tournament/[id]/item/[itemId]/layout.tsx b/apps/web/src/app/tournament/[id]/item/[itemId]/layout.tsx index 459770b5..929f5176 100644 --- a/apps/web/src/app/tournament/[id]/item/[itemId]/layout.tsx +++ b/apps/web/src/app/tournament/[id]/item/[itemId]/layout.tsx @@ -43,11 +43,8 @@ async function TournamentItemLayout({ children, params }: TournamentItemLayoutPr queryClient.ensureQueryData({ queryKey: ['me'], queryFn: getMe }), ]); - /** 아직 PROCESSING 상태인 경우에는 접근 불가 */ - if ( - tournamentItemData.status === ITEM_STATUS.PROCESSING || - tournamentItemData.status === ITEM_STATUS.PENDING - ) + /** 아직 PARSING 상태인 경우에는 접근 불가 */ + if (tournamentItemData.status === ITEM_STATUS.PARSING) redirect(ROUTES.TOURNAMENT_CREATE(tournamentId)); /** FAILED·INCOMPLETE 상태인 경우 주최자나 등록한 본인만 접근 가능 */ diff --git a/apps/web/src/app/tournament/[id]/match/page.tsx b/apps/web/src/app/tournament/[id]/match/page.tsx index f6021324..c833715c 100644 --- a/apps/web/src/app/tournament/[id]/match/page.tsx +++ b/apps/web/src/app/tournament/[id]/match/page.tsx @@ -8,7 +8,6 @@ import { QUERY_ACTION } from '@/consts/queryAction'; import { ROUTES } from '@/consts/route'; import { MIN_TOURNAMENT_ITEM_COUNT, TOURNAMENT_STATUS } from '@/consts/tournament'; import { getApiErrorCode, getApiErrorStatus } from '@/utils/apiError'; -import { hasParsingItems } from '@/utils/item'; import { getQueryClient } from '@/utils/queryClient'; import { getTournament } from '../_common/_apis/getTournament'; @@ -51,10 +50,9 @@ const isNotStartableError = (error: unknown) => { const notStartablePath = (tournamentId: number) => `${ROUTES.TOURNAMENT_CREATE(tournamentId)}?${QUERY_ACTION.KEY}=${QUERY_ACTION.VALUE.TOURNAMENT_NOT_STARTABLE}`; -/** 대기실 시작 버튼(hasPendingItem · hasUnfinishedItem)과 동일한 기준 */ const isStartable = (items: PendingTournamentItemT[]) => items.length >= MIN_TOURNAMENT_ITEM_COUNT && - !hasParsingItems(items) && + !items.some(item => item.status === ITEM_STATUS.PARSING) && !items.some(item => item.status === ITEM_STATUS.FAILED || item.status === ITEM_STATUS.INCOMPLETE); async function TournamentPage({ params }: TournamentPageProps) { diff --git a/apps/web/src/app/wish/_common/_consts/wishMock.ts b/apps/web/src/app/wish/_common/_consts/wishMock.ts index a403e5bd..2728a896 100644 --- a/apps/web/src/app/wish/_common/_consts/wishMock.ts +++ b/apps/web/src/app/wish/_common/_consts/wishMock.ts @@ -18,10 +18,11 @@ export const MOCK_WISHES: WishListItemT[] = Array.from({ length: 12 }, (_, index imageUrl: MOCK_IMAGE_URLS[index % MOCK_IMAGE_URLS.length] ?? null, sourceUrl: null, sourcePlatform: 'BRAND', + method: 'STRUCTURED', + userInput: false, + refreshing: false, discountRate: index % 4 === 3 ? null : 99, }, - refreshNeeded: null, - reused: null, winnerTournamentIds: index % 4 === 0 ? [index + 100] : [], })); diff --git a/apps/web/src/components/common/wish-card/WishProcessingCard.tsx b/apps/web/src/components/common/wish-card/WishParsingCard.tsx similarity index 72% rename from apps/web/src/components/common/wish-card/WishProcessingCard.tsx rename to apps/web/src/components/common/wish-card/WishParsingCard.tsx index a47c1705..a4e82aa2 100644 --- a/apps/web/src/components/common/wish-card/WishProcessingCard.tsx +++ b/apps/web/src/components/common/wish-card/WishParsingCard.tsx @@ -2,8 +2,7 @@ import Spinner from '@/components/spinner'; import WishStatusFrame from './WishStatusFrame'; -/** PENDING·PROCESSING 위시 카드 */ -function WishProcessingCard() { +function WishParsingCard() { return ( @@ -12,4 +11,4 @@ function WishProcessingCard() { ); } -export default WishProcessingCard; +export default WishParsingCard; diff --git a/apps/web/src/components/common/wish-card/index.tsx b/apps/web/src/components/common/wish-card/index.tsx index e213db78..002c5979 100644 --- a/apps/web/src/components/common/wish-card/index.tsx +++ b/apps/web/src/components/common/wish-card/index.tsx @@ -3,7 +3,7 @@ import type { ItemStatusT } from '@/types/item'; import WishFailedCard from './WishFailedCard'; import WishIncompleteCard from './WishIncompleteCard'; -import WishProcessingCard from './WishProcessingCard'; +import WishParsingCard from './WishParsingCard'; import WishReadyCard from './WishReadyCard'; type WishCardProps = { @@ -19,9 +19,8 @@ type WishCardProps = { /** 위시 카드 — 파싱 상태에 맞는 카드를 고른다 */ function WishCard({ status, ...readyProps }: WishCardProps) { switch (status) { - case ITEM_STATUS.PENDING: - case ITEM_STATUS.PROCESSING: - return ; + case ITEM_STATUS.PARSING: + return ; case ITEM_STATUS.FAILED: return ; case ITEM_STATUS.INCOMPLETE: diff --git a/apps/web/src/consts/item.ts b/apps/web/src/consts/item.ts index 8d8d44ac..df0cef84 100644 --- a/apps/web/src/consts/item.ts +++ b/apps/web/src/consts/item.ts @@ -1,6 +1,5 @@ export const ITEM_STATUS = { - PENDING: 'PENDING', - PROCESSING: 'PROCESSING', + PARSING: 'PARSING', READY: 'READY', INCOMPLETE: 'INCOMPLETE', FAILED: 'FAILED', diff --git a/apps/web/src/types/item.ts b/apps/web/src/types/item.ts index da6d20e8..44708941 100644 --- a/apps/web/src/types/item.ts +++ b/apps/web/src/types/item.ts @@ -3,7 +3,8 @@ import type { ITEM_STATUS } from '@/consts/item'; export type ItemTypeT = 'wish' | 'tournament'; export type ItemT = { - id: number; + /** 상품 정보가 정해지기 전에는 null */ + id: number | null; status: ItemStatusT; name: string | null; price: number | null; @@ -11,6 +12,10 @@ export type ItemT = { imageUrl: string | null; sourceUrl: string | null; sourcePlatform: string | null; + method: ItemExtractMethodT | null; + userInput: boolean; + /** 새로고침 중에도 값과 상태는 새로고침 전 것이 유지됨 */ + refreshing: boolean; }; export type ItemStatusT = @@ -24,24 +29,21 @@ export type PatchItemRequestT = { // currency?: string; }; -export type PriceHistorySourceT = - /** 서버에서 추출한 가격 */ - | 'SERVER' - /** 서버에서 LLM 사용하여 추출한 가격 */ - | 'SERVER_LLM' - /** 사용자가 수동으로 입력한 가격 */ - | 'MANUAL' - /** price history가 없는 경우 (도입 전) */ - | null; +export type ItemExtractMethodT = + /** 상품 페이지의 구조화된 데이터에서 추출한 값 */ + | 'STRUCTURED' + /** LLM 을 사용해 추출한 값 */ + | 'LLM'; export type PriceHistoryT = { price: number; extractedAt: string; - source: PriceHistorySourceT; + method: ItemExtractMethodT | null; + userInput: boolean; /** - * - true: 가격 수동 갱신 주최가 본인인 경우 (source가 MANUAL인 경우) - * - false: 가격 수동 갱신 주최가 본인이 아닌 경우 (source가 MANUAL인 경우) - * - null: source가 MANUAL이 아닌 경우 + * - true: 가격 수동 갱신 주최가 본인인 경우 (userInput이 true인 경우) + * - false: 가격 수동 갱신 주최가 본인이 아닌 경우 (userInput이 true인 경우) + * - null: userInput이 false인 경우 */ editedByMe: boolean | null; }; diff --git a/apps/web/src/types/tournament.ts b/apps/web/src/types/tournament.ts index 019f7082..68220f8f 100644 --- a/apps/web/src/types/tournament.ts +++ b/apps/web/src/types/tournament.ts @@ -8,7 +8,8 @@ export type TournamentPlayTypeT = (typeof TOURNAMENT_PLAY_TYPE)[keyof typeof TOU export type TournamentItemT = { tournamentItemId: number; - itemId?: number; + /** 상품 정보가 정해지기 전에는 null */ + itemId?: number | null; name: string; price: number; currency: string; diff --git a/apps/web/src/types/wish.ts b/apps/web/src/types/wish.ts index 079ea20d..b0a1dd2c 100644 --- a/apps/web/src/types/wish.ts +++ b/apps/web/src/types/wish.ts @@ -9,18 +9,6 @@ export type WishT = { export type GetWishlistResponseT = { wish: WishT; item: ItemT; - /** - * 갱신 필요 여부 - * - 이미지로 등록한 경우: null - * - 링크로 등록한 경우: boolean - */ - refreshNeeded: boolean | null; - /** - * 재사용 여부 - * - 이미지로 등록한 경우: null - * - 링크로 등록한 경우: boolean - */ - reused: boolean | null; /** * 내가 플레이한 토너먼트 중 이 위시가 우승한 토너먼트 id 목록 */ @@ -38,8 +26,6 @@ export type WishListItemT = Omit & { export type PostWishLinkResponseT = { wish: WishT; item: ItemT; - refreshNeeded: boolean; - reused: boolean; }; export type PostWishPresignedUrlRequestT = { @@ -57,6 +43,4 @@ export type PostWishImagesRequestT = { export type PostWishImagesResponseT = { wish: WishT; item: ItemT; - refreshNeeded: null; - reused: null; }[]; diff --git a/apps/web/src/utils/item.ts b/apps/web/src/utils/item.ts deleted file mode 100644 index 437d7c92..00000000 --- a/apps/web/src/utils/item.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ITEM_STATUS } from '@/consts/item'; -import type { ItemStatusT } from '@/types/item'; - -export const hasParsingItems = (items: { status?: ItemStatusT }[]) => - items.some(item => item.status === ITEM_STATUS.PENDING || item.status === ITEM_STATUS.PROCESSING);