From 440c741b11dd84445e9c7e30abb4c964e4865427 Mon Sep 17 00:00:00 2001 From: iOdiO89 <117376841+iOdiO89@users.noreply.github.com> Date: Sun, 2 Aug 2026 00:50:31 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=EC=9C=84=EC=8B=9C=20item=20statu?= =?UTF-8?q?s=20ready=EC=9D=BC=20=EB=95=8C=EB=8F=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wish/[id]/_components/EditContent.tsx | 8 +-- .../wish/[id]/_components/ItemEditForm.tsx | 2 +- .../common/item-edit-form/index.tsx | 62 +++++++++---------- 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/apps/web/src/app/archive/wish/[id]/_components/EditContent.tsx b/apps/web/src/app/archive/wish/[id]/_components/EditContent.tsx index f86b5840..c85021ed 100644 --- a/apps/web/src/app/archive/wish/[id]/_components/EditContent.tsx +++ b/apps/web/src/app/archive/wish/[id]/_components/EditContent.tsx @@ -1,7 +1,7 @@ 'use client'; -import { Header, HeaderIcon } from '@/components/header'; import ItemLinkBanner from '@/components/common/item-link-banner'; +import { Header, HeaderIcon } from '@/components/header'; import { useGetWish } from '../_hooks/useGetWish'; import ItemEditForm from './ItemEditForm'; @@ -24,9 +24,9 @@ function EditContent({ wishId }: EditContentProps) { {wishData.item.sourceUrl && } diff --git a/apps/web/src/app/archive/wish/[id]/_components/ItemEditForm.tsx b/apps/web/src/app/archive/wish/[id]/_components/ItemEditForm.tsx index 76835f83..970a054e 100644 --- a/apps/web/src/app/archive/wish/[id]/_components/ItemEditForm.tsx +++ b/apps/web/src/app/archive/wish/[id]/_components/ItemEditForm.tsx @@ -10,7 +10,7 @@ import { usePostWishRefresh } from '../_hooks/usePostWishRefresh'; type ItemEditFormProps = { wishId: number; itemStatus: ItemStatusT; - initialImageUrl: string | null; + initialImageUrl: string; initialName: string; initialPrice: number; }; diff --git a/apps/web/src/components/common/item-edit-form/index.tsx b/apps/web/src/components/common/item-edit-form/index.tsx index 09034a35..7565cc82 100644 --- a/apps/web/src/components/common/item-edit-form/index.tsx +++ b/apps/web/src/components/common/item-edit-form/index.tsx @@ -70,11 +70,7 @@ function ItemEditForm({ return ( <> - + @@ -86,7 +82,6 @@ function ItemEditForm({ onChange={event => setName(event.target.value)} maxLength={50} autoCorrect="off" - disabled={itemStatus === 'READY'} /> setPrice(prev => formatPrice(prev))} inputMode="numeric" autoCorrect="off" - disabled={itemStatus === 'READY'} /> @@ -113,6 +107,21 @@ function ItemEditForm({ > 삭제하기 + + )} + + + + {itemStatus === 'READY' && onRefresh && ( - - )} - - {itemStatus === 'FAILED' && ( - - - - - )} + )} + + ); } From 31ad8e0509a2d457bfdecd49d467143febc179dc Mon Sep 17 00:00:00 2001 From: iOdiO89 <117376841+iOdiO89@users.noreply.github.com> Date: Sun, 2 Aug 2026 00:53:11 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=ED=86=A0=EB=84=88=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20item=20status=20ready=EC=9D=BC=20=EB=95=8C=EB=8F=84?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[itemId]/_apis/patchTournamentItem.ts | 6 +---- .../item/[itemId]/_components/EditContent.tsx | 10 ++++----- .../item/[itemId]/_types/tournamentItem.ts | 22 +++++++++---------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/apps/web/src/app/tournament/[id]/item/[itemId]/_apis/patchTournamentItem.ts b/apps/web/src/app/tournament/[id]/item/[itemId]/_apis/patchTournamentItem.ts index 54581571..6fe1399e 100644 --- a/apps/web/src/app/tournament/[id]/item/[itemId]/_apis/patchTournamentItem.ts +++ b/apps/web/src/app/tournament/[id]/item/[itemId]/_apis/patchTournamentItem.ts @@ -2,17 +2,13 @@ import { clientApi } from '@/apis/client'; import { ENDPOINTS } from '@/consts/api'; import type { ApiResponseT } from '@/types/api'; -import type { PatchTournamentItemResponseT } from '../_types/tournamentItem'; - export const patchTournamentItem = async ( tournamentId: number, tournamentItemId: number, formData: FormData ) => { - const { data } = await clientApi.patch>( + await clientApi.patch>( ENDPOINTS.TOURNAMENT_ITEM(tournamentId, tournamentItemId), formData ); - - return data.data; }; diff --git a/apps/web/src/app/tournament/[id]/item/[itemId]/_components/EditContent.tsx b/apps/web/src/app/tournament/[id]/item/[itemId]/_components/EditContent.tsx index 166b5f29..b6d61888 100644 --- a/apps/web/src/app/tournament/[id]/item/[itemId]/_components/EditContent.tsx +++ b/apps/web/src/app/tournament/[id]/item/[itemId]/_components/EditContent.tsx @@ -1,7 +1,7 @@ 'use client'; -import { Header, HeaderIcon } from '@/components/header'; import ItemLinkBanner from '@/components/common/item-link-banner'; +import { Header, HeaderIcon } from '@/components/header'; import { cn } from '@/utils/cn'; import { useGetTournamentItem } from '../_hooks/useGetTournamentItem'; @@ -31,11 +31,9 @@ function EditContent({ tournamentId, tournamentItemId }: EditContentProps) { tournamentId={tournamentId} tournamentItemId={tournamentItemId} itemStatus={tournamentItemData.status} - initialImageUrl={ - tournamentItemData.status === 'READY' ? tournamentItemData.imageUrl : null - } - initialName={tournamentItemData.status === 'READY' ? tournamentItemData.name : ''} - initialPrice={tournamentItemData.status === 'READY' ? tournamentItemData.price : 0} + initialImageUrl={tournamentItemData.imageUrl ?? ''} + initialName={tournamentItemData.name ?? ''} + initialPrice={tournamentItemData.price ?? 0} /> {tournamentItemData.status === 'READY' && tournamentItemData.sourceUrl && ( 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 ea7ea81a..2730fb83 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 @@ -3,25 +3,23 @@ import type { ITEM_STATUS } from '@/consts/item'; export type GetTournamentItemResponseT = { tournamentItemId: number; itemId: number; + /** 이미지로 등록한 경우 undefined */ + sourceUrl?: string; + currency?: string; } & ( | { - status: (typeof ITEM_STATUS)['PROCESSING'] | (typeof ITEM_STATUS)['PENDING']; - name: null; - imageUrl: null; - price: null; - currency: string; + status: + | (typeof ITEM_STATUS)['PENDING'] + | (typeof ITEM_STATUS)['PROCESSING'] + | (typeof ITEM_STATUS)['FAILED']; + name?: undefined; + imageUrl?: undefined; + price?: undefined; } | { status: (typeof ITEM_STATUS)['READY']; name: string; imageUrl: string; price: number; - currency: string; - sourceUrl?: string; - } - | { - status: (typeof ITEM_STATUS)['FAILED']; } ); - -export type PatchTournamentItemResponseT = GetTournamentItemResponseT; From 56d27bf2b1b0e12a12979d54208e061182d51d24 Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Mon, 3 Aug 2026 16:05:54 +0900 Subject: [PATCH 03/10] =?UTF-8?q?chore:=20=EA=B3=B5=ED=86=B5=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=ED=83=80=EC=9E=85=EC=97=90=20pageResponse=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/e2e/helpers/apiResponse.ts | 4 ++++ apps/web/src/types/api.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/apps/web/e2e/helpers/apiResponse.ts b/apps/web/e2e/helpers/apiResponse.ts index 4c7f2afa..47e4361f 100644 --- a/apps/web/e2e/helpers/apiResponse.ts +++ b/apps/web/e2e/helpers/apiResponse.ts @@ -6,6 +6,10 @@ export const createApiSuccess = (data: T, status = 200): ApiResponseT => ( data, detail: '요청이 정상적으로 처리되었습니다.', code: 'COMMON_SUCCESS', + pageResponse: { + nextCursor: null, + hasNext: false, + }, }); type CreateApiErrorOptionsT = { diff --git a/apps/web/src/types/api.ts b/apps/web/src/types/api.ts index bc67bb56..e136be71 100644 --- a/apps/web/src/types/api.ts +++ b/apps/web/src/types/api.ts @@ -4,6 +4,10 @@ export type ApiResponseT = { data: T; detail: string; code: string; + pageResponse: { + nextCursor: string | null; + hasNext: boolean; + }; }; // 공통 에러 응답 타입 From 03a88017b71fc6090242b5f056a0b849b197a20b Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Mon, 3 Aug 2026 16:05:58 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EC=9C=84=EC=8B=9C=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=97=90=20refreshNeeded,=20reused=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/archive/wish/[id]/_types/wish.ts | 12 ++++++ apps/web/src/types/wish.ts | 41 ++++++++++++------- 2 files changed, 38 insertions(+), 15 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 3729c588..3802cc9d 100644 --- a/apps/web/src/app/archive/wish/[id]/_types/wish.ts +++ b/apps/web/src/app/archive/wish/[id]/_types/wish.ts @@ -21,6 +21,18 @@ export type GetWishResponseT = { sourceUrl: string | null; } ); + /** + * 갱신 필요 여부 + * - 이미지로 등록한 경우: null + * - 링크로 등록한 경우: boolean + */ + refreshNeeded: boolean | null; + /** + * 재사용 여부 + * - 이미지로 등록한 경우: null + * - 링크로 등록한 경우: boolean + */ + reused: boolean | null; }; export type PatchWishResponseT = GetWishResponseT; diff --git a/apps/web/src/types/wish.ts b/apps/web/src/types/wish.ts index 8c92f194..e157d29c 100644 --- a/apps/web/src/types/wish.ts +++ b/apps/web/src/types/wish.ts @@ -1,26 +1,37 @@ -import type { ItemStatusT, ItemT } from './item'; +import type { ItemT } from './item'; -export type PostWishOCRResponseT = { +export type WishT = { + id: number; + createdAt: string; +}; + +export type GetWishlistResponseT = { wish: WishT; item: ItemT; + /** + * 갱신 필요 여부 + * - 이미지로 등록한 경우: null + * - 링크로 등록한 경우: boolean + */ + refreshNeeded: boolean | null; + /** + * 재사용 여부 + * - 이미지로 등록한 경우: null + * - 링크로 등록한 경우: boolean + */ + reused: boolean | null; }; export type PostWishLinkResponseT = { wish: WishT; item: ItemT; + refreshNeeded: boolean; + reused: boolean; }; -export type WishT = { - id: number; - createdAt: string; -}; - -export type WishItemT = { - id: number; - itemId: number; - name: string; - price: number; - imageUrl: string | null; - status: ItemStatusT; - sourcePlatform: string | null; +export type PostWishOCRResponseT = { + wish: WishT; + item: ItemT; + refreshNeeded: null; + reused: null; }; From ab121c9208c51bdd5573b9c7d6405ccf081ebb23 Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Mon, 3 Aug 2026 16:06:03 +0900 Subject: [PATCH 05/10] =?UTF-8?q?refactor:=20=EC=9C=84=EC=8B=9C=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9D=91=EB=8B=B5=20=EB=A7=A4=ED=95=91=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=98=EA=B3=A0=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/e2e/mocks/wish.ts | 13 ++--- apps/web/src/apis/getWishlist.ts | 57 +++++-------------- .../archive/wish/_components/WishContent.tsx | 8 ++- .../wish/_components/WishContentClient.tsx | 4 +- .../wish/_components/WishGridContent.tsx | 4 +- .../archive/wish/_components/WishlistList.tsx | 2 +- .../wish/_components/wish-grid/index.tsx | 36 ++++++------ .../by-wish/_components/ByWishContent.tsx | 21 +++---- apps/web/src/hooks/useGetWishlist.ts | 4 +- 9 files changed, 56 insertions(+), 93 deletions(-) diff --git a/apps/web/e2e/mocks/wish.ts b/apps/web/e2e/mocks/wish.ts index 8b471152..bb3e1702 100644 --- a/apps/web/e2e/mocks/wish.ts +++ b/apps/web/e2e/mocks/wish.ts @@ -1,20 +1,13 @@ -import type { ItemT } from '@/types/item'; -import type { WishT } from '@/types/wish'; +import type { GetWishlistResponseT } from '@/types/wish'; import { MOCK_IMAGE_URLS } from './images'; import { MOCK_TOURNAMENT_ITEMS } from './tournament'; -/** getWishlist 원 응답의 data 항목 형태 (apis/getWishlist.ts 의 WishlistEntryT 와 동일) */ -type WishlistEntryT = { - wish: WishT; - item: ItemT; -}; - /** * by-wish 담기 spec 용 위시 4개. * item.id 를 토너먼트 목의 itemId 와 맞춰, 담기 후 pending.items 와 자연스럽게 이어지게 한다. */ -export const MOCK_WISHLIST_ENTRIES: WishlistEntryT[] = MOCK_TOURNAMENT_ITEMS.map( +export const MOCK_WISHLIST_ENTRIES: GetWishlistResponseT[] = MOCK_TOURNAMENT_ITEMS.map( (tournamentItem, index) => ({ wish: { id: index + 1, createdAt: '2026-01-01T00:00:00Z' }, item: { @@ -27,5 +20,7 @@ export const MOCK_WISHLIST_ENTRIES: WishlistEntryT[] = MOCK_TOURNAMENT_ITEMS.map sourceUrl: null, sourcePlatform: null, }, + reused: null, + refreshNeeded: null, }) ); diff --git a/apps/web/src/apis/getWishlist.ts b/apps/web/src/apis/getWishlist.ts index 2bab33df..4d758777 100644 --- a/apps/web/src/apis/getWishlist.ts +++ b/apps/web/src/apis/getWishlist.ts @@ -4,54 +4,23 @@ import { clientApi } from '@/apis/client'; import { serverApi } from '@/apis/server'; import { ENDPOINTS } from '@/consts/api'; import type { ApiResponseT } from '@/types/api'; -import type { ItemT } from '@/types/item'; -import type { WishItemT, WishT } from '@/types/wish'; +import type { GetWishlistResponseT } from '@/types/wish'; -type WishlistEntryT = { - wish: WishT; - item: ItemT; -}; - -type WishlistApiResponseT = ApiResponseT & { - pageResponse: { - nextCursor: string | null; - hasNext: boolean; - }; -}; - -export type WishlistPageT = { - items: WishItemT[]; - nextCursor: string | null; - hasNext: boolean; -}; - -const mapWishlist = (entries: WishlistEntryT[]): WishItemT[] => - entries.map(({ wish, item }) => ({ - id: wish.id, - itemId: item.id, - status: item.status, - name: item.name ?? '', - price: item.currentPrice ?? 0, - imageUrl: item.imageUrl ?? null, - sourcePlatform: item.sourcePlatform ?? null, - })); - -export const getWishlist = async (cursor: string | null = null): Promise => { +export const getWishlist = async (cursor: string | null = null) => { const params = { size: 20, ...(cursor ? { cursor } : {}) }; if (environmentManager.isServer()) { - const { data } = await serverApi.get(ENDPOINTS.WISHLISTS, { params }); - return { - items: mapWishlist(data.data), - nextCursor: data.pageResponse.nextCursor, - hasNext: data.pageResponse.hasNext, - }; + const { data } = await serverApi.get>( + ENDPOINTS.WISHLISTS, + { + params, + } + ); + return data; } - const { data } = await clientApi.get(ENDPOINTS.WISHLISTS, { params }); - return { - items: mapWishlist(data.data), - nextCursor: data.pageResponse.nextCursor, - hasNext: data.pageResponse.hasNext, - }; + const { data } = await clientApi.get>(ENDPOINTS.WISHLISTS, { + params, + }); + return data; }; diff --git a/apps/web/src/app/archive/wish/_components/WishContent.tsx b/apps/web/src/app/archive/wish/_components/WishContent.tsx index 7f7e5980..09fce7e0 100644 --- a/apps/web/src/app/archive/wish/_components/WishContent.tsx +++ b/apps/web/src/app/archive/wish/_components/WishContent.tsx @@ -1,7 +1,8 @@ import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; -import type { WishlistPageT } from '@/apis/getWishlist'; import { getWishlist } from '@/apis/getWishlist'; +import type { ApiResponseT } from '@/types/api'; +import type { GetWishlistResponseT } from '@/types/wish'; import { getQueryClient } from '@/utils/queryClient'; import WishContentClient from './WishContentClient'; @@ -11,9 +12,10 @@ async function WishContent() { await queryClient.prefetchInfiniteQuery({ queryKey: ['wishlists'], - queryFn: ({ pageParam }) => getWishlist(pageParam as string | null), + queryFn: ({ pageParam }) => getWishlist(pageParam), initialPageParam: null as string | null, - getNextPageParam: (page: WishlistPageT) => (page.hasNext ? page.nextCursor : null), + getNextPageParam: (page: ApiResponseT) => + page.pageResponse.hasNext ? page.pageResponse.nextCursor : null, }); return ( diff --git a/apps/web/src/app/archive/wish/_components/WishContentClient.tsx b/apps/web/src/app/archive/wish/_components/WishContentClient.tsx index 5d891e52..f35ec1b1 100644 --- a/apps/web/src/app/archive/wish/_components/WishContentClient.tsx +++ b/apps/web/src/app/archive/wish/_components/WishContentClient.tsx @@ -38,10 +38,10 @@ function WishContentClient() { const { wishlistData } = useGetWishlist(); const selectableIds = wishlistData .filter( - item => + ({ item }) => item.status !== 'FAILED' && item.status !== 'PENDING' && item.status !== 'PROCESSING' ) - .map(item => item.id); + .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/WishGridContent.tsx b/apps/web/src/app/archive/wish/_components/WishGridContent.tsx index f89bc044..ec35d01f 100644 --- a/apps/web/src/app/archive/wish/_components/WishGridContent.tsx +++ b/apps/web/src/app/archive/wish/_components/WishGridContent.tsx @@ -1,10 +1,10 @@ import { HeartIconFill } from '@/assets/icons'; -import type { WishItemT } from '@/types/wish'; +import type { GetWishlistResponseT } from '@/types/wish'; import WishGrid from './wish-grid'; type WishGridContentProps = { - items: WishItemT[]; + items: GetWishlistResponseT[]; isDeleteMode?: boolean; selectedIds?: Set; onToggleSelect?: (id: number) => void; diff --git a/apps/web/src/app/archive/wish/_components/WishlistList.tsx b/apps/web/src/app/archive/wish/_components/WishlistList.tsx index 91c07817..ea418e2b 100644 --- a/apps/web/src/app/archive/wish/_components/WishlistList.tsx +++ b/apps/web/src/app/archive/wish/_components/WishlistList.tsx @@ -20,7 +20,7 @@ function WishlistList({ isDeleteMode, selectedIds, onToggleSelect }: WishlistLis const sentinelRef = useRef(null); const { wishlistData, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetWishlist(); - const hasPendingItem = hasParsingItems(wishlistData); + const hasPendingItem = hasParsingItems(wishlistData.map(({ item }) => item)); useSSEFallback(['wishlists'], hasPendingItem); useScrollRestoration(); 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 2ab7982c..a2d64bd6 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 @@ -5,14 +5,14 @@ import { CheckboxEmptyIconFill, CheckboxSelectedIconFill } from '@/assets/icons' import WishCard from '@/components/common/wish-card'; import { ROUTES } from '@/consts/route'; import { Z_INDEX } from '@/consts/zIndex'; -import type { WishItemT } from '@/types/wish'; +import type { GetWishlistResponseT } from '@/types/wish'; import { saveWishScroll } from '../../_utils/wishScroll'; import WishFailedCard from './WishFailedCard'; import WishProcessingCard from './WishProcessingCard'; type WishGridProps = { - items: WishItemT[]; + items: GetWishlistResponseT[]; isDeleteMode?: boolean; selectedIds?: Set; onToggleSelect?: (id: number) => void; @@ -28,34 +28,34 @@ function WishGrid({ items, isDeleteMode = false, selectedIds, onToggleSelect }: return (
- {items.map((item, index) => { + {items.map(({ wish, item }, index) => { if (item.status === 'FAILED') return ( handleCardClick(event, item.id)} + href={ROUTES.WISH_EDIT(wish.id)} + key={wish.id} + data-wish-id={wish.id} + onClick={event => handleCardClick(event, wish.id)} > - + ); else if (item.status === 'PENDING' || item.status === 'PROCESSING') - return ; + return ; if (isDeleteMode) { - const isSelected = selectedIds?.has(item.id) ?? false; + const isSelected = selectedIds?.has(wish.id) ?? false; return ( - - )} - - {itemStatus === 'READY' && onRefresh && ( + + {/** 정보 갱신은 READY만 가능 */} + {itemStatus === 'READY' && !!onRefresh && ( )} +