Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 30 additions & 14 deletions src/pages/edit-profile/edit-profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { userMutations } from '@apis/user/user-mutations';
import { userQueries } from '@apis/user/user-queries';
import Button from '@components/button/button/button';
import Divider from '@components/divider/divider';
import Icon from '@components/icon/icon';
import Input from '@components/input/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { cn } from '@libs/cn';
Expand All @@ -14,7 +15,7 @@ import {
EditProfileSchema,
type EditProfileValues,
} from '@pages/edit-profile/schema/EditProfileSchema';
import { GENDER, NO_TEAM_OPTION, TEAMS } from '@pages/onboarding/constants/onboarding';
import { NO_TEAM_OPTION, TEAMS } from '@pages/onboarding/constants/onboarding';
import {
INTRODUCTION_RULE_MESSAGE,
NICKNAME_DUPLICATED,
Expand All @@ -34,9 +35,9 @@ const EditProfile = () => {
const { data } = useQuery(userQueries.MATCH_CONDITION());

const [team, setTeam] = useState<string | undefined>(undefined);
const [gender, setGender] = useState<string | undefined>(undefined);
const [mateTeam, setMateTeam] = useState<string | undefined>(undefined);
const [viewStyle, setViewStyle] = useState<string | undefined>(undefined);
const [avgSeason, setAvgSeason] = useState('');
const [isSubmit, setIsSubmit] = useState(false);
const [nicknameStatus, setNicknameStatus] = useState<NicknameStatus>('idle');

Expand Down Expand Up @@ -73,21 +74,21 @@ const EditProfile = () => {

const initial = {
team: data?.team ?? '',
gender: data?.genderPreference ?? '',
mateTeam: data?.teamAllowed ?? '',
viewStyle: data?.style ?? '',
avgSeason: data?.avgSeason ?? 0,
};

const teamValue = team ?? initial.team;
const genderValue = gender ?? initial.gender;
const viewStyleValue = viewStyle ?? initial.viewStyle;
const mateTeamValue = (teamValue === NO_TEAM_OPTION ? '' : (mateTeam ?? initial.mateTeam)) ?? '';
const avgSeasonValue = avgSeason === '' ? initial.avgSeason : Number(avgSeason);

const isMatchDirty =
teamValue !== initial.team ||
genderValue !== initial.gender ||
mateTeamValue !== initial.mateTeam ||
viewStyleValue !== initial.viewStyle;
viewStyleValue !== initial.viewStyle ||
avgSeasonValue !== initial.avgSeason;

const isSubmitDisabled = !isMatchDirty || isSubmit;

Expand All @@ -97,9 +98,9 @@ const EditProfile = () => {

editMatchCondition({
team: teamValue,
genderPreference: genderValue,
style: viewStyleValue,
teamAllowed: teamValue === NO_TEAM_OPTION ? null : mateTeamValue || null,
avgSeason: avgSeasonValue,
});
};

Expand All @@ -125,6 +126,15 @@ const EditProfile = () => {

{/* 닉네임 */}
<section>
<div className="mb-[2.4rem] flex-col gap-[0.8rem]">
<p className="body_16_m text-gray-black">프로필 이미지</p>
{/* TODO: 프로필 편집 api 연결 */}
<div className="relative w-fit">
<Icon name="profile" size={6.4} />
<Icon name="camera" size={1.6} className="absolute right-0 bottom-0" />
</div>
</div>

<Controller
name="nickname"
control={control}
Expand All @@ -146,7 +156,7 @@ const EditProfile = () => {
/>
)}
/>
<div className="mb-[2.5rem] flex justify-end gap-[0.8rem]">
<div className="mb-[2.4rem] flex justify-end gap-[0.8rem]">
<Button
type="button"
variant={
Expand Down Expand Up @@ -267,12 +277,18 @@ const EditProfile = () => {
onSelect={setViewStyle}
/>

<SelectionGroup
title="선호 성별"
options={GENDER}
selectedValue={genderValue}
onSelect={setGender}
/>
<div className="flex-col gap-[1.6rem]">
<p className="body_16_m text-gray-black">평균 직관 수 </p>
<Input
value={avgSeason}
placeholder={initial.avgSeason.toString()}
inputMode="numeric"
onChange={(e) => {
const numericValue = e.target.value.replace(/\D/g, '').slice(0, 3);
setAvgSeason(numericValue);
}}
/>
</div>
</div>
</section>

Expand Down
65 changes: 65 additions & 0 deletions src/pages/profile/components/profile-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Button from '@components/button/button/button';
import type { ChipColor } from '@components/card/match-card/types/card';
import Chip from '@components/chip/chip';
import Divider from '@components/divider/divider';
import { ROUTES } from '@routes/routes-config';
import { useNavigate } from 'react-router-dom';

interface ProfileCardProps {
nickname: string;
imgUrl: string;
team: string;
style: string;
matchCnt?: number;
avgSeason?: number;
onEditProfile?: () => void;
}

const ProfileCard = ({ nickname, imgUrl, team, style, matchCnt, avgSeason }: ProfileCardProps) => {
const navigate = useNavigate();

return (
<div className="w-full flex-col gap-[1.2rem] rounded-[12px] bg-gray-950 p-[2rem]">
<div className="flex-row-between">
<div className="flex gap-[0.8rem]">
<img
src={imgUrl}
alt={`${nickname} 프로필 이미지`}
className="h-[6rem] w-[6rem] rounded-[60px]"
/>
<div className="flex-col gap-[0.4rem]">
<p className="subhead_18_sb text-gray-white">{nickname}</p>
<div className="flex gap-[0.4rem]">
<Chip label={team} bgColor={team as ChipColor} textColor={team as ChipColor} />
<Chip label={style} bgColor={style as ChipColor} textColor={style as ChipColor} />
Comment on lines +33 to +34
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

as ChipColor 타입 캐스팅이 런타임 오류를 야기할 수 있습니다.

teamstyle 값을 ChipColor로 강제 캐스팅하고 있지만, API에서 반환되는 값이 chipVariants에 정의된 유효한 색상 키와 일치하지 않을 수 있습니다. 이 경우 스타일이 적용되지 않거나 예기치 않은 렌더링이 발생할 수 있습니다.

관련 코드 참조: src/shared/components/card/match-card/types/card.tsChipColor 타입 정의

♻️ 권장 수정: 타입 안전한 매핑 함수 사용
+// 유효한 ChipColor 매핑 (card.ts의 chipVariants 기반)
+const teamToChipColor: Record<string, ChipColor> = {
+  KIA: 'KIA',
+  삼성: '삼성',
+  // ... 다른 팀들 추가
+};
+
+const styleToChipColor: Record<string, ChipColor> = {
+  열정응원러: '열정응원러',
+  경기집중러: '경기집중러',
+  직관먹방러: '직관먹방러',
+};

-<Chip label={team} bgColor={team as ChipColor} textColor={team as ChipColor} />
-<Chip label={style} bgColor={style as ChipColor} textColor={style as ChipColor} />
+<Chip label={team} bgColor={teamToChipColor[team]} textColor={teamToChipColor[team]} />
+<Chip label={style} bgColor={styleToChipColor[style]} textColor={styleToChipColor[style]} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/profile/components/profile-card.tsx` around lines 33 - 34, The code
unsafely casts `team` and `style` to `ChipColor` when rendering <Chip>, which
can cause runtime styling errors if the API returns unexpected values; create a
type-safe mapper function (e.g., normalizeToChipColor or mapToChipVariant) that
accepts the raw `team`/`style` strings, checks them against the valid keys in
`chipVariants` (or the `ChipColor` union from
src/shared/components/card/match-card/types/card.ts), and returns a safe
`ChipColor` fallback when no match is found, then use that mapper when passing
bgColor and textColor to the `Chip` component instead of `team as ChipColor` and
`style as ChipColor`.

</div>
</div>
</div>
<div>
<Button
size="S"
label="프로필 수정"
className="cap_14_sb rounded-[8px]"
onClick={() => navigate(ROUTES.PROFILE_EDIT)}
/>
</div>
</div>

<div className="w-full flex-row-evenly rounded-[8px] bg-gray-700 px-[1.2rem] py-[0.8rem]">
<div className="flex-col-between gap-[0.2rem]">
<p className="cap_14_sb text-gray-400">함께한 매칭</p>
<p className="head_20_sb text-gray-white">{matchCnt}</p>
</div>
<div className="h-[3.3rem]">
<Divider direction="vertical" thickness={0.1} color="bg-gray-600" />
</div>
<div className="flex-col-between gap-[0.2rem]">
<p className="cap_14_sb text-gray-400">시즌 평균 직관</p>
<p className="head_20_sb text-gray-white">{avgSeason}</p>
Comment on lines +51 to +58
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

optional props matchCnt, avgSeasonundefined로 렌더링될 수 있습니다.

matchCntavgSeason은 optional로 선언되어 있지만, 값이 없을 때 fallback 처리 없이 직접 렌더링됩니다. 이 경우 UI에 "undefined" 텍스트가 표시될 수 있습니다.

🛡️ 권장 수정: 기본값 제공
-<p className="head_20_sb text-gray-white">{matchCnt}</p>
+<p className="head_20_sb text-gray-white">{matchCnt ?? 0}</p>
-<p className="head_20_sb text-gray-white">{avgSeason}</p>
+<p className="head_20_sb text-gray-white">{avgSeason ?? 0}</p>
📝 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
<p className="head_20_sb text-gray-white">{matchCnt}</p>
</div>
<div className="h-[3.3rem]">
<Divider direction="vertical" thickness={0.1} color="bg-gray-600" />
</div>
<div className="flex-col-between gap-[0.2rem]">
<p className="cap_14_sb text-gray-400">시즌 평균 직관</p>
<p className="head_20_sb text-gray-white">{avgSeason}</p>
<p className="head_20_sb text-gray-white">{matchCnt ?? 0}</p>
</div>
<div className="h-[3.3rem]">
<Divider direction="vertical" thickness={0.1} color="bg-gray-600" />
</div>
<div className="flex-col-between gap-[0.2rem]">
<p className="cap_14_sb text-gray-400">시즌 평균 직관</p>
<p className="head_20_sb text-gray-white">{avgSeason ?? 0}</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/profile/components/profile-card.tsx` around lines 51 - 58,
matchCnt와 avgSeason이 optional이라 값이 없을 때 "undefined"가 렌더링될 수 있으므로 ProfileCard
컴포넌트에서 matchCnt와 avgSeason에 기본값을 제공하도록 수정하세요; 예를 들어 props 구조분해할 때 또는 렌더링 위치(해당
<p> 요소들에서)에서 matchCnt와 avgSeason을 nullish 병합 연산자(또는 삼항)로 대체값을 넣어 안전하게 표시하도록
변경하고, 필요하면 컴포넌트의 기본 props 선언(defaultProps 또는 파라미터 기본값)을 추가해 undefined를 방지하세요 (참조
심볼: matchCnt, avgSeason, ProfileCard / profile-card.tsx).

</div>
</div>
</div>
);
};

export default ProfileCard;
25 changes: 7 additions & 18 deletions src/pages/profile/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { userMutations } from '@apis/user/user-mutations';
import { userQueries } from '@apis/user/user-queries';
import Button from '@components/button/button/button';
import Card from '@components/card/match-card/card';
import type { ChipColor } from '@components/chip/chip-list';
import Divider from '@components/divider/divider';
import Footer from '@components/footer/footer';
import { FEEDBACK_LINK, REQUEST_LINK } from '@pages/profile/constants/link';
import { ROUTES } from '@routes/routes-config';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import ProfileCard from './components/profile-card';

const Profile = () => {
const navigate = useNavigate();
Expand All @@ -20,25 +18,16 @@ const Profile = () => {

return (
<div className="h-full flex-col-between">
<div className="w-full flex-col-center gap-[1.6rem] px-[1.6rem] pt-[1.6rem] pb-[5.6rem]">
<Card
className="!shadow-none"
type="user"
<div className="w-full flex-col-center gap-[3.2rem] px-[1.6rem] pt-[1.6rem] pb-[5.6rem]">
<ProfileCard
nickname={data.nickname ?? ''}
imgUrl={[data.imgUrl ?? '']}
team={data.team ?? ''}
style={data.style ?? ''}
age={data.age ?? ''}
gender={data.gender ?? ''}
introduction={data.introduction ?? ''}
chips={[(data.team ?? '') as ChipColor, (data.style ?? '') as ChipColor]}
imgUrl={data.imgUrl ?? ''}
matchCnt={data.matchCnt ?? 0}
avgSeason={data.avgSeason ?? 0}
onEditProfile={() => navigate(ROUTES.PROFILE_EDIT)}
/>
Comment on lines +22 to 30
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

onEditProfile prop이 ProfileCard에서 사용되지 않습니다.

onEditProfile 콜백을 ProfileCard에 전달하고 있지만, ProfileCard 컴포넌트 내부에서는 이 prop을 destructure하지 않고 자체적으로 useNavigate를 사용해 네비게이션을 처리하고 있습니다 (profile-card.tsx Line 18, 43 참조).

두 가지 방법 중 하나를 선택하세요:

  1. ProfileCard에서 onEditProfile prop을 사용하도록 수정
  2. 여기서 onEditProfile prop 전달을 제거
♻️ 권장 수정 방안 (옵션 1: ProfileCard에서 prop 사용)

profile-card.tsx 수정:

-const ProfileCard = ({ nickname, imgUrl, team, style, matchCnt, avgSeason }: ProfileCardProps) => {
-  const navigate = useNavigate();
+const ProfileCard = ({ nickname, imgUrl, team, style, matchCnt, avgSeason, onEditProfile }: ProfileCardProps) => {
-            onClick={() => navigate(ROUTES.PROFILE_EDIT)}
+            onClick={onEditProfile}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/profile/profile.tsx` around lines 26 - 34, The passed onEditProfile
prop is unused by ProfileCard; update ProfileCard to accept and use this
callback instead of internally calling useNavigate: add onEditProfile to the
component props/interface (e.g., in the ProfileCardProps type), destructure
onEditProfile in the ProfileCard function, replace the internal navigate-based
edit handler (the code that currently calls navigate(...) inside ProfileCard)
with a call to onEditProfile(), and remove the internal useNavigate usage for
that action; ensure the prop remains optional or provide a safe no-op fallback
if needed.

<Button
size="L"
label="프로필 · 매칭 조건 수정"
onClick={() => navigate(ROUTES.PROFILE_EDIT)}
/>
<Divider thickness={0.4} color="bg-gray-200" />
<section className="w-full flex-col items-start">
<a
href={REQUEST_LINK}
Expand Down
4 changes: 2 additions & 2 deletions src/shared/apis/user/user-mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export const userMutations = {
EDIT_MATCH_CONDITION: () =>
mutationOptions<postMatchConditionRequest, Error, postMatchConditionRequest>({
mutationKey: USER_KEY.MATCH_CONDITION(),
mutationFn: ({ team, teamAllowed, style, genderPreference }) =>
patch(END_POINT.MATCH_CONDITION, { team, teamAllowed, style, genderPreference }),
mutationFn: ({ team, teamAllowed, style, avgSeason }) =>
patch(END_POINT.MATCH_CONDITION, { team, teamAllowed, style, avgSeason }),
}),

AGREEMENT_INFO: () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const buttonVariants = cva(
disabled: 'rounded-[0.8rem] bg-gray-100 text-gray-400',
},
size: {
S: 'w-full px-[1.6rem] py-[0.6rem]',
M: 'w-full px-[0.8rem] py-[1.2rem]',
L: 'w-full px-[0.8rem] py-[1.2rem]',
setting_M: 'w-full p-[0.8rem]',
Expand Down
4 changes: 2 additions & 2 deletions src/shared/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ export const END_POINT = {
USER_INFO: '/v2/users/info',
GET_NICKNAME_CHECK: (nickname: string) =>
`/v2/users/info?nickname=${encodeURIComponent(nickname)}`,
GET_USER_INFO: '/v1/users/info',
GET_USER_INFO: '/v3/users/info',
POST_INFO_NICKNAME: '/v1/users/info/nickname',
POST_EDIT_PROFILE: '/v2/users/info',
MATCH_CONDITION: '/v2/users/match-condition',
MATCH_CONDITION: '/v3/users/match-condition',

// 경기 관련
GET_GAME_SCHEDULE: (date: string) => `/v1/users/game/schedule?date=${date}`,
Expand Down
4 changes: 4 additions & 0 deletions src/shared/styles/custom-utilities.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
@apply flex flex-row justify-between items-center;
}

.flex-row-evenly {
@apply flex flex-row justify-evenly items-center;
}

.flex-row-end {
@apply flex flex-row justify-end items-center;
}
Expand Down
1 change: 1 addition & 0 deletions src/shared/styles/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

/* Grayscale */
--color-gray-black: #1a1a1a;
--color-gray-950: #333333;
--color-gray-900: #3a3a3b;
--color-gray-800: #464748;
--color-gray-700: #656667;
Expand Down
11 changes: 5 additions & 6 deletions src/shared/types/user-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ import type { ApiResponse } from './base-types';
/**
* 유저 정보 조회 응답
* get
* /v1/users/info
* /v3/users/info
*/
export interface getUserInfoResponse {
nickname: string | null;
age: string | null;
gender: string | null;
team: string | null;
style: string | null;
introduction: string | null;
imgUrl: string | null;
matchCnt: number | null;
avgSeason: number | null;
}

/**
Expand Down Expand Up @@ -61,7 +60,7 @@ export interface getMatchConditionResponse {
team: string;
teamAllowed: string | null;
style: string;
genderPreference: string;
avgSeason: number;
}

/**
Expand All @@ -73,5 +72,5 @@ export interface postMatchConditionRequest {
team: string;
teamAllowed: string | null;
style: string;
genderPreference: string;
avgSeason: number;
}
1 change: 1 addition & 0 deletions src/stories/foundations/tokens/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const ETC_COLORS = {

export const GRAYSCALE_COLORS = {
'gray-black': '#1a1a1a',
'gray-950': '#333333',
'gray-900': '#3a3a3b',
'gray-800': '#464748',
'gray-700': '#656667',
Expand Down
Loading