Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dd21379
deploy: prod launch - v1 (#179)
heesunee Jul 15, 2025
5cc8126
deploy: prod-launch (#239)
heesunee Jul 17, 2025
440f6b9
fix: 뒤로가기 클릭 시에도 탭 유지 (#226)
Dubabbi Jul 17, 2025
1b7a5f9
refactor: 매칭 탭 뒤로가기 (#226)
Dubabbi Jul 17, 2025
40266aa
fix: (QA/2) 매칭 생성 로딩 중 수정 (#246)
yeeeww Jul 17, 2025
7b78ef5
refactor: auth 쿼리 무효화 (#240) (#245)
bongtta Jul 17, 2025
fc7490b
fix: (QA/2) 매칭 성사 그라데이션 수정 (#248)
Dubabbi Jul 17, 2025
2561f82
fix: (QA/1) splash 도중 loading 제거 및 profile-svg 교체 (#250)
heesunee Jul 17, 2025
e71fbb4
feat: 그룹 매칭 요청 api 연동 (#253)
bongtta Jul 17, 2025
2e3302d
fix: 홈 탭 마진 조정, 요청 거절 페이지 폰트색 수정(#256) (#258)
yeeeww Jul 17, 2025
e573ab3
fix: 배포를 위한 develop 수정 (#260)
heesunee Jul 17, 2025
1ebef6c
fix: resolve conflict (#261)
heesunee Jul 17, 2025
a7272aa
Merge branch 'main' into develop
heesunee Jul 17, 2025
c8ffb44
fix: develop conflict 해결 (#264)
heesunee Jul 17, 2025
d7ed459
style: footer background 옵션 수정 (#266)
heesunee Jul 18, 2025
5d8058e
feat: (QA/1) 온보딩 BottomSheet 홈과 통일 (#267)
yeeeww Jul 18, 2025
b1d2ed5
fix: (QA/1) 회원가입 화면 디자인 QA 반영 (#262)
heesunee Jul 18, 2025
726ce63
refactor: 코드 리뷰 반영 (#226)
Dubabbi Jul 18, 2025
2f84562
feat: (QA/2) 매칭 요청에 유저 닉네임 추가 (#271)
Dubabbi Jul 18, 2025
8e4dc05
fix: (QA/2) 유저 카드에서 응원하는 팀이 없는 경우 레이아웃 깨지는 문제 해결 (#273)
Dubabbi Jul 18, 2025
ebb0c96
fix: 뒤로가기 클릭 시에도 탭 유지 (#226)
Dubabbi Jul 17, 2025
d72e2e2
refactor: 매칭 탭 뒤로가기 (#226)
Dubabbi Jul 17, 2025
c0ae80b
refactor: 코드 리뷰 반영 (#226)
Dubabbi Jul 18, 2025
d9ffad8
Merge branch 'fix/#226/back-tab' of https://github.com/MATEBALL/MATEB…
Dubabbi Jul 18, 2025
88b629d
fix: 빌드 오류 해결 (#226)
Dubabbi Jul 18, 2025
a33d744
refactor: 필요 없는 타입 제거 (#226)
Dubabbi Jul 18, 2025
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
47 changes: 41 additions & 6 deletions public/svgs/profile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/pages/home/components/calendar-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const CalendarSection = ({
onDateChange(date);
}}
/>
<section className="mt-[3.5rem] flex justify-between">
<section className="mt-[2.5rem] flex justify-between">
<TabList colorMode="home" activeType={activeType} onTabChange={handleTabChange} />
<CalendarButton onOpenBottomSheet={onOpenBottomSheet} />
</section>
Expand Down
18 changes: 18 additions & 0 deletions src/pages/login/login-with-splash.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Splash from '@pages/login/components/splash';
import Login from '@pages/login/login';
import { useEffect, useState } from 'react';

const LoginWithSplash = () => {
const [showSplash, setShowSplash] = useState(true);

useEffect(() => {
const timer = setTimeout(() => setShowSplash(false), 1200);
return () => clearTimeout(timer);
}, []);

if (showSplash) return <Splash />;

return <Login />;
};

export default LoginWithSplash;
4 changes: 2 additions & 2 deletions src/pages/match/components/mate-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ interface MateHeaderProps {
isGroupMatching?: boolean;
}

const MateHeader = ({ isGroupMatching }: MateHeaderProps) => (
const MateHeader = ({ isGroupMatching, nickname }: MateHeaderProps) => (
<section className="gap-[0.8rem] text-center">
<h1 className="title_24_sb text-gray-black">
{isGroupMatching ? MATCHING_HEADER_MESSAGE.group : MATCHING_HEADER_MESSAGE.single}
{MATCHING_HEADER_MESSAGE(nickname, !!isGroupMatching)}
</h1>
<p className="body_16_m text-gray-600">상대의 정보를 확인하고, 매칭을 요청해 보세요.</p>
</section>
Expand Down
5 changes: 1 addition & 4 deletions src/pages/match/components/mate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ const Mate = ({ matchId, onRequestClick, isGroupMatching = true }: MateProps) =>
return (
<div className="h-full flex-col-between">
<section className="w-full flex-col-center gap-[4rem] pt-[4rem]">
<MateHeader
nickname={mates[currentIndex]?.nickname?.[0] ?? ''}
isGroupMatching={isGroupMatching}
/>
<MateHeader nickname={data?.nickname ?? ''} isGroupMatching={isGroupMatching} />
<MateCarousel
mates={mates}
currentIndex={currentIndex}
Expand Down
15 changes: 8 additions & 7 deletions src/pages/match/constants/matching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@ export const MATCHING_COMPLETE_MESSAGE = {
single: '상대방이 요청을 승인하면 매칭이 성사돼요.',
};

export const MATCHING_HEADER_MESSAGE = {
group: '사용자님과 딱 맞는 그룹원이에요!',
single: '사용자님과 딱 맞는 메이트예요!',
};
export const MATCHING_HEADER_MESSAGE = (nickname: string, isGroup: boolean) =>
({
group: `${nickname} 님과 딱 맞는 그룹원이에요!`,
single: `${nickname} 님과 딱 맞는 메이트예요!`,
})[isGroup ? 'group' : 'single'];

export const MATCHING_DESCRIPTION = {
group: {
description: '그룹 매칭은 최대 2건까지 신청할 수 있어요.',
description: '동시에 진행할 수 있는 그룹 매칭은 최대 2개예요.',
subDescription: '단, 하루에 한 경기만 매칭이 성사되며 같은 날짜의 중복 매칭은 불가능해요!',
},
single: {
description: '1:1 매칭은 최대 3건까지 요청할 수 있어요.',
description: '동시에 진행할 수 있는 1:1 매칭은 최대 3개예요.',
subDescription: '단, 하루에 한 경기만 매칭이 성사되며 같은 날짜의 중복 매칭은 불가능해요!',
},
};
Expand All @@ -34,7 +35,7 @@ export const MAX_CREATE_DESCRIPTION = {
};

export const MATCHING_GUIDE_MESSAGE_TITLE = (nickname: string) =>
`${nickname}님을 위한\n맞춤 매칭이 생성되었어요!`;
`${nickname} 님을 위한\n맞춤 매칭이 생성되었어요!`;

export const MATCHING_GUIDE_MESSAGE_DESCRIPTION =
'딱! 맞는 메이트의 요청이 도착하면\n' + "'매칭 현황'에서 확인할 수 있어요.";
Expand Down
14 changes: 14 additions & 0 deletions src/pages/match/create/create.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import Loading from '@pages/loading/loading';
import ButtonSection from '@pages/match/create/components/button-section';
import MatchCardSection from '@pages/match/create/components/match-card-section';
import { isInvalidMatchId } from '@pages/match/utils/match-validators';
import { ROUTES } from '@routes/routes-config';
import { useEffect, useState } from 'react';
import { Navigate, useParams, useSearchParams } from 'react-router-dom';

const Create = () => {
const [searchParams] = useSearchParams();
const { matchId } = useParams();
const [isLoading, setIsLoading] = useState(true);
const matchTypeParam = searchParams.get('type');
const matchType =
matchTypeParam === 'single' || matchTypeParam === 'group' ? matchTypeParam : undefined;

const numericMatchId = Number(matchId);

useEffect(() => {
const timer = setTimeout(() => {
setIsLoading(false);
}, 1500);

return () => clearTimeout(timer);
}, []);
Comment on lines +19 to +25
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

인위적인 로딩 지연 시간 재검토 필요

1.5초의 고정된 로딩 지연이 사용자 경험에 부정적인 영향을 미칠 수 있습니다. 실제 데이터 로딩이나 API 호출 없이 인위적인 지연을 추가하는 것은 권장되지 않습니다.

다음과 같은 대안을 고려해보세요:

- useEffect(() => {
-   const timer = setTimeout(() => {
-     setIsLoading(false);
-   }, 1500);
-   return () => clearTimeout(timer);
- }, []);
+ // 실제 데이터 로딩이 필요한 경우에만 로딩 상태 사용
+ // 또는 더 짧은 지연 시간 (예: 300ms) 사용
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const timer = setTimeout(() => {
setIsLoading(false);
}, 1500);
return () => clearTimeout(timer);
}, []);
// 실제 데이터 로딩이 필요한 경우에만 로딩 상태 사용
// 또는 더 짧은 지연 시간 (예: 300ms) 사용
🤖 Prompt for AI Agents
In src/pages/match/create/create.tsx around lines 19 to 25, the fixed 1.5-second
loading delay is artificially prolonging the loading state without real data
fetching, which can harm user experience. Remove the setTimeout and instead
control the loading state based on actual data loading or API call completion.
Implement proper async data fetching logic and set isLoading to false only after
the data is fully loaded.


if (isInvalidMatchId(matchId?.toString()) || !matchType) {
return <Navigate to={ROUTES.ERROR} replace />;
}
if (isLoading) {
return <Loading />;
}

return (
<div className="h-full flex-col-between gap-[2.4rem] px-[1.6rem] pt-[9.6rem]">
Expand Down
1 change: 1 addition & 0 deletions src/pages/match/groups/mates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const MatesPage = () => {
description={MATCHING_DESCRIPTION.group.description}
subDescription={MATCHING_DESCRIPTION.group.subDescription}
isGroupMatching={true}
matchId={numericMatchId}
/>
)}
</>
Expand Down
22 changes: 22 additions & 0 deletions src/pages/match/hooks/mapMatchData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type {
ChipColor,
GroupCardProps,
SingleCardProps,
} from '@components/card/match-card/types/card';
import type { getGroupMatchMate, singleMatchMate } from '@/shared/types/match-types';

export const mapSingleMatchData = (mates: singleMatchMate[] = []): SingleCardProps[] => {
return mates.map((mate) => ({
...mate,
type: 'single',
imgUrl: [mate.imgUrl],
chips: [mate.team, mate.style].map((v) => v as ChipColor),
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

타입 캐스팅 검증 필요

mate.teammate.styleChipColor 타입으로 캐스팅하고 있는데, 런타임에서 이 값들이 실제로 유효한 ChipColor 값인지 확인이 필요합니다.

다음 스크립트로 ChipColor 타입 정의를 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# ChipColor 타입 정의와 team, style 필드의 타입 호환성 확인
ast-grep --pattern 'type ChipColor = $$$'
ast-grep --pattern 'interface $_ {
  $$$
  team: $_;
  $$$
  style: $_;
  $$$
}'

Length of output: 2459


ChipColor 런타임 검증 로직 추가 필요

mate.teammate.stylestring 타입이므로, 아래와 같이 강제 캐스팅만 하면 유효하지 않은 값이 들어올 수 있습니다. 실제로 ChipColor에 정의된 값인지 검증하는 타입 가드나 필터링 로직을 추가해주세요.

  • Location: src/pages/match/hooks/mapMatchData.ts (라인 13)

제안 예시:

// ChipColor 타입 값인지 확인하는 유틸 함수
const isChipColor = (v: string): v is ChipColor =>
  Object.values(chipVariants).some(variant => variant.bgColor === v);

// mapMatchData 내부
chips: [mate.team, mate.style]
  .filter(isChipColor)       // 유효한 값만
  .map(v => v as ChipColor), // 안전하게 캐스팅

위와 같이 검증 후 캐스팅하거나, 기본값을 지정하는 방식으로 런타임 안정성을 확보해주세요.

🤖 Prompt for AI Agents
In src/pages/match/hooks/mapMatchData.ts at line 13, the code casts mate.team
and mate.style directly to ChipColor without runtime validation, which risks
invalid values. To fix this, implement a type guard function that checks if a
string is a valid ChipColor by comparing against chipVariants values, then
filter mate.team and mate.style using this guard before casting. This ensures
only valid ChipColor values are included, improving runtime safety.

}));
};

export const mapGroupMatchData = (mates: getGroupMatchMate[] = []): GroupCardProps[] => {
return mates.map((mate) => ({
...mate,
type: 'group',
}));
};
42 changes: 42 additions & 0 deletions src/pages/match/hooks/useMatchTabState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { TabType } from '@components/tab/tab/tab-content';
import { ROUTES } from '@routes/routes-config';
import { useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';

export const useMatchTabState = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();

const isTabType = (value: string | null): value is TabType => {
return value === '1:1' || value === '그룹';
};

const tabParam = searchParams.get('tab');
const filterParam = searchParams.get('filter');
const initialTab: TabType = isTabType(tabParam) ? tabParam : '1:1';
const initialFilter = filterParam || '전체';

const [tabState, setTabState] = useState({ type: initialTab, filter: initialFilter });

const updateTabQuery = (type: TabType, filter: string) => {
const query = new URLSearchParams();
query.set('tab', type);
query.set('filter', filter);
navigate(`${ROUTES.MATCH}?${query.toString()}`, { replace: true });
};

const handleTabChange = (type: TabType) => {
setTabState({ type, filter: '전체' });
updateTabQuery(type, '전체');
};

const handleFilterChange = (filter: string) => {
setTabState((prev) => {
const next = { ...prev, filter };
updateTabQuery(next.type, filter);
return next;
});
};

return { tabState, handleTabChange, handleFilterChange };
};
58 changes: 20 additions & 38 deletions src/pages/match/match.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,47 @@
import { matchQueries } from '@apis/match/match-queries';
import type {
ChipColor,
GroupCardProps,
SingleCardProps,
} from '@components/card/match-card/types/card';
import FillTabList from '@components/tab/fill-tab/fill-tab-list';
import type { TabType } from '@components/tab/tab/tab-content';
import TabContent from '@components/tab/tab/tab-content';
import TabList from '@components/tab/tab/tab-list';
import MatchTabPanel from '@pages/match/components/match-tab-pannel';
import { mapGroupMatchData, mapSingleMatchData } from '@pages/match/hooks/mapMatchData';
import { useMatchTabState } from '@pages/match/hooks/useMatchTabState';
import { fillTabItems } from '@pages/match/utils/match-status';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import type { getGroupMatchMate, singleMatchMate } from '@/shared/types/match-types';

const Match = () => {
const [tabState, setTabState] = useState<{ type: TabType; filter: string }>({
type: '1:1',
filter: '전체',
});

const { tabState, handleTabChange, handleFilterChange } = useMatchTabState();
const { type: activeType, filter } = tabState;
const statusParam = filter === '전체' ? '' : filter;

const handleTabChange = (type: TabType) => {
setTabState({ type, filter: '전체' });
};

const handleFilterChange = (filter: string) => {
setTabState((prev) => ({ ...prev, filter }));
};

const { data: singleData } = useQuery<{ mates: singleMatchMate[] }>({
const { data: singleData } = useQuery({
...matchQueries.SINGLE_MATCH_STATUS(statusParam),
enabled: activeType === '1:1',
});

const { data: groupData } = useQuery<{ mates: getGroupMatchMate[] }>({
const { data: groupData } = useQuery({
...matchQueries.GROUP_MATCH_STATUS(statusParam),
enabled: activeType === '그룹',
});

const singleCards: SingleCardProps[] = (singleData?.mates ?? []).map((card) => ({
...card,
type: 'single',
imgUrl: [card.imgUrl],
chips: [card.team, card.style].map((v) => v as ChipColor),
}));

const groupCards: GroupCardProps[] = (groupData?.mates ?? []).map((card) => ({
...card,
type: 'group',
}));

const contentMap = {
'1:1': <MatchTabPanel key="single" cards={singleCards} filter={filter} />,
그룹: <MatchTabPanel key="group" cards={groupCards} filter={filter} />,
'1:1': (
<MatchTabPanel
key="single"
cards={mapSingleMatchData(singleData?.mates)}
filter={filter}
/>
),
그룹: (
<MatchTabPanel
key="group"
cards={mapGroupMatchData(groupData?.mates)}
filter={filter}
/>
),
};

return (
<div className="scrollbar-hide h-full grow flex-col">
<div className="h-full grow flex-col">
<nav className="sticky top-0 z-[var(--z-under-header-section)] w-full bg-gray-100">
<TabList
className="px-[1.6rem]"
Expand Down
74 changes: 18 additions & 56 deletions src/pages/onboarding/components/date-select.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,23 @@
import BottomSheet from '@components/bottom-sheet/bottom-sheet';
import { gameQueries } from '@apis/game/game-queries';
import GameMatchBottomSheet from '@components/bottom-sheet/game-match/game-match-bottom-sheet';
import useBottomSheet from '@components/bottom-sheet/hooks/use-bottom-sheet';
import Button from '@components/button/button/button';
import ButtonGame from '@components/button/button-game/button-game';
import MonthCalendar from '@components/calendar/month-calendar';
import { getInitialSelectedDate } from '@components/calendar/utils/date-grid';
import Icon from '@components/icon/icon';
import { mockGameDatas } from '@mocks/mockGameData';
import { TAB_TYPES } from '@components/tab/tab/constants/tab-type';
import { useQuery } from '@tanstack/react-query';
import { format } from 'date-fns';
import { ko } from 'date-fns/locale';
import { useState } from 'react';

interface DateSelectProps {
onComplete: () => void;
}

const DateSelect = ({ onComplete }: DateSelectProps) => {
const DateSelect = () => {
const initialSelectedDate = getInitialSelectedDate(new Date());

const [selectedDate, setSelectedDate] = useState<Date | null>(initialSelectedDate);
const [currentMonth, setCurrentMonth] = useState<Date>(initialSelectedDate);
const [selectedGameId, setSelectedGameId] = useState<number | null>(null);
const activeType = TAB_TYPES.GROUP;

const { isOpen, open, close } = useBottomSheet();

const handleDateSelect = (date: Date) => {
setSelectedDate(date);
setSelectedGameId(null);
open();
};

Expand All @@ -34,9 +26,10 @@ const DateSelect = ({ onComplete }: DateSelectProps) => {
setSelectedDate(null);
};

const handleGameClick = (id: number) => {
setSelectedGameId(id);
};
const dateStr = format(selectedDate ?? new Date(), 'yyyy-MM-dd');
const { data } = useQuery({
...gameQueries.GAME_LIST(dateStr),
});

return (
<div className="h-full w-full flex-col-between px-[1.6rem] pt-[1.6rem]">
Expand All @@ -56,45 +49,14 @@ const DateSelect = ({ onComplete }: DateSelectProps) => {
/>
</div>

<BottomSheet isOpen={isOpen} onClose={close}>
<div className="flex-col gap-[1.6rem]">
<div className="flex-col gap-[1.3rem] px-[1.6rem] pt-[1.6rem]">
<p className="cap_14_m text-gray-black">
{selectedDate && format(selectedDate, 'M월 d일 EEEE', { locale: ko })}
</p>
<div className="flex-col-center gap-[0.8rem]">
{mockGameDatas.map((game) => (
<ButtonGame
key={game.id}
awayTeam={game.awayTeam}
homeTeam={game.homeTeam}
gameTime={game.gameTime}
stadium={game.stadium}
selected={selectedGameId === game.id}
onClick={() => handleGameClick(game.id)}
/>
))}
</div>
</div>

<div className="flex-col-center gap-[1.2rem]">
<div className="flex-row-center gap-[0.8rem]">
<Icon name="caution" size={1.8} />
<p className="cap_12_m text-gray-600">하루에 한 경기만 매칭 생성이 가능해요.</p>
</div>
<div className="w-full p-[1.6rem]">
<Button
label="맞춤 매칭 생성하기"
disabled={!selectedGameId}
onClick={() => {
close();
onComplete();
}}
/>
</div>
</div>
</div>
</BottomSheet>
<GameMatchBottomSheet
isOpen={isOpen}
onClose={close}
date={format(selectedDate ?? new Date(), 'yyyy-MM-dd')}
gameSchedule={data ?? []}
activeType={activeType}
fromOnboarding={true}
/>
</div>
);
};
Expand Down
Loading