+ {renderProfile(props)}
+
+
{props.nickname}
{props.age} | {props.gender}
-
diff --git a/src/shared/components/card/match-card/components/card-profile-image.tsx b/src/shared/components/card/match-card/components/card-profile-image.tsx
index 31841fbc..f4ee65ba 100644
--- a/src/shared/components/card/match-card/components/card-profile-image.tsx
+++ b/src/shared/components/card/match-card/components/card-profile-image.tsx
@@ -1,10 +1,17 @@
+import { PROFILE_SLOT_COUNT } from '@components/card/constants/MATCH';
import { profileVariants } from '@components/card/match-card/styles/card-variants';
import type { CardProfileProps } from '@components/card/match-card/types/card';
+import { normalizeUrls } from '@components/card/match-card/utils/normalize-urls';
import Icon from '@components/icon/icon';
import { cn } from '@libs/cn';
-import type { ReactNode } from 'react';
-const CardProfile = ({ type, imgUrl = [] }: CardProfileProps) => {
+export type ProfileType = CardProfileProps['type'];
+
+const CardProfile = ({ type, imgUrl }: CardProfileProps) => {
+ const urls = normalizeUrls(imgUrl);
+ const slotCount = PROFILE_SLOT_COUNT[type];
+ const slots = Array.from({ length: slotCount }, (_, i) => urls[i] ?? '');
+
const zIndexClasses = [
'z-[var(--z-card-profile-1)] ml-0',
'-ml-[0.9rem] z-[var(--z-card-profile-2)]',
@@ -12,55 +19,61 @@ const CardProfile = ({ type, imgUrl = [] }: CardProfileProps) => {
'-ml-[0.9rem] z-[var(--z-card-profile-4)]',
];
- if (type === 'group') {
- const filledImages = imgUrl.slice(0, 4);
- const emptySlots = Array(4 - filledImages.length).fill('');
-
- const profileElements: ReactNode[] = [];
-
- filledImages.forEach((url, order) => {
- const key = url || `profile-slot-${order}`;
- profileElements.push(
-
-
-
,
- );
- });
+ const renderGroupItem = (src: string, i: number) => {
+ const hasSrc = src.length > 0;
+ const isEmptyTail = i >= urls.length;
- emptySlots.forEach((_, order) => {
- const slotIndex = filledImages.length + order;
- profileElements.push(
-
+ return (
+
+ {hasSrc ? (
+

+ ) : isEmptyTail ? (
-
,
- );
- });
+ ) : (
+
+ )}
+
+ );
+ };
- return
{profileElements}
;
+ const renderSingleItem = (src: string, i: number) => (
+
+ {src ? (
+

+ ) : (
+
+ )}
+
+ );
+
+ if (type === 'group') {
+ return
{slots.map(renderGroupItem)}
;
}
return (
-
+ {slots.map(renderSingleItem)}
);
};
diff --git a/src/shared/components/card/match-card/styles/card-variants.ts b/src/shared/components/card/match-card/styles/card-variants.ts
index 4b116528..b2ac6199 100644
--- a/src/shared/components/card/match-card/styles/card-variants.ts
+++ b/src/shared/components/card/match-card/styles/card-variants.ts
@@ -22,10 +22,10 @@ export const cardVariants = cva('relative w-full rounded-[12px] bg-white', {
export const profileVariants = cva('overflow-hidden rounded-full object-cover', {
variants: {
type: {
- single: 'h-[6rem] w-[6rem] border border-gray-900',
- group: 'h-[2.8rem] w-[2.8rem] border border-main-600',
- detailed: 'h-[8.2rem] w-[8.2rem] border border-gray-900',
- user: 'h-[8.2rem] w-[8.2rem] border border-gray-900',
+ single: 'h-[6rem] w-[6rem]',
+ group: 'h-[2.8rem] w-[2.8rem]',
+ detailed: 'h-[8.2rem] w-[8.2rem]',
+ user: 'h-[8.2rem] w-[8.2rem]',
},
},
defaultVariants: {
diff --git a/src/shared/components/card/match-card/types/card.ts b/src/shared/components/card/match-card/types/card.ts
index d24fab7e..b8a73743 100644
--- a/src/shared/components/card/match-card/types/card.ts
+++ b/src/shared/components/card/match-card/types/card.ts
@@ -22,6 +22,7 @@ export interface BaseCardProps {
status?: string;
color?: ColorType;
onClick?: () => void;
+ isCreated?: boolean;
}
export interface SingleCardProps extends BaseCardProps {
@@ -34,6 +35,7 @@ export interface SingleCardProps extends BaseCardProps {
team: string;
style: string;
matchRate?: number;
+ isCreated?: boolean;
}
export interface GroupCardProps extends BaseCardProps {
@@ -41,6 +43,7 @@ export interface GroupCardProps extends BaseCardProps {
count: number;
color?: 'active' | 'inactive';
matchRate?: number;
+ isCreated?: boolean;
}
export interface DetailedCardProps extends BaseCardProps {
@@ -52,6 +55,7 @@ export interface DetailedCardProps extends BaseCardProps {
chips: ChipColor[];
team: string;
style: string;
+ isCreated?: boolean;
}
export interface UserCardProps {
@@ -66,6 +70,7 @@ export interface UserCardProps {
className?: string;
color?: 'active' | 'inactive';
chips: ChipColor[];
+ isCreated?: boolean;
}
export type CardProps = SingleCardProps | GroupCardProps | DetailedCardProps | UserCardProps;
diff --git a/src/shared/components/card/match-card/utils/normalize-urls.ts b/src/shared/components/card/match-card/utils/normalize-urls.ts
new file mode 100644
index 00000000..ccb33eed
--- /dev/null
+++ b/src/shared/components/card/match-card/utils/normalize-urls.ts
@@ -0,0 +1,6 @@
+export type ImageUrlInput = string | string[] | undefined;
+
+export const normalizeUrls = (url: ImageUrlInput): string[] => {
+ if (typeof url === 'string') return [url];
+ return url ?? [];
+};
diff --git a/src/shared/components/tab/tab/styles/tab-style.ts b/src/shared/components/tab/tab/styles/tab-style.ts
index 0e25efc9..1a1f2bca 100644
--- a/src/shared/components/tab/tab/styles/tab-style.ts
+++ b/src/shared/components/tab/tab/styles/tab-style.ts
@@ -5,19 +5,21 @@ export const tabStyleMap = {
textInactive: 'text-gray-500',
borderActive: 'border-sub-900',
borderInactive: 'border-transparent',
- borderThickness: 'border-b-[0.4rem]',
+ borderStyle: 'border-b-[0.4rem]',
size: 'h-[3.9rem] w-[5.6rem]',
- typography: 'subhead_18_sb',
+ textStyle: 'subhead_18_sb',
},
match: {
- gap: 'gap-[2.4rem]',
+ gap: 'gap-[2.4rem] px-[1.6rem] border-b border-gray-300',
textActive: 'text-gray-black',
textInactive: 'text-gray-600',
- borderActive: 'border-gray-black',
- borderInactive: 'border-transparent',
- borderThickness: 'border-b-[0.2rem]',
- size: 'h-[3.0rem] w-[4.8rem]',
- typography: 'head_20_sb',
+ borderActive: 'after:bg-gray-black',
+ borderInactive: 'after:bg-transparent',
+ borderStyle:
+ 'relative after:absolute after:bottom-0 after:left-1/2 after:-translate-x-1/2 ' +
+ 'after:h-[0.2rem] after:w-[12rem] after:rounded-full after:content-[""]',
+ size: 'h-[3.0rem] w-[17.2rem] mx-[2.6rem]',
+ textStyle: 'head_20_sb pb-[0.4rem]',
},
} as const;
diff --git a/src/shared/components/tab/tab/tab-item.tsx b/src/shared/components/tab/tab/tab-item.tsx
index 74ec4b24..8633391c 100644
--- a/src/shared/components/tab/tab/tab-item.tsx
+++ b/src/shared/components/tab/tab/tab-item.tsx
@@ -17,13 +17,13 @@ const TabItem = ({ label, isActive, style, onClick }: TabItemProps) => {
className={cn(
isActive ? style.borderActive : style.borderInactive,
style.size,
- style.borderThickness,
+ style.borderStyle,
'flex-row-center cursor-pointer whitespace-nowrap py-[0.6rem]',
)}
>
`/v1/users/match-stage/direct?status=${status}`,
- GET_GROUP_STATUS: (status: string) => `/v1/users/match-stage/group?status=${status}`,
+ GET_SINGLE_STATUS: (status: string) => `/v2/users/match-stage/direct?status=${status}`,
+ GET_GROUP_STATUS: (status: string) => `/v2/users/match-stage/group?status=${status}`,
GET_MATCH_DETAIL: (matchId: number | string) => `/v1/users/match/${matchId}`,
POST_MATCH_REQUEST: (matchId: number | string) => `/v1/users/match-request/${matchId}`,
diff --git a/src/shared/constants/error-toast.ts b/src/shared/constants/error-toast.ts
index e9142301..6dc6dd21 100644
--- a/src/shared/constants/error-toast.ts
+++ b/src/shared/constants/error-toast.ts
@@ -9,4 +9,12 @@ export const MATCH_REQUEST_ERROR_MESSAGES = {
},
};
+export const MATCH_PENDING_TOAST_MESSAGES = {
+ REQUEST_WAITING: '메이트의 요청을 기다리는 중입니다.',
+ APPROVAL_WAITING: {
+ single: '메이트의 승인을 기다리는 중입니다.',
+ group: '메이트 전원의 승인을 기다리는 중입니다.',
+ },
+};
+
export const DATE_SELECT_TOAST_MESSAGE = '직관 준비를 위해 2일 후 날짜부터 선택 가능해요.';
diff --git a/src/shared/mocks/matchCardData.ts b/src/shared/mocks/matchCardData.ts
deleted file mode 100644
index 1c32b7f6..00000000
--- a/src/shared/mocks/matchCardData.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import type { GroupCardProps, SingleCardProps } from '@components/card/match-card/types/card';
-
-export const singleMockData: SingleCardProps[] = [
- {
- id: 1,
- type: 'single',
- nickname: '다진마늘땨땨',
- age: '26',
- gender: '여성',
- team: '두산',
- style: '직관 먹방러',
- awayTeam: 'LG',
- homeTeam: '두산',
- stadium: '경기장',
- date: '2025-07-22',
- status: '승인 대기 중',
- imgUrl: ['https://example.jpg'],
- chips: ['두산', '직관먹방러'],
- },
- {
- id: 2,
- type: 'single',
- nickname: '다진마늘땨땨',
- age: '26',
- gender: '여성',
- team: '두산',
- style: '직관 먹방러',
- awayTeam: 'LG',
- homeTeam: '두산',
- stadium: '경기장',
- date: '2025-07-22',
- status: '요청 대기 중',
- imgUrl: ['https://example.jpg'],
- chips: ['두산', '직관먹방러'],
- },
- {
- id: 3,
- type: 'single',
- nickname: '다진마늘땨땨',
- age: '26',
- gender: '여성',
- team: '두산',
- style: '직관 먹방러',
- awayTeam: 'LG',
- homeTeam: '두산',
- stadium: '경기장',
- date: '2025-07-22',
- status: '새 요청',
- imgUrl: ['https://example.jpg'],
- chips: ['두산', '직관먹방러'],
- },
- {
- id: 4,
- type: 'single',
- nickname: '다진마늘땨땨',
- age: '26',
- gender: '여성',
- team: '두산',
- style: '직관 먹방러',
- awayTeam: 'LG',
- homeTeam: '두산',
- stadium: '경기장',
- date: '2025-07-22',
- status: '대기 중',
- imgUrl: ['https://example.jpg'],
- chips: ['두산', '직관먹방러'],
- },
- {
- id: 5,
- type: 'single',
- nickname: '다진마늘땨땨',
- age: '26',
- gender: '여성',
- team: '두산',
- style: '직관 먹방러',
- awayTeam: 'LG',
- homeTeam: '두산',
- stadium: '경기장',
- date: '2025-07-22',
- status: '매칭 실패',
- imgUrl: ['https://example.jpg'],
- chips: ['두산', '직관먹방러'],
- },
- {
- id: 6,
- type: 'single',
- nickname: '다진마늘땨땨',
- age: '26',
- gender: '여성',
- team: '두산',
- style: '직관 먹방러',
- awayTeam: 'LG',
- homeTeam: '두산',
- stadium: '경기장',
- date: '2025-07-22',
- status: '매칭 완료',
- imgUrl: ['https://example.jpg'],
- chips: ['두산', '직관먹방러'],
- },
-];
-
-export const groupMockData: GroupCardProps[] = [
- {
- id: 1,
- type: 'group',
- nickname: '다진마늘땨땨',
- awayTeam: 'LG',
- homeTeam: '두산',
- stadium: '경기장',
- date: '2025-07-22',
- status: '새 요청',
- count: 2,
- imgUrl: ['https://example.jpg', 'https://example.jpg'],
- },
- {
- id: 2,
- type: 'group',
- nickname: '다진마늘땨땨',
- awayTeam: 'LG',
- homeTeam: '두산',
- stadium: '경기장',
- date: '2025-07-22',
- status: '요청 대기 중',
- count: 2,
- imgUrl: ['https://example.jpg'],
- },
- {
- id: 3,
- type: 'group',
- nickname: '다진마늘땨땨',
- awayTeam: 'LG',
- homeTeam: '두산',
- stadium: '경기장',
- date: '2025-07-22',
- status: '승인 완료',
- count: 2,
- imgUrl: ['https://example.jpg', 'https://example.jpg', 'https://example.jpg'],
- },
- {
- id: 4,
- type: 'group',
- nickname: '다진마늘땨땨',
- awayTeam: 'LG',
- homeTeam: '두산',
- stadium: '경기장',
- date: '2025-07-22',
- status: '매칭 실패',
- count: 3,
- imgUrl: ['https://example.jpg'],
- },
-];
diff --git a/src/shared/mocks/mockMatchData.ts b/src/shared/mocks/mockMatchData.ts
deleted file mode 100644
index abd8f5ca..00000000
--- a/src/shared/mocks/mockMatchData.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import type { DetailedCardProps } from '@components/card/match-card/types/card';
-
-export const mockMatchData: (DetailedCardProps & { id: number; matchId: number })[] = [
- {
- id: 1,
- matchId: 1,
- type: 'detailed',
- nickname: '두리번러',
- date: '2024-08-01',
- imgUrl: ['baseball.png'],
- chips: ['열정응원러', '두산'],
- awayTeam: '기아',
- homeTeam: '두산',
- stadium: '잠실야구장',
- age: '25',
- gender: '남성',
- introduction: '주변을 잘 살펴요 👀',
- matchRate: 98,
- team: '두산',
- style: '감성적인',
- color: 'inactive',
- },
- {
- id: 2,
- matchId: 1,
- type: 'detailed',
- nickname: 'LG직관왕',
- date: '2024-08-01',
- imgUrl: ['baseball.png'],
- chips: ['직관먹방러'],
- awayTeam: 'LG',
- homeTeam: 'SSG',
- stadium: '문학야구장',
- age: '28',
- gender: '여성',
- introduction: '이젠 좀 이기자',
- matchRate: 91,
- team: 'LG',
- style: '열정적인',
- color: 'inactive',
- },
- {
- id: 3,
- matchId: 2,
- type: 'detailed',
- nickname: '두리번러',
- date: '2024-08-01',
- imgUrl: ['/img/sample1.png'],
- chips: ['열정응원러'],
- awayTeam: '기아',
- homeTeam: '두산',
- stadium: '잠실야구장',
- age: '25',
- gender: '남성',
- introduction: '주변을 잘 살펴요 👀',
- matchRate: 98,
- team: '두산',
- style: '감성적인',
- color: 'inactive',
- },
- {
- id: 4,
- matchId: 2,
- type: 'detailed',
- nickname: 'LG직관왕',
- date: '2024-08-01',
- imgUrl: ['/img/sample2.png'],
- chips: ['직관먹방러'],
- awayTeam: 'LG',
- homeTeam: 'SSG',
- stadium: '문학야구장',
- age: '28',
- gender: '여성',
- introduction: '이젠 좀 이기자',
- matchRate: 91,
- team: 'LG',
- style: '열정적인',
- color: 'active',
- },
-];
diff --git a/src/shared/mocks/mockMatchReceiveData.ts b/src/shared/mocks/mockMatchReceiveData.ts
deleted file mode 100644
index 926a77a0..00000000
--- a/src/shared/mocks/mockMatchReceiveData.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { ChipColor } from '@components/card/match-card/types/card';
-
-export const mockMateReceive = {
- id: 1,
- type: 'detailed',
- nickname: '두밥비',
- date: '2025-07-20',
- imgUrl: ['/images/profile-1.png'],
- chips: ['LG', '열정응원러'] as ChipColor[],
- awayTeam: 'LG',
- homeTeam: '두산',
- stadium: '잠실야구장',
- age: '20',
- gender: '여성',
- team: 'lg',
- style: '열정응원러',
- introduction: '잠실더비전 보러가고파요',
- matchRate: 99,
-};
diff --git a/src/shared/styles/theme.css b/src/shared/styles/theme.css
index a0dcaf92..55cd88d2 100644
--- a/src/shared/styles/theme.css
+++ b/src/shared/styles/theme.css
@@ -27,6 +27,7 @@
--color-background: #f6f7f9;
--color-overlay: rgba(0, 0, 0, 0.3);
--color-kakao-brand: #fee500;
+ --color-owner: #fddb00;
/* Grayscale */
--color-gray-black: #1a1a1a;
@@ -76,6 +77,7 @@
--z-bottom-nav: 8;
--z-under-header-section: 5;
--z-modal: 7;
+ --z-card-owner: 5;
--z-card-profile-1: 4;
--z-card-profile-2: 3;
--z-card-profile-3: 2;
diff --git a/src/shared/types/match-types.ts b/src/shared/types/match-types.ts
index e03e1e9c..d4756c09 100644
--- a/src/shared/types/match-types.ts
+++ b/src/shared/types/match-types.ts
@@ -12,6 +12,7 @@ export interface baseMate {
homeTeam: string;
stadium: string;
date: string; // YYYY-MM-DD
+ isCreated: boolean;
}
/**
@@ -176,7 +177,7 @@ export interface postMatchConditionRequest {
* /v1/users/match-stage/direct?
*/
export interface getSingleMatchStatusResponse {
- mates: singleMatchMate[];
+ results: singleMatchMate[];
}
/**
@@ -203,6 +204,7 @@ export interface getGroupMatchMate {
status: string;
count: number;
imgUrl: string[];
+ isCreated: boolean;
}
/**
diff --git a/src/shared/utils/show-error-toast.tsx b/src/shared/utils/show-error-toast.tsx
index c491415a..347714b8 100644
--- a/src/shared/utils/show-error-toast.tsx
+++ b/src/shared/utils/show-error-toast.tsx
@@ -1,14 +1,40 @@
import Icon from '@components/icon/icon';
import { toast } from 'react-compact-toast';
-export const showErrorToast = (message: string, offset: string = '8.3rem') => {
+type ShowErrorToastOptions = {
+ icon?: boolean;
+ offset?: string;
+};
+
+const DEFAULT_OFFSET = '8.3rem';
+const DEFAULT_AUTOCLOSE = 3000;
+const isValidRem = (v?: string) => !!v && /^-?\d+(\.\d+)?rem$/i.test(v);
+
+const showErrorToastCore = (message: string, opts?: ShowErrorToastOptions) => {
+ const { icon = true, offset } = opts ?? {};
+ const resolvedOffset = isValidRem(offset) ? offset : DEFAULT_OFFSET;
+
toast({
text: message,
- icon: ,
- autoClose: 3000,
+ icon: icon ? : undefined,
+ autoClose: DEFAULT_AUTOCLOSE,
position: 'bottomCenter',
- offset,
+ offset: resolvedOffset,
className:
- 'min-h-[4.5rem]! max-w-[calc(43rem-3.2rem)] w-[calc(100%-3.2rem)] cap_14_m text-gray-white rounded-[12px] bg-gray-900',
+ '!min-h-[4.5rem] max-w-[calc(43rem-3.2rem)] w-[calc(100%-3.2rem)] cap_14_m text-gray-white rounded-[12px] bg-gray-900',
+ });
+};
+
+export const showErrorToast = (
+ message: string,
+ offsetOrOptions?: string | ShowErrorToastOptions,
+ showIcon?: boolean,
+) => {
+ if (typeof offsetOrOptions === 'object' && offsetOrOptions) {
+ return showErrorToastCore(message, offsetOrOptions);
+ }
+ return showErrorToastCore(message, {
+ offset: typeof offsetOrOptions === 'string' ? offsetOrOptions : undefined,
+ icon: typeof showIcon === 'boolean' ? showIcon : undefined,
});
};