From 82604793c15e433964e630e0b07114216fd46fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Sat, 15 Aug 2026 23:40:23 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EC=9C=84=EC=8B=9C=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=ED=85=9C=20INCOMPLETE=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서버가 추출 결과를 일부만 채웠을 때 INCOMPLETE 를 내려준다 (TeamPiKi/core#945). 기존 코드는 FAILED·PENDING·PROCESSING 이 아니면 전부 정상 카드로 그려, 이름·가격이 빈 칸인 카드가 보이고 채우라는 유도가 없었다 - 위시 그리드에서 INCOMPLETE 를 FAILED 와 같은 편집 유도 카드로 보내되 문구만 "일부만 가져왔어요" 로 가른다 - name·price 를 nullable 로 바꾸면서 카드 컴포넌트도 nullable 을 받게 했다. INCOMPLETE 를 앞에서 걸러 실제로 빈 값이 정상 카드에 들어가지는 않는다 - 토너먼트: 담기 후보에서 INCOMPLETE 를 제외하고(서버가 출전을 막는다), 바스켓에서는 클릭 가능하게 둔다(값을 채워야 하므로) --- .../web/e2e/specs/tournament/tournamentItemAdd.spec.ts | 2 +- apps/web/src/app/archive/wish/[id]/_types/wish.ts | 8 ++++++++ .../wish/_components/wish-grid/WishFailedCard.tsx | 4 ++-- .../app/archive/wish/_components/wish-grid/index.tsx | 8 ++++++-- .../tournament-item-basket/TournamentBasketItem.tsx | 3 ++- .../[id]/create/by-wish/_components/ByWishContent.tsx | 6 +++++- .../[id]/create/by-wish/_components/WishSelectCard.tsx | 4 ++-- apps/web/src/components/common/wish-card/index.tsx | 10 ++++++---- apps/web/src/consts/item.ts | 2 ++ apps/web/src/types/item.ts | 5 +++-- 10 files changed, 37 insertions(+), 15 deletions(-) diff --git a/apps/web/e2e/specs/tournament/tournamentItemAdd.spec.ts b/apps/web/e2e/specs/tournament/tournamentItemAdd.spec.ts index 2ada2a41..4d4593a2 100644 --- a/apps/web/e2e/specs/tournament/tournamentItemAdd.spec.ts +++ b/apps/web/e2e/specs/tournament/tournamentItemAdd.spec.ts @@ -64,7 +64,7 @@ test('위시에서 상품 4개를 가져오면 장바구니에 담긴다', async await expect(nextButton).toBeDisabled(); for (const { item } of MOCK_WISHLIST_ENTRIES) { - await page.getByRole('button', { name: item.name }).click(); + await page.getByRole('button', { name: item.name ?? '' }).click(); } /** 담기 성공 후 재조회부터는 4개 담긴 응답 (뒤에 등록한 목이 우선 매칭) */ 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 5bb37c68..392a7b00 100644 --- a/apps/web/src/app/archive/wish/[id]/_types/wish.ts +++ b/apps/web/src/app/archive/wish/[id]/_types/wish.ts @@ -19,6 +19,14 @@ type ItemT = { price: null; currency: null; } + | { + /** 추출이 일부만 채운 상태 — 채운 필드만 값이 있다 */ + status: (typeof ITEM_STATUS)['INCOMPLETE']; + name: string | null; + imageUrl: string | null; + price: number | null; + currency: string | null; + } | { status: (typeof ITEM_STATUS)['READY']; name: string; diff --git a/apps/web/src/app/archive/wish/_components/wish-grid/WishFailedCard.tsx b/apps/web/src/app/archive/wish/_components/wish-grid/WishFailedCard.tsx index 053504c2..abdf32ea 100644 --- a/apps/web/src/app/archive/wish/_components/wish-grid/WishFailedCard.tsx +++ b/apps/web/src/app/archive/wish/_components/wish-grid/WishFailedCard.tsx @@ -1,13 +1,13 @@ import { WarningIconFill } from '@/assets/icons'; -function WishFailedCard() { +function WishFailedCard({ message }: { message: string }) { return (
-

가져오는데 실패했어요

+

{message}

직접 입력하기

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 19fdc43b..00ef63a7 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 @@ -29,7 +29,7 @@ function WishGrid({ items, isDeleteMode = false, selectedIds, onToggleSelect }: return (
{items.map(({ wish, item }, index) => { - if (item.status === 'FAILED') + if (item.status === 'FAILED' || item.status === 'INCOMPLETE') return ( handleCardClick(event, wish.id)} > - + ); else if (item.status === 'PENDING' || item.status === 'PROCESSING') diff --git a/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentBasketItem.tsx b/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentBasketItem.tsx index 02228380..25c2d3a1 100644 --- a/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentBasketItem.tsx +++ b/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentBasketItem.tsx @@ -28,7 +28,8 @@ function TournamentBasketItem({
diff --git a/apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx b/apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx index ad090598..403e25c9 100644 --- a/apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx +++ b/apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx @@ -37,7 +37,11 @@ function ByWishContent({ tournamentId }: ByWishContentProps) { const existingItemIds = new Set(pending?.items.map(i => i.itemId) ?? []); const items = wishlistData.filter( ({ item }) => - item.status !== 'FAILED' && item.status !== 'PROCESSING' && !existingItemIds.has(item.id) + item.status !== 'FAILED' && + item.status !== 'PROCESSING' && + /** 서버가 미완성 아이템의 토너먼트 출전을 막는다 */ + item.status !== 'INCOMPLETE' && + !existingItemIds.has(item.id) ); const isWishlistLoaded = !hasNextPage && !isFetchingNextPage; diff --git a/apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectCard.tsx b/apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectCard.tsx index fc804330..03154cde 100644 --- a/apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectCard.tsx +++ b/apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectCard.tsx @@ -2,8 +2,8 @@ import { CheckboxEmptyIconFill, CheckboxSelectedIconFill } from '@/assets/icons' import WishCard from '@/components/common/wish-card'; type WishSelectCardProps = { - name: string; - price: number; + name: string | null; + price: number | null; imageUrl: string | null; sourcePlatform?: string | null; isSelected: boolean; diff --git a/apps/web/src/components/common/wish-card/index.tsx b/apps/web/src/components/common/wish-card/index.tsx index 64b95843..6858d584 100644 --- a/apps/web/src/components/common/wish-card/index.tsx +++ b/apps/web/src/components/common/wish-card/index.tsx @@ -4,8 +4,8 @@ import Skeleton from '@/components/skeleton'; import formatPrice from '@/utils/formatPrice'; type WishCardProps = { - name: string; - price: number; + name: string | null; + price: number | null; imageUrl: string | null; sourcePlatform?: string | null; preload?: boolean; @@ -19,7 +19,7 @@ function WishCard({ name, price, imageUrl, sourcePlatform, preload = false }: Wi {imageUrl ? ( } @@ -40,7 +40,9 @@ function WishCard({ name, price, imageUrl, sourcePlatform, preload = false }: Wi {/* 상품명 + 가격 + 커머스칩 */}
-

{name}

+

+ {name} +

{formatPrice(String(price))}

{sourcePlatform && ( diff --git a/apps/web/src/consts/item.ts b/apps/web/src/consts/item.ts index 83c12e4a..fe153ff9 100644 --- a/apps/web/src/consts/item.ts +++ b/apps/web/src/consts/item.ts @@ -2,5 +2,7 @@ export const ITEM_STATUS = { PENDING: 'PENDING', PROCESSING: 'PROCESSING', READY: 'READY', + /** 추출이 일부 필드만 채운 상태. 사용자가 나머지를 채우면 READY 가 된다 */ + INCOMPLETE: 'INCOMPLETE', FAILED: 'FAILED', } as const; diff --git a/apps/web/src/types/item.ts b/apps/web/src/types/item.ts index d5ffe477..68c64401 100644 --- a/apps/web/src/types/item.ts +++ b/apps/web/src/types/item.ts @@ -5,8 +5,9 @@ export type ItemTypeT = 'wish' | 'tournament'; export type ItemT = { id: number; status: ItemStatusT; - name: string; - price: number; + /** INCOMPLETE 면 비어 있을 수 있다 */ + name: string | null; + price: number | null; currency: string | null; imageUrl: string | null; sourceUrl: string | null; From 4177ae4b74a8c8b72df8747cdb52a49684d4c4f8 Mon Sep 17 00:00:00 2001 From: kanghaeun Date: Sun, 16 Aug 2026 00:57:15 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=ED=86=A0=EB=84=88=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EC=97=90=20incomplete=20=EC=83=81=ED=83=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tournament/[id]/item/[itemId]/_types/tournamentItem.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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 2d9f7983..2c665b80 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 @@ -18,6 +18,12 @@ export type GetTournamentItemResponseT = { imageUrl?: undefined; price?: undefined; } + | { + status: (typeof ITEM_STATUS)['INCOMPLETE']; + name?: string; + imageUrl?: string; + price?: number; + } | { status: (typeof ITEM_STATUS)['READY']; name: string; From a2cbd9ab252dcc759946ea4ac3e1a602d4d9f99e Mon Sep 17 00:00:00 2001 From: kanghaeun Date: Sun, 16 Aug 2026 01:00:57 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=EB=A6=AC=ED=84=B0=EB=9F=B4/?= =?UTF-8?q?=EC=83=81=EC=88=98=EB=A1=9C=20=EC=84=9E=EC=96=B4=20=EC=93=B0?= =?UTF-8?q?=EB=8D=98=20=EA=B3=B3=EC=9D=84=20ITEM=5FSTATUS=20=EB=A1=9C=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/archive/wish/_components/wish-grid/index.tsx | 9 ++++++--- .../[id]/create/_components/product-image/index.tsx | 8 ++++++-- .../tournament-item-basket/TournamentBasketItem.tsx | 5 ++++- .../[id]/create/by-wish/_components/ByWishContent.tsx | 8 ++++---- 4 files changed, 20 insertions(+), 10 deletions(-) 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 00ef63a7..a71c3271 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 @@ -3,6 +3,7 @@ import type { MouseEvent } from 'react'; import { CheckboxEmptyIconFill, CheckboxSelectedIconFill } from '@/assets/icons'; import WishCard from '@/components/common/wish-card'; +import { ITEM_STATUS } from '@/consts/item'; import { ROUTES } from '@/consts/route'; import { Z_INDEX } from '@/consts/zIndex'; import type { GetWishlistResponseT } from '@/types/wish'; @@ -29,7 +30,7 @@ function WishGrid({ items, isDeleteMode = false, selectedIds, onToggleSelect }: return (
{items.map(({ wish, item }, index) => { - if (item.status === 'FAILED' || item.status === 'INCOMPLETE') + if (item.status === ITEM_STATUS.FAILED || item.status === ITEM_STATUS.INCOMPLETE) return ( ); - else if (item.status === 'PENDING' || item.status === 'PROCESSING') + else if (item.status === ITEM_STATUS.PENDING || item.status === ITEM_STATUS.PROCESSING) return ; if (isDeleteMode) { diff --git a/apps/web/src/app/tournament/[id]/create/_components/product-image/index.tsx b/apps/web/src/app/tournament/[id]/create/_components/product-image/index.tsx index d7be46cb..5b826eff 100644 --- a/apps/web/src/app/tournament/[id]/create/_components/product-image/index.tsx +++ b/apps/web/src/app/tournament/[id]/create/_components/product-image/index.tsx @@ -4,6 +4,7 @@ import type { ImageProps } from 'next/image'; import type { ReactNode } from 'react'; import BaseImage from '@/components/base-image'; +import { ITEM_STATUS } from '@/consts/item'; import type { ItemStatusT } from '@/types/item'; import { cn } from '@/utils/cn'; @@ -51,8 +52,11 @@ function ProductImage({ className={containerClass} {...(!fill ? { style: { width: dimension, height: dimension } } : {})} > - {(parsingStatus === 'PENDING' || parsingStatus === 'PROCESSING') && } - {parsingStatus === 'FAILED' && + {(parsingStatus === ITEM_STATUS.PENDING || parsingStatus === ITEM_STATUS.PROCESSING) && ( + + )} + {/* INCOMPLETE 는 이미지를 못 건진 경우라 FAILED 와 같은 빈 이미지 자리를 쓴다 (디자인 확인 전 임시) */} + {(parsingStatus === ITEM_STATUS.FAILED || parsingStatus === ITEM_STATUS.INCOMPLETE) && (size === 'lg' ? : )}
); diff --git a/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentBasketItem.tsx b/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentBasketItem.tsx index 25c2d3a1..1a96f92b 100644 --- a/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentBasketItem.tsx +++ b/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentBasketItem.tsx @@ -1,6 +1,7 @@ import Image from 'next/image'; import ProductImage from '@/app/tournament/[id]/create/_components/product-image'; +import { ITEM_STATUS } from '@/consts/item'; import { Z_INDEX } from '@/consts/zIndex'; import { useGetMe } from '@/hooks/useGetMe'; import { cn } from '@/utils/cn'; @@ -28,7 +29,9 @@ function TournamentBasketItem({
i.itemId) ?? []); const items = wishlistData.filter( ({ item }) => - item.status !== 'FAILED' && - item.status !== 'PROCESSING' && - /** 서버가 미완성 아이템의 토너먼트 출전을 막는다 */ - item.status !== 'INCOMPLETE' && + item.status !== ITEM_STATUS.FAILED && + item.status !== ITEM_STATUS.PROCESSING && + item.status !== ITEM_STATUS.INCOMPLETE && !existingItemIds.has(item.id) ); From a4fd17a27d2aede9925418c5af707dd211c73423 Mon Sep 17 00:00:00 2001 From: kanghaeun Date: Sun, 16 Aug 2026 01:01:24 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=ED=86=A0=EB=84=88=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20=EC=8B=9C=EC=9E=91=20=EC=B0=A8=EB=8B=A8=EC=97=90=20?= =?UTF-8?q?INCOMPLETE=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[id]/create/_components/TournamentCreateClient.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 3d7a4918..4e283562 100644 --- a/apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx +++ b/apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import { Dialog } from '@/components/dialog'; import GetItemDialogContent from '@/components/get-item-dialog'; +import { ITEM_STATUS } from '@/consts/item'; import { QUERY_ACTION } from '@/consts/queryAction'; import { useGetMe } from '@/hooks/useGetMe'; import { useQueryAction } from '@/hooks/useQueryAction'; @@ -75,6 +76,10 @@ function TournamentCreateClient({ tournamentId }: TournamentCreateClientProps) { const hasPendingItem = hasParsingItems(pending?.items ?? []); useSSEFallback(['tournament', tournamentId], hasPendingItem); + const hasUnfinishedItem = (pending?.items ?? []).some( + item => item.status === ITEM_STATUS.FAILED || item.status === ITEM_STATUS.INCOMPLETE + ); + // 주최자가 ROOT 를 시작한 후(ownerStarted=true) 에는 초대 기간이 이미 종료된 상태라 // inviteExpiresAt 이 null 이고 담기 마감 카운트다운도 의미 없다. const ownerStarted = pending?.ownerStarted ?? false; @@ -211,9 +216,7 @@ function TournamentCreateClient({ tournamentId }: TournamentCreateClientProps) { item.status === 'FAILED') ?? false) - } + hasUnreadyItem={hasPendingItem || hasUnfinishedItem} hasFriends={hasFriends} isWaitingForOwnerStart={isWaitingForOwnerStart} isDepositClosed={isDepositClosed} From 58c8fa1d9b157e13a5a460b45dd2beb228ac2bbc Mon Sep 17 00:00:00 2001 From: kanghaeun Date: Sun, 16 Aug 2026 01:01:42 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20INCOMPLETE=20=EB=A5=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tournament-item-basket/TournamentItemBasket.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasket.tsx b/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasket.tsx index 730f7834..9b252391 100644 --- a/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasket.tsx +++ b/apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasket.tsx @@ -6,6 +6,7 @@ import { useState } from 'react'; import AddIcon from '@/assets/icons/fill/add.svg'; import { Dialog, DialogTrigger } from '@/components/dialog'; import GetItemDialogContent from '@/components/get-item-dialog'; +import { ITEM_STATUS } from '@/consts/item'; import { ROUTES } from '@/consts/route'; import type { TournamentPendingItemT } from '../../../_common/_types/tournamentResponse'; @@ -76,7 +77,7 @@ function TournamentItemBasket({
{addSlot} {items.map((item, index) => { - if (item.status === 'READY') { + if (item.status === ITEM_STATUS.READY || item.status === ITEM_STATUS.INCOMPLETE) { return ( Date: Sun, 16 Aug 2026 01:05:26 +0900 Subject: [PATCH 6/6] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=8B=B1=20=EC=83=81=ED=83=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/archive/wish/[id]/_types/wish.ts | 1 - apps/web/src/consts/item.ts | 1 - apps/web/src/types/item.ts | 1 - 3 files changed, 3 deletions(-) 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 392a7b00..b551c192 100644 --- a/apps/web/src/app/archive/wish/[id]/_types/wish.ts +++ b/apps/web/src/app/archive/wish/[id]/_types/wish.ts @@ -20,7 +20,6 @@ type ItemT = { currency: null; } | { - /** 추출이 일부만 채운 상태 — 채운 필드만 값이 있다 */ status: (typeof ITEM_STATUS)['INCOMPLETE']; name: string | null; imageUrl: string | null; diff --git a/apps/web/src/consts/item.ts b/apps/web/src/consts/item.ts index fe153ff9..8d8d44ac 100644 --- a/apps/web/src/consts/item.ts +++ b/apps/web/src/consts/item.ts @@ -2,7 +2,6 @@ export const ITEM_STATUS = { PENDING: 'PENDING', PROCESSING: 'PROCESSING', READY: 'READY', - /** 추출이 일부 필드만 채운 상태. 사용자가 나머지를 채우면 READY 가 된다 */ INCOMPLETE: 'INCOMPLETE', FAILED: 'FAILED', } as const; diff --git a/apps/web/src/types/item.ts b/apps/web/src/types/item.ts index 68c64401..5a4944be 100644 --- a/apps/web/src/types/item.ts +++ b/apps/web/src/types/item.ts @@ -5,7 +5,6 @@ export type ItemTypeT = 'wish' | 'tournament'; export type ItemT = { id: number; status: ItemStatusT; - /** INCOMPLETE 면 비어 있을 수 있다 */ name: string | null; price: number | null; currency: string | null;